• 项目中的自定义注解


    1、实际开发场景

    在项目的实际开发中,我们肯定会需要使用自定义注解来做到某些我们想要达到的效果

    2、代码开发

    2.1、自定义注解的使用

    2.1.1、controller层

    	//swagger中的注解,用于构建Api文档
        @ApiOperation(value = "新增用户")
        //自定义注解-日志
        @WebLog(info = "新增用户")
        //自定义注解-加解密
        @SecurityParameter
        @PostMapping("/addUser")
        public ResponseEntity<String> addUser(@RequestBody @Validated({User.AddAction.class}) User user) {
            .
            .
            .
            .
            .
            .
            return "新增成功";
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.1.2、对象

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    //开启链式编程
    @Accessors(chain = true)
    //现实实体类型和数据库中的表实现映射
    @TableName(value = "user")
    public class User extends BasicEntity implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        @TableId(value = "id", type = IdType.AUTO)
        @NotNull(groups = {UpdateAction.class}, message = "userId不能为空")
        private Long id;
    
        @ApiModelProperty(value = "分组名称")
        @NotBlank(groups = {AddAction.class, UpdateAction.class}, message = "分组名称不能为空")
        private String roleName;
    
        @ApiModelProperty(value = "性别")
        @NotNull(groups = {AddAction.class, UpdateAction.class}, message = "性别不能为空")
        @EnumCheck(groups = {AddAction.class, UpdateAction.class}, clazz = Sex.class, message = "性别类型不合法")
        private Sex sex;
    
        @TableField(fill = FieldFill.INSERT)
        @TableLogic(value = "0", delval = "1")
        private Byte isDel;
    
        public interface AddAction {
        }
    
        public interface UpdateAction {
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    2.2、自定义注解 - 校验字段的枚举值类型是否合法

    2.2.1、注解

    //将注解包含在javadoc中
    @Documented
    /**
    * Constraint    详细信息
    * @Null    被注释的元素必须为 null
    * @NotNull    被注释的元素必须不为 null
    * @AssertTrue    被注释的元素必须为 true
    * @AssertFalse    被注释的元素必须为 false
    * @Min(value)    被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    * @Max(value)    被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    * @DecimalMin(value)    被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    * @DecimalMax(value)    被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    * @Size(max, min)    被注释的元素的大小必须在指定的范围内
    * @Digits (integer, fraction)    被注释的元素必须是一个数字,其值必须在可接受的范围内
    * @Past    被注释的元素必须是一个过去的日期
    * @Future    被注释的元素必须是一个将来的日期
    * @Pattern(value)    被注释的元素必须符合指定的正则表达式
    */
    //标明由哪个类执行校验逻辑
    @Constraint(validatedBy = EnumConstraintValidator.class)
    /**
    * 接口、类、枚举、注解
    * @Target(ElementType.TYPE) 
    * 字段、枚举的常量
    * @Target(ElementType.FIELD)
    * 方法
    * @Target(ElementType.METHOD)
    * 方法参数
    * @Target(ElementType.PARAMETER)
    * 构造函数
    * @Target(ElementType.CONSTRUCTOR) 
    * 局部变量
    * @Target(ElementType.LOCAL_VARIABLE)
    * 注解
    * @Target(ElementType.ANNOTATION_TYPE)
    * 包   
    * @Target(ElementType.PACKAGE)
    */
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    /**
    * 注解仅存在于源码中,在class字节码文件中不包含
    * @Retention(RetentionPolicy.SOURCE)
    * 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
    * @Retention(RetentionPolicy.CLASS)    
    * 注解会在class字节码文件中存在,在运行时可以通过反射获取到
    * @Retention(RetentionPolicy.RUNTIME)
    */
    @Retention(RetentionPolicy.RUNTIME)
    //可以在注解的地方重复标注注解
    @Repeatable(EnumCheck.List.class)
    public @interface EnumCheck {
    
        String message() default "{javax.validation.constraints.EnumCheck.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        /**
         * 枚举类
         */
        Class<? extends EnumValidator> clazz();
    
        /**
         * 调用的方法名称
         */
        String method() default "getValue";
    
        @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @interface List {
            EnumCheck[] value();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    2.2.2、校验类

    //需要继承ConstraintValidator<注解名,校验的Object>
    public class EnumConstraintValidator implements ConstraintValidator<EnumCheck, Object> {
    
        /**
         * 注解对象
         */
        private EnumCheck annotation;
    
        /**
         * 初始化方法
         *
         * @param constraintAnnotation 注解对象
         */
         //方法initialize()不需要一定有,使用可用来对注解定义的参数进行初始化给isValid()方法进行使用
        @Override
        public void initialize(EnumCheck constraintAnnotation) {
            this.annotation = constraintAnnotation;
        }
    
    	//方法isValid()必须实现,是校验逻辑所在的位置
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            if (Objects.isNull(value)) {
                return true;
            }
            Object[] enumConstants = annotation.clazz().getEnumConstants();
            try {
                Method method = annotation.clazz().getMethod(annotation.method());
                for (Object enumConstant : enumConstants) {
                    if (method.invoke(value).equals(method.invoke(enumConstant))) {
                        return true;
                    }
                }
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                return false;
            }
            return false;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    2.2.3、枚举接口

    public interface EnumValidator {
    
        Object getValue();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.2.4、枚举对象

    public enum Sex implements EnumValidator {
    
        MAN(0, "男"),
        WOMAN(1, "女");
    
        @EnumValue
        @JsonValue
        private Integer type;
    
        private String desc;
    
        Sex(Integer type, String desc) {
            this.type = type;
            this.desc = desc;
        }
    
        public Integer getType() {
            return type;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public static Sex getByType(int type) {
            for (Sex sex : values()) {
                if (sex.type == type) {
                    return sex;
                }
            }
            return null;
        }
    
        @Override
        public Object getValue() {
            return type;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    2.3、自定义注解 - 日志

    2.3.1、注解

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD})
    @Documented
    public @interface WebLog {
    
        /**
         * 日志方法描述信息
         */
        String info() default "";
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.3.2、切面

    //@Aspect:作用是把当前类标识为一个切面供容器读取
    @Aspect
    @Component
    @Slf4j
    //切面加载顺序
    @Order(1)
    public class RequestAspect {
    
        private static ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
    
    	//@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
        @Pointcut("@annotation(xxx.xxx.xxx.WebLog)")
        public void webLog() {
        }
    
    	//@Before:标识一个前置增强方法,相当于BeforeAdvice的功能
        @Before("webLog()")
        public void doBefore(JoinPoint joinPoint) {
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            String ip = request.getRemoteAddr();
            String httpMethod = request.getMethod();
            String url = request.getRequestURL().toString();
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            WebLog webLog = signature.getMethod().getAnnotation(WebLog.class);
            String apiInfo = webLog.info();
            log.info("~~~Start~~~收到来自 {} 的 {} 请求,URL: {},开始执行后台方法:{},全路径({}.{}),入参为:{}~~~",
                    ip, httpMethod, url, apiInfo,
                    joinPoint.getSignature().getDeclaringTypeName(),
                    joinPoint.getSignature().getName(),
                    JSONObject.toJSONString(getFieldsName(joinPoint)));
        }
    
    	//@Around:环绕增强,相当于MethodInterceptor
        @Around("webLog()")
        public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            WebLog webLog = signature.getMethod().getAnnotation(WebLog.class);
            String apiInfo = webLog.info();
            long startTime = System.currentTimeMillis();
            Object result = proceedingJoinPoint.proceed();
            log.info("~~~End~~~方法 {}.{} ({}) 执行结束,耗时:{}秒,返回值为:{}~~~",
                    proceedingJoinPoint.getSignature().getDeclaringTypeName(),
                    proceedingJoinPoint.getSignature().getName(),
                    apiInfo,
                    (float) (System.currentTimeMillis() - startTime) / 1000,
                    JSONObject.toJSONString(result));
            return result;
        }
    
        /**
         * 获取参数列表
         */
        private static Map<String, Object> getFieldsName(JoinPoint joinPoint) {
            // 参数值
            Object[] args = joinPoint.getArgs();
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            String[] parameterNames = pnd.getParameterNames(method);
            if (parameterNames != null) {
                Map<String, Object> paramMap = new HashMap<>(parameterNames.length);
                for (int i = 0; i < parameterNames.length; i++) {
                    paramMap.put(parameterNames[i], args[i]);
                }
                return paramMap;
            }
            return Collections.emptyMap();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    2.4、自定义注解 - 加解密

    2.4.1、注解

    /**
     * 请求数据解密
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Mapping
    @Documented
    public @interface SecurityParameter {
    
        /**
         * 入参是否解密,默认解密
         */
        boolean inDecode() default true;
    
        /**
         * 出参是否加密,默认加密
         */
        boolean outEncode() default true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2.4.2、请求数据解密

    /**
     * 请求数据解密
     */
     //范围 - 在括号内的包下生效
    @ControllerAdvice(basePackages = "xxx.xxx.controller")
    public class DecodeRequestBodyAdvice implements RequestBodyAdvice {
    
        private static final Logger logger = LoggerFactory.getLogger(DecodeRequestBodyAdvice.class);
    
        @Override
        public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
            return true;
        }
    
        @Override
        public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
            return body;
        }
    
        @Override
        public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
            try {
                boolean encode = false;
                if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
                    //获取注解配置的包含和去除字段
                    SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
                    //入参是否需要解密
                    encode = serializedField.inDecode();
                }
                if (encode) {
                    logger.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密");
                    return new MyHttpInputMessage(inputMessage);
                } else {
                    return inputMessage;
                }
            } catch (Exception e) {
                e.printStackTrace();
                logger.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
                return inputMessage;
            }
        }
    
        @Override
        public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
            return body;
        }
    
        class MyHttpInputMessage implements HttpInputMessage {
            private HttpHeaders headers;
    
            private InputStream body;
    
            public MyHttpInputMessage(HttpInputMessage inputMessage) throws Exception {
                this.headers = inputMessage.getHeaders();
                this.body = IOUtils.toInputStream(AesEncryptUtils.decryptAES(IOUtils.toString(inputMessage.getBody(), "UTF-8")));
            }
    
            @Override
            public InputStream getBody() throws IOException {
                return body;
            }
    
            @Override
            public HttpHeaders getHeaders() {
                return headers;
            }
    
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70

    2.4.3、返回数据加密

    /**
     * 返回数据加密
     */
    @ControllerAdvice(basePackages = "xxx.xxx.controller")
    public class EncodeResponseBodyAdvice implements ResponseBodyAdvice {
    
        private final static Logger logger = LoggerFactory.getLogger(EncodeResponseBodyAdvice.class);
    
        @Override
        public boolean supports(MethodParameter methodParameter, Class aClass) {
            return true;
        }
    
        @Override
        public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
            boolean encode = false;
            if (methodParameter.getMethod().isAnnotationPresent(SecurityParameter.class)) {
                //获取注解配置的包含和去除字段
                SecurityParameter serializedField = methodParameter.getMethodAnnotation(SecurityParameter.class);
                //出参是否需要加密
                encode = serializedField.outEncode();
            }
            if (encode) {
                logger.info("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行加密");
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    String result = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
                    return AesEncryptUtils.encryptAES(result);
                } catch (Exception e) {
                    e.printStackTrace();
                    logger.error("对方法method :【" + methodParameter.getMethod().getName() + "】返回数据进行解密出现异常:" + e.getMessage());
                }
            }
            return body;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.4.4、前后端数据传输加密工具类

    /**
     * 前后端数据传输加密工具类
     *
     * @author monkey
     */
    @Slf4j
    public class AesEncryptUtils {
    
        /*
         * 加密用的Key 可以用26个字母和数字组成 例如使用AES-128-CBC加密模式,key需要为16位。
         */
        private static final String KEY = "xxxxxxxxxxxxxxxx";
        private static final String IV = "xxxxxxxxxxxxxxxx";
        //参数分别代表 算法名称/加密模式/数据填充方式
        private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS7Padding";
    
        static {
            Security.addProvider(new BouncyCastleProvider());
        }
    
        /**
         * AES算法加密明文
         *
         * @param data 明文
         * @return 密文
         */
        public static String encryptAES(String data) {
            try {
                Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
                SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
                // CBC模式,需要一个向量iv,可增加加密算法的强度
                IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes("UTF-8"));
                cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
                byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
                // BASE64做转码。
                return AesEncryptUtils.encode(encrypted).trim();
            } catch (Exception e) {
                log.error("{} 加密失败", data, e);
                return null;
            }
        }
    
        /**
         * AES算法解密密文
         *
         * @param data 密文
         * @return 明文
         */
        public static String decryptAES(String data) {
            try {
                if (StringUtils.isEmpty(data)) {
                    return null;
                }
                //先用base64解密
                byte[] encrypted = AesEncryptUtils.decode(data);
                Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
                SecretKeySpec keyspec = new SecretKeySpec(KEY.getBytes("UTF-8"), "AES");
                IvParameterSpec ivspec = new IvParameterSpec(IV.getBytes("UTF-8"));
                cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
                byte[] original = cipher.doFinal(encrypted);
                String originalString = new String(original);
                return originalString.trim();
            } catch (Exception e) {
                log.error("{} 解密失败", data, e);
                return data;
            }
        }
    
        /**
         * 编码
         *
         * @param byteArray
         * @return
         */
        public static String encode(byte[] byteArray) {
            return new String(new Base64().encode(byteArray));
        }
    
        /**
         * 解码
         *
         * @param base64EncodedString
         * @return
         */
        public static byte[] decode(String base64EncodedString) {
            return new Base64().decode(base64EncodedString);
        }
    
        /**
         * 用于对GET请求中的查询字符串进行解密
         *
         * @param params GET请求中的查询字符串
         */
        public static Map<String, Object> decryptQueryParams(Map<String, String> params) {
            Map<String, Object> doAfter = new HashMap<>();
            for (Map.Entry<String, String> entry : params.entrySet()) {
                if (!StringUtils.isEmpty(entry.getValue())) {
                    doAfter.put(entry.getKey(), decryptAES(entry.getValue()));
                }
            }
            return doAfter;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
  • 相关阅读:
    MATLAB | kmeans聚类如何绘制更强的聚类边界(决策边界)
    利用PHP的特性做免杀Webshell
    java--自增自减运算符
    基于HTML5的机器猫在线商城设计与实现
    第五章React路由
    项目踩坑—跨域问题
    软件测试秋招技术面试(面经)
    C3P0和Druid数据库连接池的使用
    浅摇自动化测试
    代码随想录算法训练营第三十六天| 435. 无重叠区间 763.划分字母区间 56. 合并区间
  • 原文地址:https://blog.csdn.net/NewBeeMu/article/details/126719327