当前位置:   article > 正文

SpringBoot 实现数据加密脱敏(注解 + 反射 + AOP)_springboot 对敏感数据加密解密

springboot 对敏感数据加密解密

响应政府要求,商业软件应保证用户基本信息不被泄露,不能直接展示用户手机号,身份证,地址等敏感信息。

根据上面场景描述,我们可以分析出两个点。

  • 不被泄露说明用户信息应被加密储存;

  • 不能直接展示说明用户信息应脱敏展示;

解决方案

  • 傻瓜式编程: 将项目中关于用户信息实体类的字段,比如姓名,手机号,身份证,地址等,在新增进数据库之前,对数据进行加密处理;在列表中展示用户信息时,对数据库中的数据进行解密脱敏,然后返回给前端;

  • 切入式编程: 将项目中关于用户信息实体类的字段用注解给标记,新增用户信息实体类(这里我们用UserBO来表示,给UserBO里面的name,phone字段添加@EncryptField),返回用户信息实体类(这里我们用UserDO来表示,给UserDO里面的name,phone字段添加@DecryptField);然后利用@EncryptField@DecryptField做为切入点,以切面的形式实现加密,解密脱敏;

傻瓜式编程不是说傻,而是相当于切入式编程,傻瓜式编程需要对用户信息相关的所有接口进行加密,解密脱敏的逻辑处理,这里改动的地方就比较多,风险高,重复操作相同的逻辑,工作量大,后期不好维护;切入式编程只需要对用户信息字段添加注解,对有注解的字段统一进行加密,解密脱敏逻辑处理,操作方便,高聚合,易维护;

方案实现

傻瓜式编程没什么难度,这里我给大家有切入式编程来实现;在实现之前,跟大家预热一下注解,反射,AOP的知识;

注解实战

创建注解

创建一个只能标记在方法上的注解:

  1. package com.weige.javaskillpoint.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD)         //METHOD 说明该注解只能用在方法上
  7. @Retention(RetentionPolicy.RUNTIME) //RUNTIME 说明该注解在运行时生效
  8. public @interface Encryption {
  9. }

创建一个只能标记在字段上的注解:

  1. package com.weige.javaskillpoint.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.FIELD)           //FIELD 说明该注解只能用在字段上
  7. @Retention(RetentionPolicy.RUNTIME)  //RUNTIME 说明该注解在运行时生效
  8. public @interface EncryptField {
  9. }

创建一个标记在字段上,且有值的注解:

  1. package com.weige.javaskillpoint.annotation;
  2. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. @Target(ElementType.FIELD)
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface DecryptField {
  10.  // 注解是可以有值的,这里可以为数组,String,枚举等类型
  11.  // DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value(); 这里的field是指当前标记的字段
  12.     DesensitizationEnum value(); 
  13. }

注解使用

创建枚举

  1. package com.weige.javaskillpoint.enums;
  2. public enum DesensitizationEnum {
  3.     name,     // 用户信息姓名脱敏
  4.     address,  // 用户信息地址脱敏
  5.     phone;    // 用户信息手机号脱敏
  6. }

创建UserDO类

  1. package com.weige.javaskillpoint.entity;
  2. import com.weige.javaskillpoint.annotation.DecryptField;
  3. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  4. import com.weige.javaskillpoint.utils.AesUtil;
  5. import java.lang.reflect.Field;
  6. // 用户信息返回实体类
  7. public class UserDO {
  8.     @DecryptField(DesensitizationEnum.name)
  9.     private String name;
  10.     @DecryptField(DesensitizationEnum.address)
  11.     private String address;
  12.     public String getName() {
  13.         return name;
  14.     }
  15.     public void setName(String name) {
  16.         this.name = name;
  17.     }
  18.     public String getAddress() {
  19.         return address;
  20.     }
  21.     public void setAddress(String address) {
  22.         this.address = address;
  23.     }
  24.     public UserDO(String name, String address) {
  25.         this.name = name;
  26.         this.address = address;
  27.     }
  28.     public static void main(String[] args) throws IllegalAccessException {
  29.         // 生成并初始化对象
  30.         UserDO userDO = new UserDO("梦想是什么","湖北省武汉市");
  31.         // 反射获取当前对象的所有字段
  32.         Field[] fields = userDO.getClass().getDeclaredFields();
  33.         // 遍历字段
  34.         for (Field field : fields) {
  35.             // 判断字段上是否存在@DecryptField注解
  36.             boolean hasSecureField = field.isAnnotationPresent(DecryptField.class);
  37.             // 存在
  38.             if (hasSecureField) {
  39.                 // 暴力破解 不然操作不了权限为private的字段
  40.                 field.setAccessible(true);
  41.                 // 如果当前字段在userDo中不为空 即name,address字段有值
  42.                 if (field.get(userDO) != null) {
  43.                     // 获取字段上注解的value
  44.                     DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value();
  45.                     // 控制台输出
  46.                     System.out.println(desensitizationEnum);
  47.                     // 根据不同的value值 我们可以对字段进行不同逻辑的脱敏 比如姓名脱敏-魏*,手机号脱敏-187****2275 
  48.                 }
  49.             }
  50.         }
  51.     }
  52. }

反射实战

创建UserBO类

  1. package com.weige.javaskillpoint.entity;
  2. import com.weige.javaskillpoint.annotation.EncryptField;
  3. import java.lang.reflect.Field;
  4. // 用户信息新增实体类
  5. public class UserBO {
  6.     @EncryptField
  7.     private String name;
  8.     @EncryptField
  9.     private String address;
  10.     public String getName() {
  11.         return name;
  12.     }
  13.     public void setName(String name) {
  14.         this.name = name;
  15.     }
  16.     public String getAddress() {
  17.         return address;
  18.     }
  19.     public void setAddress(String address) {
  20.         this.address = address;
  21.     }
  22.     public UserBO(String name, String address) {
  23.         this.name = name;
  24.         this.address = address;
  25.     }
  26.     @Override
  27.     public String toString() {
  28.         return "UserBO{" +
  29.                 "name='" + name + '\'' +
  30.                 ", address='" + address + '\'' +
  31.                 '}';
  32.     }
  33.     public static void main(String[] args) throws IllegalAccessException {
  34.         UserBO userBO = new UserBO("周传雄","湖北省武汉市");
  35.         Field[] fields = userBO.getClass().getDeclaredFields();
  36.         for (Field field : fields) {
  37.             boolean annotationPresent = field.isAnnotationPresent(EncryptField.class);
  38.             if(annotationPresent){
  39.                 // 当前字段内容不为空
  40.                 if(field.get(userBO) != null){
  41.                     // 这里对字段内容进行加密
  42.                     Object obj = encrypt(field.get(userBO));
  43.                     // 字段内容加密过后 通过反射重新赋给该字段
  44.                     field.set(userBO, obj);
  45.                 }
  46.             }
  47.         }
  48.         System.out.println(userBO);
  49.     }
  50.     public static Object encrypt(Object obj){
  51.         return "加密: " + obj;
  52.     }
  53. }

AOP实战

切入点:

  1. package com.weige.javaskillpoint.controller;
  2. import com.weige.javaskillpoint.annotation.Encryption;
  3. import com.weige.javaskillpoint.entity.UserBO;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. @RestController
  10. @RequestMapping("/encrypt")
  11. @Slf4j
  12. public class EncryptController {
  13.     @PostMapping("/v1")
  14.     @Encryption  // 切入点
  15.     public UserBO insert(@RequestBody UserBO user) {
  16.         log.info("加密后对象:{}", user);
  17.         return user;
  18.     }
  19. }

切面:

  1. package com.weige.javaskillpoint.aop;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.aspectj.lang.annotation.Pointcut;
  7. import org.springframework.stereotype.Component;
  8. @Slf4j
  9. @Aspect
  10. @Component
  11. public class EncryptAspect {
  12.     //拦截需加密注解 切入点
  13.     @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")
  14.     public void point() {
  15.     }
  16.     @Around("point()") //环绕通知
  17.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  18.         //加密逻辑处理
  19.         encrypt(joinPoint);
  20.         return joinPoint.proceed();
  21.     }
  22. }

为什么这里要使用AOP:无论是注解,反射,都需要一个启动方法,我上面演示的是通过main函数来启动。使用AOP,项目启动后,只要调用切入点对应的方法,就会根据切入点来形成一个切面,进行统一的逻辑增强;如果大家熟悉SpringMVC,SpringMVC提供了 ResponseBodyAdvice 和 RequestBodyAdvice两个接口,这两个接口可以对请求和响应进行预处理,就可以不需要使用AOP;

加密解密脱敏实战

项目目录:

图片

