自定义注解实现功能

169 阅读4分钟

1. 自定义注解 实现赋值和校验

1.1. 自定义注解

import java.lang.annotation.*;

/**
 * @Author: Julbreeze
 * @Date: 2024/8/29 17:07
 * @Version: v1.0.0
 * @Description: 自定义 性别赋值 注解
 **/
@Documented
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface InitSex {
    enum SEX_TYPE {MAN, WOMAN}

    SEX_TYPE sex() default SEX_TYPE.MAN;
}
/**
 * @Author: Julbreeze
 * @Date: 2024/8/29 17:10
 * @Version: v1.0.0
 * @Description: 自定义 年龄校验 注解
 **/
@Documented
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ValidateAge {
    /**
     * 最小值
     * @return
     */
    int min() default 18;

    /**
     * 最大值
     * @return
     */
    int max() default 99;

    /**
     * 默认值
     * @return
     */
    int value() default 20;
}

1.2. 定义实体类

@Data
public class User {
    private String username;

    @ValidateAge(min = 20, max = 35, value = 22)
    private int age;

    @InitSex(sex = InitSex.SEX_TYPE.MAN)
    private String sex;

}

1.3. 测试调用

package customAnnotation.assignmentAndVerification.test;

import customAnnotation.assignmentAndVerification.annotation.InitSex;
import customAnnotation.assignmentAndVerification.annotation.ValidateAge;
import customAnnotation.assignmentAndVerification.domain.User;
import java.lang.reflect.Field;


/**
 * @Author: Julbreeze
 * @Date: 2024/8/30 9:46
 * @Version: v1.0.0
 * @Description: TODO
 **/
public class Test {
    public static void main(String[] args) throws IllegalAccessException {
        User user = new User();
        initUser(user);
        boolean checkUser = checkUser(user);
        System.out.println(checkUser);
    }

    // 校验
    static boolean checkUser(User user) throws IllegalAccessException {
        // 获取 User 类中所有的属性,getFields 无法获得 private 属性
        Field[] fields = User.class.getDeclaredFields();

        boolean result = true;
        // 遍历所有属性
        for (Field field:fields) {
            // 如果属性上有此注解,则进行赋值操作
            if (field.isAnnotationPresent(ValidateAge.class)) {
                ValidateAge validateAge = field.getAnnotation(ValidateAge.class);
                field.setAccessible(true);
                int age = (int)field.get(user);
                if (age < validateAge.min() || age > validateAge.max()) {
                    result = false;
                    System.out.println("年龄值不符合条件!");
                }
            }
        }
        return result;
    }
    
    // 赋值
    static void initUser(User user) throws IllegalAccessException {
        // 获取 User 类中所有的属性,getFields 无法获得 private 属性
        Field[] fields = User.class.getDeclaredFields();
        // 遍历所有属性
        for (Field field:fields) {
            if (field.isAnnotationPresent(InitSex.class)) {
                InitSex init = field.getAnnotation(InitSex.class);
                field.setAccessible(true);
                // 设置属性的性别
                field.set(user,init.sex().toString());
                System.out.println("完成属性值的修改,修改值为:" + init.sex().toString());
            }

        }
    }
}

2. 自定义注解+AOP 实现日志打印

2.1. 项目结构

2.2. 添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>logstarter</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.3</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>

        <!-- 避免编写那些冗余的 Java 样板式代码,如 get、set 方法等 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>

        <!-- Web 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- aop 切面 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.16.1</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.16.1</version>
        </dependency>

        <!-- 解决 Jackson Java 8 新日期 API 的序列化问题 -->
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
        </dependency>
    </dependencies>
</project>

注意:parent 添加

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.3</version>
</parent>

2.3. 添加自定义注解

新建 aspect 包,放置切面相关的功能类,在包下建 ApiOperationLog 注解。

package cn.julbreeze.aspect;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperationLog {
    /**
     * API 功能描述
     *
     * @return
     */
    String description() default "";
}

