实现自定义注解

在 springBoot 中,注解是编码时必不可少的,它可以帮助我们更方便快捷的去开发。常见的注解如:@Autowired、@Slf4j、@Data 等等。

然而这些注解都是别人已经封装好给我们用的,如果我们想自定义一个拥有特别功能的注解,该怎么操作呢?

看完这篇文章,给你答案~

今天以日志功能为例,灵活的运用自定义注解方便快捷的记录每个接口的日志。

在项目中,有众多的接口,如果接口报错了,该怎么去快速定位代码呢?这个时候就要用到日志了。当接口接收到请求的时候,我希望可以记录下来这个接口的各种信息。比如请求时间、请求参数,结束时间等,还可以在接口开始和结束的时候加一个标记,方便出现报错或者 bug 的时候可以快速定位到是哪个接口出了问题。

接下来就用日志系统来介绍自定义注解~


不使用注解

我们可以在接口方法的开头和结尾加一行日志。

1
2
3
4
5
6
7
8
9
public class Mycontroller {
@GetMapping("/get")
public String get(String name,int age){
log.info("Mycontroller**********get********start");
System.out.println("执行了get方法");
log.info("Mycontroller**********get********end");
return name;
}
}

执行结果:

方法的首尾两行都会有一个日志输出,把这个方法的所有运行包在了日志里面,如果个方法出现了问题,就很容易定位到这里了。

比如我故意写一个报错:int i = 2/0;

1
2
3
4
5
6
7
8
@GetMapping("/get")
public String get(String name,int age){
log.info("Mycontroller**********get********start");
int i = 2/0;
System.out.println("执行了get方法");
log.info("Mycontroller**********get********end");
return name;
}

那么输出结果如下:

可以看到报错的上一行日志定位了 get 方法。我们只需在 get 方法里面找问题就好了。

每个方法的首尾都要这样写一个日志记录,代码就会大量冗余。想获取入参的话,还得再写一段代码来实现,并且根据每个方法的入参数量、类型的不同,可能代码也要相应的变动。

既然这个是重复性的工作,而且逻辑上都是:在方法开始之前和方法结束之后做一个标记。那么我们能不能把这一部分抽取出来,只写一次代码,就能作用在每一个方法上面呢?

毫无疑问,答案是可以


使用自定义注解

在一个事情的开始和结束插入另一个事情,很容易联想到 Spring 的一个重要特性 ——AOP

Spring 的 AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架中的一个重要特性,用于将横切关注点从应用程序的主业务逻辑中分离出来,使得关注点的代码可以被模块化、重用,并且与主业务逻辑解耦。

定义注解

使用 @interface 关键字定义一个注解

1
2
3
public @interface LogInfo {

}

在自定义注解中,根据需要标注元注解,如果没有特定需求的话也可以不标注

一共有以下 5 个元注解:

  1. @Retention

    (保留策略):

    • RetentionPolicy.SOURCE:注解仅存在于源代码中,在编译时会被丢弃。这种类型的注解通常用于提供编译时的辅助信息,不会对运行时产生影响。
    • RetentionPolicy.CLASS:注解存在于编译后的字节码文件中,但在运行时会被丢弃。这种类型的注解可以在编译时对代码进行一些处理,但不会影响程序运行时的行为。
    • RetentionPolicy.RUNTIME:注解在运行时可以通过反射获取到。这种类型的注解可以在运行时对程序的行为进行动态调整,例如在 AOP(面向切面编程)中经常使用。
  2. @Target

    (目标类型):

    • ElementType.METHOD:指定注解可以应用于方法。
    • ElementType.FIELD:指定注解可以应用于字段。
    • ElementType.TYPE:指定注解可以应用于类、接口(包括注解类型)。
    • ElementType.PARAMETER:指定注解可以应用于参数。
    • ElementType.CONSTRUCTOR:指定注解可以应用于构造函数等。
  3. @Documented

    (文档化):

    • 当一个注解被 @Documented 修饰时,这个注解将会包含在 Javadoc 生成的文档中,使得注解的信息可以被文档化展示。
  4. @Inherited

    (继承性):

    • 如果一个注解被 @Inherited 修饰,那么子类会继承父类的该注解。这对于一些需要在继承关系中传递注解的情况非常有用。
  5. @Repeatable

    (可重复性):

    • 允许一个注解在同一个目标上被多次应用,而不需要使用容器注解来包裹多个相同的注解实例。这样可以使代码更加简洁和易读。

引 AOP 依赖

要实现 AOP 自定义注解,第一步先引入 AOP 的依赖:

1
2
3
4
5
<!--AOP-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写 AOP 程序

新建一个 AOP 类,针对于特定方法根据业务需要进行编程 (加 @Aspect 注解声明为 AOP 类)

这个类中,我们要实现自定义注解的功能,比如在方法开始之前,做一个标记,记录该方法的入参,方法结束之后再做一个标记。

新建一个 AOP 类:

1
2
3
4
5
@Aspect
@Component
@Slf4j
public class LogAOP {
}

@Aspect 注解:标记该类为切面类,Spring AOP 会自动识别带有 @Aspect 注解的类,并将其视为切面,然后根据定义的通知和切点来实现横切逻辑。

@Component:用来表示一个受 Spring 容器管理的组件的注解。可以让 Spring 自动扫描并识别被注解的类,然后将其实例化并加入到 Spring 容器中管理。

写一个在接口执行之前要执行的逻辑方法:

@Before 注解标注,里面的 @annotation 是用于定义切点表达式的一种特殊用法,

下列代码中 @Before("@annotation(LogInfo)") 表示在执行被自定义注解标记的方法前执行 logBefore方法

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
@Aspect
@Component
@Slf4j
public class LogAOP {
@Before("@annotation(LogInfo)")
public void logBefore(JoinPoint joinPoint){
// 获取方法所在类的名称
String fullClassName = joinPoint.getSignature().getDeclaringTypeName();

// 获取方法名称
String methodName = joinPoint.getSignature().getName();

// 提取类名的最后一部分
// 比如:com.pidanxia.aop.LogAOP,只拿LogAOP
String[] classNameParts = fullClassName.split("\\.");
String className = classNameParts[classNameParts.length - 1];

// 在方法执行前记录日志
log.info(className + "****************" + methodName + "****************start");

// 获取参数列表
Object[] args = joinPoint.getArgs();
// 入参集合
Map<String, Object> map = new HashMap<>();
// 获取方法参数名称
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
// 输出入参值
if (parameterNames != null) {
for (int i = 0; i < args.length; i++) {
if (parameterNames.length > i) {
String paramName = parameterNames[i];
Object paramValue = args[i];
map.put(paramName, paramValue);
}
}
}
JSONObject json = new JSONObject(map);
log.info("\n入参:" + json);
}

之前有了,理应也要有一个之后的。写一个在接口执行之后要执行的逻辑方法:

@After 注解标注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@After("@annotation(LogInfo)")
public void logAfter(JoinPoint joinPoint){
// 获取方法所在类的名称
String fullClassName = joinPoint.getSignature().getDeclaringTypeName();

// 获取方法名称
String methodName = joinPoint.getSignature().getName();

// 提取类名的最后一部分
String[] classNameParts = fullClassName.split("\\.");
String className = classNameParts[classNameParts.length - 1];

// 在方法执行后记录日志
log.info(className + "****************" + methodName + "****************end");
}

使用自定义注解

在接口处使用自定义注解标记:

1
2
3
4
5
6
@LogInfo
@GetMapping("/get")
public String get(String name,int age){
System.out.println("执行了get方法");
return name;
}

执行结果如下:

即使我们没有在接口方法中写任何的日志逻辑,只要标记了注解,就会自动调用注解方法!

整合成 @Around 注解

有了之前,有了之后,还会有一个包围的注解!

上面的 @Before@After 可以合并为一个注解:@Around

一般开发中都是使用 @Around 注解比较多,因为这样只用写一个注解方法就可以了。

使用方法也很简单,就是用 Object result = point.proceed(); 来隔开之前和之后执行的两部分。

Object result = point.proceed(); 语句就是执行接口方法的意思,执行完这条语句,接口方法就执行完了。

特别注意:@Around 注解标注的方法入参必须是:**ProceedingJoinPoint 类型**的,因为 proceed() 方法是在 ProceedingJoinPoint 接口中定义的,JoinPoint 接口中没有定义。

把之前的 logBefore方法logAfter方法都注释掉,然后写一个新的 logAround方法

1
2
3
4
5
6
7
8
9
10
11
12
@Around("@annotation(LogInfo)")
public void logAround(ProceedingJoinPoint joinPoint) throws Throwable{

…… //这里代表logBefore方法的代码,一模一样拷贝过来即可

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

// 在方法执行后记录日志
log.info(className + "****************" + methodName + "****************end");

}

然后再来请求一下接口,看看控制台输出:

可以看到效果是跟之前的。


实现自定义注解
http://blog.hrseno.cn/2024/03/15/Java-实现自定义注解/
作者
黄浩森
发布于
2024年3月15日
许可协议