pom.xml文件:

  1. <dependencies>
  2.     <!--Springboot项目自带 -->
  3.     <dependency>
  4.         <groupId>org.springframework.boot</groupId>
  5.         <artifactId>spring-boot-starter</artifactId>
  6.     </dependency>
  7.     <dependency>
  8.         <groupId>org.springframework.boot</groupId>
  9.         <artifactId>spring-boot-starter-test</artifactId>
  10.         <scope>test</scope>
  11.     </dependency>
  12.     <!--Springboot Web项目 -->
  13.     <dependency>
  14.         <groupId>org.springframework.boot</groupId>
  15.         <artifactId>spring-boot-starter-web</artifactId>
  16.     </dependency>
  17.     <!--lombok -->
  18.     <dependency>
  19.         <groupId>org.projectlombok</groupId>
  20.         <artifactId>lombok</artifactId>
  21.         <version>1.18.22</version>
  22.     </dependency>
  23.     <!-- hutool  -->
  24.     <dependency>
  25.         <groupId>cn.hutool</groupId>
  26.         <artifactId>hutool-all</artifactId>
  27.         <version>5.7.20</version>
  28.     </dependency>
  29.  <!-- 切面 aop  -->
  30.     <dependency>
  31.         <groupId>org.aspectj</groupId>
  32.         <artifactId>aspectjweaver</artifactId>
  33.         <version>1.9.7</version>
  34.     </dependency>
  35. </dependencies>
实体类

用户信息新增实体类 :UserBO

  1. package com.weige.javaskillpoint.entity;
  2. import com.weige.javaskillpoint.annotation.EncryptField;
  3. // 实体类
  4. public class UserBO {
  5.     @EncryptField
  6.     private String name;
  7.     @EncryptField
  8.     private String address;
  9.     public String getName() {
  10.         return name;
  11.     }
  12.     public void setName(String name) {
  13.         this.name = name;
  14.     }
  15.     public String getAddress() {
  16.         return address;
  17.     }
  18.     public void setAddress(String address) {
  19.         this.address = address;
  20.     }
  21.     public UserBO(String name, String address) {
  22.         this.name = name;
  23.         this.address = address;
  24.     }
  25.     @Override
  26.     public String toString() {
  27.         return "UserBO{" +
  28.                 "name='" + name + '\'' +
  29.                 ", address='" + address + '\'' +
  30.                 '}';
  31.     }
  32. }

用户信息返回实体类 :UserDO

  1. package com.weige.javaskillpoint.entity;
  2. import com.weige.javaskillpoint.annotation.DecryptField;
  3. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  4. // 实体类
  5. public class UserDO {
  6.     @DecryptField(DesensitizationEnum.name)
  7.     private String name;
  8.     @DecryptField(DesensitizationEnum.address)
  9.     private String address;
  10.     public String getName() {
  11.         return name;
  12.     }
  13.     public void setName(String name) {
  14.         this.name = name;
  15.     }
  16.     public String getAddress() {
  17.         return address;
  18.     }
  19.     public void setAddress(String address) {
  20.         this.address = address;
  21.     }
  22.     public UserDO(String name, String address) {
  23.         this.name = name;
  24.         this.address = address;
  25.     }
  26. }
脱敏枚举
  1. package com.weige.javaskillpoint.enums;
  2. public enum DesensitizationEnum {
  3.     name,
  4.     address,
  5.     phone;
  6. }
注解

解密字段注解(字段):

  1. package com.weige.javaskillpoint.annotation;
  2. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7. @Target(ElementType.FIELD)
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface DecryptField {
  10.     DesensitizationEnum value();
  11. }

解密方法注解(方法 作切入点):

  1. package com.weige.javaskillpoint.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface Decryption {
  9. }

加密字段注解(字段):

  1. package com.weige.javaskillpoint.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.FIELD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface EncryptField {
  9. }

加密方法注解(方法 作切入点):

  1. package com.weige.javaskillpoint.annotation;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Target(ElementType.METHOD)
  7. @Retention(RetentionPolicy.RUNTIME)
  8. public @interface Encryption {
  9. }
控制层