元注解说明:

  • @Retention(RetentionPolicy.RUNTIME):这个元注解用于指定注解的保留策略,即注解在何时生效。RetentionPolicy.RUNTIME 表示该注解将在运行时保留,这意味着它可以通过反射在运行时被访问和解析。
  • @Target({ElementType.METHOD}): 这个元注解用于指定注解的目标元素,即可以在哪些地方使用这个注解。ElementType.METHOD 表示该注解只能用于方法上。这意味着您只能在方法上使用这个特定的注解。
  • @Documented: 这个元注解用于指定被注解的元素是否会出现在生成的Java文档中。如果一个注解使用了 @Documented,那么在生成文档时,被注解的元素及其注解信息会被包含在文档中。这可以帮助文档生成工具(如 JavaDoc)在生成文档时展示关于注解的信息。

2.4. 创建 JSON 工具类

创建 utils 包,然后创建 JSONUtils 类用于在日志切面中打印出入参为 JSON 字符串。

package cn.julbreeze.util;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;


public class JsonUtils {
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    /**
     *  将对象转换为 JSON 字符串
     * @param obj
     * @return
     */
    @SneakyThrows
    public static String toJsonString(Object obj) {
        return OBJECT_MAPPER.writeValueAsString(obj);
    }
}

上面代码使用了 Spring Boot 内置的 JSON 工具Jackson , 同时,创建了一个静态的 ObjectMapper 类,并写个一个 toJsonString 方法,用于将传入的对象打印成 JSON 字符串。

2.5. 定义日志切面类

aspect 包下,新建切面类 ApiOperationLogAspect

package cn.julbreeze.aspect;

import cn.julbreeze.util.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;

@Aspect
@Component
@Slf4j
public class ApiOperationLogAspect {
    /** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码 */
    @Pointcut("@annotation(cn.julbreeze.aspect.ApiOperationLog)")
    public void apiOperationLog() {}

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("apiOperationLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        // 请求开始时间
        long startTime = System.currentTimeMillis();

        // 获取被请求的类和方法
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = joinPoint.getSignature().getName();

        // 请求入参
        Object[] args = joinPoint.getArgs();
        // 入参转 JSON 字符串
        String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));

        // 功能描述信息
        String description = getApiOperationLogDescription(joinPoint);

        // 打印请求相关参数
        log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} <----------------- ",
                description, argsJsonStr, className, methodName);

        // 执行切点方法
        Object result = joinPoint.proceed();

        // 执行耗时
        long executionTime = System.currentTimeMillis() - startTime;

        // 打印出参等相关信息
        log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} <----------------- ",
                description, executionTime, JsonUtils.toJsonString(result));

        return result;
    }

    /**
     * 获取注解的描述信息
     * @param joinPoint
     * @return
     */
    private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {
        // 1. 从 ProceedingJoinPoint 获取 MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 2. 使用 MethodSignature 获取当前被注解的 Method
        Method method = signature.getMethod();

        // 3. 从 Method 中提取 LogExecution 注解
        ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);

        // 4. 从 LogExecution 注解中获取 description 属性
        return apiOperationLog.description();
    }

    /**
     * 转 JSON 字符串
     * @return
     */
    private Function<Object, String> toJsonStr() {
        return JsonUtils::toJsonString;
    }
}

功能核心代码为:

2.6. 测试

2.6.1. 启动类中添加扫描注解

package cn.julbreeze;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan({"cn.julbreeze.*"})
public class WeblogWebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WeblogWebApplication.class, args);
    }
}

@ComponentScan 注解指定扫描类

2.6.2. 添加实体类

创建 model 包,添加 User 类

package cn.julbreeze.model;

import lombok.Data;

@Data
public class User {
    // 用户名
    private String username;
    // 性别
    private Integer sex;
}

2.6.3. 添加测试接口

创建 controller 包,添加 TestController 类

package cn.julbreeze.controller;

import cn.julbreeze.aspect.ApiOperationLog;
import cn.julbreeze.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;


@RestController
@Slf4j
public class TestController {
    @PostMapping("/test")
    @ApiOperationLog(description = "测试接口")
    public User test(@RequestBody User user) {
        // 返参
        return user;
    }
}

2.6.4. 测试结果

启动项目后,使用 Apipost 调用接口。

地址:localhost:8080/test

参数:

{
    "username": "admin",
    "sex": 1
}

结果: