zoukankan      html  css  js  c++  java
  • java实现签名验签详解(含所有代码)

    第一部分:什么是签名验签?

    私钥:可以解密公钥加密的数据

    公钥:可以解密私钥加密的数据

    也就是说公钥和私钥之间可以互相加解密

    公钥加密私钥解密称之为——加解密

    私钥加密公钥解密称之为——签名验签

    签名:使用私钥对数据进行加密,该操作称之为——签名

    验签:使用与私钥对应的公钥进行解密,该操作称之为——验签

    到此知道什么是公钥什么是私钥,以及区别和可以用来干嘛的了。那么下面开始进入正题(如果公钥私钥和签名验签的概念还有不明白的朋友请自行百度,这篇博客的重点是关于如何通过java代码在实现签名验签)

    第二部分:java实现签名验签

    废话:数据在网络中通信,安全一直是一个比较核心的问题和困扰,博主在2018年的时候,参与过一个支付项目的开发,凡是鉴于当时的水平原文,对于数据安全那一块的开发并没有参与,仅仅是使用了别人写好的api然后调用。印象比较深刻的就是pfx文件和cer文件。当时就知道pfx是用来签名的,cer文件是用来验签的。别的就不知道了。后来到了现在的这家公司,我也到了支付小组,发现这边的数据传输是通过时间戳和一些约定的其他的参数来做一个MD5摘要(这里强调下,MD5不是加密,而是一种信息摘要的算法,是一种散列函数),瞬间就感觉好low的。然后我就会想起之前公司的方式。花了大量的时间去网上查阅资料和看博客。但是结果发现很多都是你抄我我抄你,而且网上的很多根本就没法用于生产,你们不信自己百度就知道了。你去看看你的证书或者是私钥怎么给对方,直接将串给对方吗?这样是不是显然没有达到生成的级别

    总体思路:先介绍下总体思路,有助于读者更好的理解本博客的内容。服务端,创建根证书(相当于ca机构,https协议之所以能够保证数据的安全传输,其核心就是签名验签,如果您对这部分也不了解,或者说想学习下这部分,请给博主留言。只要有一人想知道。我就不惜下班后加班加点写博客。为你们解释清楚),然后通过根证书来创建实际来签名验签的证书,当然,除了根证书,这样的用来签名验签数据的证书需要有两套。为什么需要两套呢?

    ①服务端自己的一套公私钥(服务端的公钥是需要先提供给客户端的)。这一套的作用是:当服务端向客户端传输数据的时候,服务端使用服务端的私钥进行签名。然后客户端使用服务端的公钥验签,这样客户端可以验证服务端的身份和数据是否被篡改

    ②客户端自己也有一套公私钥(当然客户端的这一套是需要服务端提供的,服务端将客户端这套的私钥提供给客户端,同时服务端需要保留客户端的公钥)。这一套的作用是:当客户端向服务端传输数据的时候,客户端需要通过客户端的私钥来签名,而服务端刚好可以使用客户端的公钥来验签,以判断数据在传输的过程中是否被篡改过

    明白没?没明白看代码。我会注释的很详细的

    2.1、创建根证书

      1 import com.example.signature.util.IssueCertUtils;
      2 import org.slf4j.Logger;
      3 import org.slf4j.LoggerFactory;
      4 import sun.security.tools.keytool.CertAndKeyGen;
      5 import sun.security.x509.X500Name;
      6 
      7 import java.io.*;
      8 import java.security.*;
      9 import java.security.cert.CertificateException;
     10 import java.security.cert.X509Certificate;
     11 
     12 /**
     13  * fileName:IssueRootCert
     14  *
     15  * @author :zyz
     16  * Date    :2020/1/15 11:29
     17  * -------------------------
     18  * 功能和描述:颁发根证书
     19  **/
     20 public class IssueRootCert {
     21     public static final Logger logger = LoggerFactory.getLogger(IssueRootCert.class);
     22     private static SecureRandom secureRandom;
     23 
     24     static {
     25         //定义随机数来源
     26         try {
     27             secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN");
     28         } catch (NoSuchAlgorithmException e) {
     29             logger.error("算法不存在");
     30         } catch (NoSuchProviderException e) {
     31             logger.error("该随机数提供者不存在");
     32         }
     33     }
     34 
     35     /**
     36      * 定义pfx根证书文件
     37      */
     38     public static final String ROOT_ISSUE_PFX_FILE = "D:\signverify\rootcert\ROOTCA.pfx";
     39 
     40     /**
     41      * 定义私钥证书的密码
     42      */
     43     public static final String ROOT_ISSUE_PFX_PASSWORD = "123456";
     44     /**
     45      * 定义crt根证书文件
     46      */
     47     public static final String ROOT_ISSUE_CRT_FILE = "D:\signverify\rootcert\ROOTCA.cer";
     48 
     49     /**
     50      * 定义根证书的别名
     51      */
     52     public static final String ROOT_ISSUE_ALIAS = "rootca";
     53 
     54     public static void main(String[] args) {
     55         try {
     56             X500Name issue = new X500Name("CN=RootCA,OU=ISI,O=BenZeph,L=CD,ST=SC,C=CN");
     57             issueRootCert(issue);
     58         } catch (IOException e) {
     59             e.printStackTrace();
     60         }
     61 
     62     }
     63 
     64     /**
     65      * 签名算法
     66      */
     67     public static final String ALGORITHM = "MD5WithRSA";
     68 
     69     public static void issueRootCert(X500Name x500Name) {
     70         try {
     71             CertAndKeyGen certAndKeyGen = new CertAndKeyGen("RSA", ALGORITHM, null);
     72             //设置生成密钥时使用的随机数的来源
     73             certAndKeyGen.setRandom(secureRandom);
     74 
     75             //设置密钥长度,太短容易被攻击破解
     76             certAndKeyGen.generate(1024);
     77 
     78             //时间间隔设置为10年(设置证书有效期的时候需要使用到)
     79             long interval = 60L * 60L * 24L * 3650;
     80             //
     81             X509Certificate x509Certificate = certAndKeyGen.getSelfCertificate(x500Name, interval);
     82 
     83             X509Certificate[] x509Certificates = new X509Certificate[]{x509Certificate};
     84 
     85             IssueCertUtils.createKeyStore(ROOT_ISSUE_ALIAS, certAndKeyGen.getPrivateKey(), ROOT_ISSUE_PFX_PASSWORD.toCharArray(), x509Certificates, ROOT_ISSUE_PFX_FILE);
     86             //根据私钥导出公钥
     87             OutputStream outputStream = new FileOutputStream(new File(ROOT_ISSUE_CRT_FILE));
     88             outputStream.write(x509Certificate.getEncoded());
     89             outputStream.close();
     90         } catch (NoSuchAlgorithmException e) {
     91             e.printStackTrace();
     92         } catch (NoSuchProviderException e) {
     93             e.printStackTrace();
     94         } catch (InvalidKeyException e) {
     95             e.printStackTrace();
     96         } catch (CertificateException e) {
     97             e.printStackTrace();
     98         } catch (SignatureException e) {
     99             e.printStackTrace();
    100         } catch (FileNotFoundException e) {
    101             e.printStackTrace();
    102         } catch (IOException e) {
    103             e.printStackTrace();
    104         }
    105     }
    106 }

    2.2、创建服务端证书

      1 ature.util.IssueCertUtils;
      2   2 import sun.security.tools.keytool.CertAndKeyGen;
      3   3 import sun.security.x509.*;
      4   4 
      5   5 import java.io.*;
      6   6 import java.security.*;
      7   7 import java.security.cert.CertificateException;
      8   8 import java.security.cert.CertificateFactory;
      9   9 import java.security.cert.X509Certificate;
     10  10 import java.util.Date;
     11  11 import java.util.Random;
     12  12 
     13  13 /**
     14  14  * fileName:IssueCert
     15  15  *
     16  16  * @author :zyz
     17  17  * Date    :2020/1/15 12:46
     18  18  * -------------------------
     19  19  * 功能和描述:颁发证书
     20  20  **/
     21  21 public class IssueCert {
     22  22     private static SecureRandom secureRandom;
     23  23 
     24  24     static {
     25  25         try {
     26  26             secureRandom = SecureRandom.getInstance("SHA1PRNG", "SUN");
     27  27         } catch (NoSuchAlgorithmException e) {
     28  28             e.printStackTrace();
     29  29         } catch (NoSuchProviderException e) {
     30  30             e.printStackTrace();
     31  31         }
     32  32     }
     33  33 
     34  34     /**
     35  35      * 私钥证书-用于签名
     36  36      */
     37  37     public static final String ISSUE_PFX_FILE = "D:\signverify\mycert\ISSUE.pfx";
     38  38     /**
     39  39      * 公钥证书-用于验签
     40  40      */
     41  41     public static final String ISSUE_CRT_FILE = "D:\signverify\mycert\ISSUE.cer";
     42  42 
     43  43     /**
     44  44      * 定义pfx根证书文件
     45  45      */
     46  46     public static final String ROOT_ISSUE_PFX_FILE = "D:\signverify\rootcert\ROOTCA.pfx";
     47  47 
     48  48     /**
     49  49      * 定义私钥证书的密码
     50  50      */
     51  51     public static final String ROOT_ISSUE_PFX_PASSWORD = "123456";
     52  52     /**
     53  53      * 定义crt根证书文件
     54  54      */
     55  55     public static final String ROOT_ISSUE_CRT_FILE = "D:\signverify\rootcert\ROOTCA.cer";
     56  56 
     57  57     /**
     58  58      * 定义根证书的别名
     59  59      */
     60  60     public static final String ROOT_ISSUE_ALIAS = "rootca";
     61  61 
     62  62     /**
     63  63      * 证书别名
     64  64      */
     65  65     public static final String ISSUE_ALIAS = "subject";
     66  66 
     67  67     /**
     68  68      * 私钥证书密码
     69  69      */
     70  70     public static final String ISSUE_PASSWORD = "123456";
     71  71 
     72  72 
     73  73     /**
     74  74      * 签名算法
     75  75      */
     76  76     public static final String SIG_ALG = "MD5WithRSA";
     77  77 
     78  78     public static void main(String[] args) {
     79  79         try {
     80  80 
     81  81             X500Name issue = new X500Name("CN=RootCA,OU=ISI,O=BenZeph,L=CD,ST=SC,C=CN");
     82  82             X500Name subject = new X500Name(
     83  83                     "CN=subject,OU=ISI,O=BenZeph,L=CD,ST=SC,C=CN");
     84  84             createIssueCert(issue, subject);
     85  85         } catch (IOException e) {
     86  86             e.printStackTrace();
     87  87         }
     88  88     }
     89  89 
     90  90     /**
     91  91      *
     92  92      */
     93  93     public static void createIssueCert(X500Name rootX500name, X500Name subjectX500Name) {
     94  94         try {
     95  95             CertAndKeyGen certAndKeyGen = new CertAndKeyGen("RSA", SIG_ALG, null);
     96  96 
     97  97             //生成密钥时候使用的随机数的来源
     98  98             certAndKeyGen.setRandom(secureRandom);
     99  99 
    100 100             //设置密钥的大小
    101 101             certAndKeyGen.generate(1024);
    102 102 
    103 103 
    104 104             //设置时间,设置证书有效期的时候需要使用到
    105 105             long validity = 60L * 60L * 24L * 3650;
    106 106             Date startDate = new Date();
    107 107             Date endDate = new Date();
    108 108             endDate.setTime(startDate.getTime() + validity * 1000);
    109 109             //设置证书有效期
    110 110             CertificateValidity interval = new CertificateValidity(startDate, endDate);
    111 111 
    112 112             //获取X509CertInfo对象,并为其添加所有的强制属性
    113 113             X509CertInfo info = new X509CertInfo();
    114 114 
    115 115             info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
    116 116             info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(new Random().nextInt() & 0x7fffffff));
    117 117 
    118 118             AlgorithmId algID = AlgorithmId.get(SIG_ALG);
    119 119             info.set(X509CertInfo.ALGORITHM_ID, new CertificateAlgorithmId(algID));
    120 120 
    121 121             info.set(X509CertInfo.SUBJECT, subjectX500Name);
    122 122 
    123 123             info.set(X509CertInfo.KEY, new CertificateX509Key(certAndKeyGen.getPublicKey()));
    124 124 
    125 125             info.set(X509CertInfo.VALIDITY, interval);
    126 126 
    127 127             info.set(X509CertInfo.ISSUER, rootX500name);
    128 128 
    129 129             PrivateKey privateKey = getPrivateKey();
    130 130 
    131 131             X509CertImpl cert = new X509CertImpl(info);
    132 132             cert.sign(privateKey, SIG_ALG);
    133 133 
    134 134             //X509Certificate certificate = (X509Certificate) cert;
    135 135 
    136 136             X509Certificate x509Certificate = readX509Certificate();
    137 137 
    138 138             X509Certificate[] x509Certificates = new X509Certificate[]{cert, x509Certificate};
    139 139 
    140 140             IssueCertUtils.createKeyStore(ISSUE_ALIAS, certAndKeyGen.getPrivateKey(), ISSUE_PASSWORD.toCharArray(), x509Certificates, ISSUE_PFX_FILE);
    141 141 
    142 142             OutputStream outputStream = new FileOutputStream(new File(ISSUE_CRT_FILE));
    143 143             outputStream.write(cert.getEncoded());
    144 144             outputStream.close();
    145 145 
    146 146         } catch (NoSuchAlgorithmException e) {
    147 147             e.printStackTrace();
    148 148         } catch (NoSuchProviderException e) {
    149 149             e.printStackTrace();
    150 150         } catch (InvalidKeyException e) {
    151 151             e.printStackTrace();
    152 152         } catch (CertificateException e) {
    153 153             e.printStackTrace();
    154 154         } catch (IOException e) {
    155 155             e.printStackTrace();
    156 156         } catch (SignatureException e) {
    157 157             e.printStackTrace();
    158 158         }
    159 159     }
    160 160 
    161 161 
    162 162     /**
    163 163      * 获取私钥
    164 164      *
    165 165      * @return
    166 166      */
    167 167     private static PrivateKey getPrivateKey() {
    168 168         try {
    169 169             //后去指定类型的KeyStore对象
    170 170             KeyStore keyStore = KeyStore.getInstance("PKCS12");
    171 171             InputStream in = null;
    172 172             in = new FileInputStream(ROOT_ISSUE_PFX_FILE);
    173 173             keyStore.load(in, ROOT_ISSUE_PFX_PASSWORD.toCharArray());
    174 174             in.close();
    175 175             //使用指定的密码来获取指定的别名对应的私钥
    176 176             Key key = keyStore.getKey(ROOT_ISSUE_ALIAS, ROOT_ISSUE_PFX_PASSWORD.toCharArray());
    177 177             return (PrivateKey) key;
    178 178         } catch (KeyStoreException e) {
    179 179             e.printStackTrace();
    180 180         } catch (FileNotFoundException e) {
    181 181             e.printStackTrace();
    182 182         } catch (CertificateException e) {
    183 183             e.printStackTrace();
    184 184         } catch (NoSuchAlgorithmException e) {
    185 185             e.printStackTrace();
    186 186         } catch (IOException e) {
    187 187             e.printStackTrace();
    188 188         } catch (UnrecoverableKeyException e) {
    189 189             e.printStackTrace();
    190 190         }
    191 191         return null;
    192 192     }
    193 193 
    194 194     /**
    195 195      * 读取crt根证书信息
    196 196      *
    197 197      * @return
    198 198      */
    199 199     private static X509Certificate readX509Certificate() {
    200 200         InputStream inputStream = null;
    201 201         try {
    202 202             inputStream = new FileInputStream(ROOT_ISSUE_CRT_FILE);
    203 203             CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    204 204             X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
    205 205 
    206 206             inputStream.close();
    207 207             return certificate;
    208 208         } catch (FileNotFoundException e) {
    209 209             e.printStackTrace();
    210 210         } catch (CertificateException e) {
    211 211             e.printStackTrace();
    212 212         } catch (IOException e) {
    213 213             e.printStackTrace();
    214 214         }
    215 215         return null;
    216 216     }
    217 217 }
    218  

    2.3、创建客户端证书

    和创建客户端方式一模一样

    2.4、工具类

     1 import java.io.FileOutputStream;
     2 import java.io.IOException;
     3 import java.io.OutputStream;
     4 import java.security.Key;
     5 import java.security.KeyStore;
     6 import java.security.KeyStoreException;
     7 import java.security.NoSuchAlgorithmException;
     8 import java.security.cert.Certificate;
     9 import java.security.cert.CertificateException;
    10 
    11 /**
    12  * fileName:IssueCertUtils
    13  *
    14  * @author :zyz
    15  * Date    :2020/1/15 14:30
    16  * -------------------------
    17  * 功能和描述:
    18  **/
    19 public class IssueCertUtils {
    20 
    21     private IssueCertUtils() {
    22     }
    23 
    24     /**
    25      * 证书私钥的存储设施
    26      *
    27      * @param alias        别名(会与对应的privateKey关联)
    28      * @param privateKey   密钥-私钥
    29      * @param password     密码(用来保护私钥的密码)
    30      * @param certificates 证书链
    31      * @param pfxFile      pfx文件
    32      */
    33     public static void createKeyStore(String alias, Key privateKey, char[] password, Certificate[] certificates, String pfxFile) {
    34         try {
    35             //获取指定类型的KeyStore对象
    36             KeyStore keyStore = KeyStore.getInstance("PKCS12");
    37             //加载KeyStore
    38             keyStore.load(null, password);
    39 
    40             //将给定的密钥分配给指定的别名,并用给定的密码来保护它
    41             keyStore.setKeyEntry(alias, privateKey, password, certificates);
    42 
    43             //以下几步就是想私钥证书导出
    44             OutputStream outputStream = new FileOutputStream(pfxFile);
    45             keyStore.store(outputStream, password);
    46             outputStream.close();
    47         } catch (KeyStoreException e) {
    48             e.printStackTrace();
    49         } catch (CertificateException e) {
    50             e.printStackTrace();
    51         } catch (NoSuchAlgorithmException e) {
    52             e.printStackTrace();
    53         } catch (IOException e) {
    54             e.printStackTrace();
    55         }
    56     }
    57 }

    2.5、测试签名验签

    
    
      1 import sun.misc.BASE64Decoder;
      2 import sun.misc.BASE64Encoder;
      3 
      4 import java.io.FileInputStream;
      5 import java.io.IOException;
      6 import java.io.InputStream;
      7 import java.security.KeyStore;
      8 import java.security.PrivateKey;
      9 import java.security.PublicKey;
     10 import java.security.Signature;
     11 import java.security.cert.Certificate;
     12 import java.security.cert.CertificateFactory;
     13 
     14 /**
     15  * fileName:SignVerifyDemo
     16  *
     17  * @author :Miles zhu
     18  * Date    :2020/1/15 14:35
     19  * -------------------------
     20  * 功能和描述:
     21  **/
     22 public class SignVerifyDemo {
     23     public static final String PRIVATE_KEY_PASSWORD = "123456";
     24 
     25     public static final String PUBLIC_KEY_FILE_PATH = "D:\signverify\mycert\ISSUE.cer";
     26     //public static final String PUBLIC_KEY_FILE_PATH = "D:\signature\mykey.cer";
     27     public static final String PRIVATE_KEY_FILE_PATH = "D:\signverify\mycert\ISSUE.pfx";
     28     //public static final String PRIVATE_KEY_FILE_PATH = "D:\signature\mykey.pfx";
     29     public static final String ALIAS_NAME = "subject";
     30     public static final String DEFAULT_UTF8 = "UTF-8";
     31 
     32     public static void main(String[] args) {
     33         String originalData = "Hello我是原始的数据World";
     34         String sign = sign(originalData);
     35         System.out.println(sign);
     36         boolean verify = verify(sign, originalData);
     37         if (verify) {
     38             System.out.println("验签通过");
     39         } else {
     40             System.out.println("验签失败");
     41         }
     42 
     43     }
     44 
     45     /**
     46      * 签名
     47      */
     48     public static String sign(String originalData) {
     49         String base64Sign = "";
     50         try {
     51 
     52             //返回与此给定的别名的密码,并用给定的密钥来恢复它
     53             PrivateKey privateKey = getPrivateKey();
     54 
     55             //返回指定签名的Signature对象
     56             Signature sign = Signature.getInstance("SHA1withRSA");
     57 
     58             //初始化这个用于签名的对象
     59             sign.initSign(privateKey);
     60 
     61             byte[] bysData = originalData.getBytes(DEFAULT_UTF8);
     62 
     63             //使用指定的byte数组更新要签名的数据
     64             sign.update(bysData);
     65             //返回所有已经更新数据的签名字节
     66             byte[] signByte = sign.sign();
     67             //对其进行Base64编码
     68             BASE64Encoder encoder = new BASE64Encoder();
     69             base64Sign = encoder.encode(signByte);
     70         } catch (Exception e) {
     71             System.out.println("签名异常");
     72             e.printStackTrace();
     73         }
     74         return base64Sign;
     75     }
     76 
     77 
     78     /**
     79      * 验签
     80      *
     81      * @param signStr      签名数据
     82      * @param originalData 原始数据
     83      * @return
     84      */
     85     public static boolean verify(String signStr, String originalData) {
     86         System.out.println("开始进行验签,原始数据为:" + originalData);
     87         try {
     88             //从此证书对象中获取公钥
     89             PublicKey publicKey = getPublicKey();
     90 
     91             //将签名数据
     92             BASE64Decoder decoder = new BASE64Decoder();
     93             byte[] signed = decoder.decodeBuffer(signStr);
     94 
     95             //通过Signature的getInstance方法,获取指定签名算法的Signature对象
     96             Signature signature = Signature.getInstance("SHA1withRSA");
     97             //初始化用于验证的对象
     98             signature.initVerify(publicKey);
     99             //使用指定的byte[]更新要验证的数据
    100             signature.update(originalData.getBytes(DEFAULT_UTF8));
    101             //验证传入的签名
    102             return signature.verify(signed);
    103         } catch (Exception e) {
    104             return false;
    105         }
    106 
    107     }
    108 
    109     /**
    110      * 获取公钥
    111      *
    112      * @return
    113      */
    114     private static PublicKey getPublicKey() {
    115         InputStream in = null;
    116         try {
    117             in = new FileInputStream(PUBLIC_KEY_FILE_PATH);
    118             //获取实现指定证书类型的CertificateFactory对象
    119             CertificateFactory cf = CertificateFactory.getInstance("x509");
    120             //生成一个证书对象,并从执行的输入流中读取数据对它进行初始化
    121             Certificate certificate = cf.generateCertificate(in);
    122             //从此证书中获取公钥
    123             return certificate.getPublicKey();
    124         } catch (Exception e) {
    125             e.printStackTrace();
    126             return null;
    127         } finally {
    128             if (null != in) {
    129                 try {
    130                     in.close();
    131                 } catch (IOException e) {
    132                     e.printStackTrace();
    133                 }
    134             }
    135         }
    136     }
    137 
    138 
    139     /**
    140      * 获取私钥
    141      *
    142      * @return
    143      */
    144     private static PrivateKey getPrivateKey() {
    145         InputStream in = null;
    146         try {
    147             in = new FileInputStream(PRIVATE_KEY_FILE_PATH);
    148             //返回指定类型的KeyStore对象
    149             KeyStore keyStore = KeyStore.getInstance("PKCS12");
    150 
    151             char[] pscs = PRIVATE_KEY_PASSWORD.toCharArray();
    152             //从给定的输入流中加载此keyStore
    153             keyStore.load(in, pscs);
    154             //返回与给定别名关联的密钥,并用给定的密码来恢复它
    155             return (PrivateKey) keyStore.getKey(ALIAS_NAME, pscs);
    156         } catch (Exception e) {
    157             e.printStackTrace();
    158             return null;
    159         } finally {
    160             if (null != in) {
    161                 try {
    162                     in.close();
    163                 } catch (IOException e) {
    164                     e.printStackTrace();
    165                 }
    166             }
    167         }
    168     }
    169 }
    
    
    
    
    
     
  • 相关阅读:
    各种不同的mq
    24. Swap Nodes in Pairs
    应用中有使用到集群么?多大规模?
    曹工杂谈:用好verbose,Jar包冲突不再难
    曹工杂谈:Java 类加载还会死锁?这是什么情况?
    Linux下使用docker 拉取 vsftpd 镜像搭建 Ftp 服务器,连接 Ftp 时遇到的错误(425 Failed to establish connection)
    曹工杂谈:Linux服务器上,Spring Boot 原地修改 jar 包配置文件/替换class文件,免去重复上传的麻烦
    曹工杂谈:手把手带你读懂 JVM 的 gc 日志
    曹工杂谈:一道阿里面试题,两个线程交替打印奇偶数
    曹工说Tomcat4:利用 Digester 手撸一个轻量的 Spring IOC容器
  • 原文地址:https://www.cnblogs.com/zyzblogs/p/12197542.html
Copyright © 2011-2022 走看看