解密 Controller:

  1. package com.weige.javaskillpoint.controller;
  2. import com.weige.javaskillpoint.annotation.Decryption;
  3. import com.weige.javaskillpoint.entity.UserDO;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RequestMapping;
  6. import org.springframework.web.bind.annotation.RestController;
  7. @RestController
  8. @RequestMapping("/decrypt")
  9. public class DecryptController {
  10.     @GetMapping("/v1")
  11.     @Decryption
  12.     public UserDO decrypt() {
  13.         return new UserDO("7c29e296e92893476db5f9477480ba7f""b5c7ff86ac36c01dda45d9ffb0bf73194b083937349c3901f571d42acdaa7bae");
  14.     }
  15. }

加密 Controller:

  1. package com.weige.javaskillpoint.controller;
  2. import com.weige.javaskillpoint.annotation.Encryption;
  3. import com.weige.javaskillpoint.entity.UserBO;
  4. import lombok.extern.slf4j.Slf4j;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestBody;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. @RestController
  10. @RequestMapping("/encrypt")
  11. @Slf4j
  12. public class EncryptController {
  13.     @PostMapping("/v1")
  14.     @Encryption
  15.     public UserBO insert(@RequestBody UserBO user) {
  16.         log.info("加密后对象:{}", user);
  17.         return user;
  18.     }
  19. }
切面

解密脱敏切面:

  1. package com.weige.javaskillpoint.aop;
  2. import com.weige.javaskillpoint.annotation.DecryptField;
  3. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  4. import com.weige.javaskillpoint.utils.AesUtil;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.aspectj.lang.ProceedingJoinPoint;
  7. import org.aspectj.lang.annotation.Around;
  8. import org.aspectj.lang.annotation.Aspect;
  9. import org.aspectj.lang.annotation.Pointcut;
  10. import org.springframework.stereotype.Component;
  11. import java.lang.reflect.Field;
  12. import java.util.ArrayList;
  13. import java.util.Collection;
  14. import java.util.List;
  15. import java.util.Objects;
  16. @Slf4j
  17. @Aspect
  18. @Component
  19. public class DecryptAspect {
  20.     //拦截需解密注解
  21.     @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Decryption)")
  22.     public void point() {
  23.     }
  24.     @Around("point()")
  25.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  26.         //解密
  27.         return decrypt(joinPoint);
  28.     }
  29.     public Object decrypt(ProceedingJoinPoint joinPoint) {
  30.         Object result = null;
  31.         try {
  32.             Object obj = joinPoint.proceed();
  33.             if (obj != null) {
  34.                 //抛砖引玉 ,可自行扩展其他类型字段的判断
  35.                 if (obj instanceof String) {
  36.                     decryptValue();
  37.                 } else {
  38.                     result = decryptData(obj);
  39.                 }
  40.             }
  41.         } catch (Throwable e) {
  42.             e.printStackTrace();
  43.         }
  44.         return result;
  45.     }
  46.     private Object decryptData(Object obj) throws IllegalAccessException {
  47.         if (Objects.isNull(obj)) {
  48.             return null;
  49.         }
  50.         if (obj instanceof ArrayList) {
  51.             decryptList(obj);
  52.         } else {
  53.             decryptObj(obj);
  54.         }
  55.         return obj;
  56.     }
  57.     private void decryptObj(Object obj) throws IllegalAccessException {
  58.         Field[] fields = obj.getClass().getDeclaredFields();
  59.         for (Field field : fields) {
  60.             boolean hasSecureField = field.isAnnotationPresent(DecryptField.class);
  61.             if (hasSecureField) {
  62.                 field.setAccessible(true);
  63.                 if (field.get(obj) != null) {
  64.                     String realValue = (String) field.get(obj);
  65.                     DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value();
  66.                     String value = (StringAesUtil.decrypt(realValue,desensitizationEnum);
  67.                     field.set(obj, value);
  68.                 }
  69.             }
  70.         }
  71.     }
  72.     private void decryptList(Object obj) throws IllegalAccessException {
  73.         List<Object> result = new ArrayList<>();
  74.         if (obj instanceof ArrayList) {
  75.             result.addAll((Collection<?>) obj);
  76.         }
  77.         for (Object object : result) {
  78.             decryptObj(object);
  79.         }
  80.     }
  81.     private void decryptValue() {
  82.         log.info("根据对象进行解密脱敏,单个字段不做处理!");
  83.     }
  84. }

加密切面:

  1. package com.weige.javaskillpoint.aop;
  2. import com.weige.javaskillpoint.annotation.EncryptField;
  3. import com.weige.javaskillpoint.entity.UserBO;
  4. import com.weige.javaskillpoint.utils.AesUtil;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.aspectj.lang.ProceedingJoinPoint;
  7. import org.aspectj.lang.annotation.Around;
  8. import org.aspectj.lang.annotation.Aspect;
  9. import org.aspectj.lang.annotation.Pointcut;
  10. import org.springframework.stereotype.Component;
  11. import java.lang.reflect.Field;
  12. @Slf4j
  13. @Aspect
  14. @Component
  15. public class EncryptAspect {
  16.     //拦截需加密注解
  17.     @Pointcut("@annotation(com.weige.javaskillpoint.annotation.Encryption)")
  18.     public void point() {
  19.     }
  20.     @Around("point()")
  21.     public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
  22.         //加密
  23.         encrypt(joinPoint);
  24.         return joinPoint.proceed();
  25.     }
  26.     public void encrypt(ProceedingJoinPoint joinPoint) {
  27.         Object[] objects;
  28.         try {
  29.             objects = joinPoint.getArgs();
  30.             if (objects.length != 0) {
  31.                 for (Object object : objects) {
  32.                     if (object instanceof UserBO) {
  33.                         Field[] fields = object.getClass().getDeclaredFields();
  34.                         for (Field field : fields) {
  35.                             if (field.isAnnotationPresent(EncryptField.class)) {
  36.                                 field.setAccessible(true);
  37.                                 if (field.get(object) != null) {
  38.                                     // 进行加密
  39.                                     Object o = field.get(object);
  40.                                     Object encrypt = AesUtil.encrypt(field.get(object));
  41.                                     field.set(object, encrypt);
  42.                                 }
  43.                             }
  44.                         }
  45.                     }
  46.                 }
  47.             }
  48.         } catch (Exception e) {
  49.             log.error(e.getMessage());
  50.         }
  51.     }
  52. }
工具类

加密工具类:AesUtil

  1. package com.weige.javaskillpoint.utils;
  2. import cn.hutool.core.util.CharsetUtil;
  3. import cn.hutool.crypto.SecureUtil;
  4. import cn.hutool.crypto.symmetric.AES;
  5. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  6. public class AesUtil {
  7.     // 默认16位 或 128 256位
  8.     public static String AES_KEY = "Wk#qerdfdshbd910";
  9.     public static AES aes = SecureUtil.aes(AES_KEY.getBytes());
  10.     public static Object encrypt(Object obj) {
  11.         return aes.encryptHex((String) obj);
  12.     }
  13.     public static Object decrypt(Object obj, DesensitizationEnum desensitizationEnum) {
  14.         // 解密
  15.         Object decrypt = decrypt(obj);
  16.         // 脱敏
  17.         return DesensitizationUtil.desensitization(decrypt, desensitizationEnum);
  18.     }
  19.     public static Object decrypt(Object obj) {
  20.         return aes.decryptStr((String) obj, CharsetUtil.CHARSET_UTF_8);
  21.     }
  22. }

脱敏工具类:DesensitizationUtil

  1. package com.weige.javaskillpoint.utils;
  2. import cn.hutool.core.util.StrUtil;
  3. import com.weige.javaskillpoint.enums.DesensitizationEnum;
  4. public class DesensitizationUtil {
  5.     public static Object desensitization(Object obj, DesensitizationEnum desensitizationEnum) {
  6.         Object result;
  7.         switch (desensitizationEnum) {
  8.             case name:
  9.                 result = strUtilHide(obj, 1);
  10.                 break;
  11.             case address:
  12.                 result = strUtilHide(obj, 3);
  13.                 break;
  14.             default:
  15.                 result = "";
  16.         }
  17.         return result;
  18.     }
  19.     /**
  20.      * start0开始
  21.      */
  22.     public static Object strUtilHide(String obj, int start, int end) {
  23.         return StrUtil.hide(obj, startend);
  24.     }
  25.     public static Object strUtilHide(Object obj, int start) {
  26.         return strUtilHide(((String) obj), start, ((String) obj).length());
  27.     }
  28. }

完结

以上代码不难,大伙复制到本地跑一遍,基本就能理解;愿每一位程序员少走弯路!

最后说一句(求关注!别白嫖!)

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、转发、在看。

关注公众号:woniuxgg,在公众号中回复:笔记  就可以获得蜗牛为你精心准备的java实战语雀笔记,回复面试、开发手册、有超赞的粉丝福利!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/920932
推荐阅读
相关标签
  

闽ICP备14008679号