自定义注解
前言
在Java编程中,我们经常会遇到一些重复性的工作,比如检查某个方法是否满足特定的条件、为某个类添加特定的元数据等。虽然我们可以通过编写额外的代码来完成这些工作,但这无疑会增加代码的复杂性和维护成本。而自定义注解正是解决这一问题的有效手段。通过定义注解,我们可以将那些与业务逻辑无关但又必须执行的代码(如检查、验证等)与业务逻辑代码分离开来,使代码更加清晰、简洁。
注解基本概念
什么是注解?
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
注解分类
标准注解
Java自带的标准注解为我们提供了常用的编译检查和文档生成等功能,如:
- @Override:用于标识一个方法覆盖父类方法。当子类中的方法使用了 @Override 注解,但实际上并没有覆盖父类中的方法时,编译器会发出错误警告。
- @Deprecated:用于标记已过时的方法或类。当我们使用这些被标记为过时的方法或类时,编译器会发出提醒警告,建议不再使用它们。
- @SuppressWarnings:用于抑制编译器产生特定的警告。通过为 @SuppressWarnings 注解提供参数(如”unchecked”、”deprecation”等),可以告诉编译器忽略这些类型的警告。
元注解
元注解用于定义注解的属性,如作用目标、生命周期和继承性等,都位于java.lang.annotation包中,我们创建自定义注解时会用到4个标准元注解,如:
- @Documented:用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
- @Inherited:用于阐述某个被标注的类型是被继承的。使用了 @Inherited 修饰的注解类型被用于一个class时该class的子类也有了该注解。
- @Retention:定义该注解的生命周期:某些注解仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的注解可能会被虚拟机忽略,而另一些在class被装载时将被读取。使用当前元注解可以对自定义注解的”生命周期”进行限制。
RetentionPolicy.RUNTIME注解会在class字节码文件中存在,在运行时可以通过反射获取到。RetentionPolicy.CLASS默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得。RetentionPolicy.SOURCE注解仅存在于源码中,在class字节码文件中不包含。
- @Target:说明注解所修饰的对象范围:注解可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
ElementType.CONSTRUCTOR作用于构造器ElementType.FIELD作用于域/属性ElementType.LOCAL_VARIABLE用于描述局部变量ElementType.METHOD作用于方法ElementType.PACKAGE用于描述包ElementType.PARAMETER用于描述参数ElementType.TYPE用于描述类、接口(包括注解类型) 或enum声明,最常用
自定义注解
自定义注解允许开发者根据具体需求定义自己的注解,为代码添加额外的元数据。这些元数据可以在编译时或运行时被读取和使用,以实现特定的功能或检查。
自定义注解
作用
- 代码可读性:通过为方法、类、字段等添加注解,可以提供关于它们如何被使用或它们的预期行为的额外信息,方便于以后的查看和理解。
- 简化配置:在框架和库中,自定义注解经常用于简化配置。例如,Spring框架中的
@Component、@Service、@Repository和@Controller等注解用于定义bean,从而避免了在XML配置文件中手动定义。 - 运行时行为控制:通过反射API,可以在运行时读取和处理注解,从而改变程序的行为。例如,某些框架可能会查找带有特定注解的方法,并在特定条件下调用它们。
- 框架开发:当开发框架或库时,自定义注解是提供可扩展性和可配置性的关键工具。框架开发者可以定义一组注解,然后允许用户在自己的代码中使用这些注解来定制框架的行为。
- AOP(面向切面编程):自定义注解经常与AOP一起使用,以定义切点(pointcuts)。例如,Spring AOP允许开发者定义带有注解的方法作为切点,并在这些方法执行前后插入额外的逻辑。
- 减少模板代码:通过自定义注解和相应的处理逻辑,可以减少或消除重复的模板代码。例如,Lombok中
@Data可以使用注解自动生成getter和setter方法,或者自动生成数据库访问代码等。 - 测试:在测试场景中,自定义注解可以用于标记需要被测试的方法、类或字段,或者用于定义测试数据等。
实例一(登录验证)
全局拦截器+自定义登录验证注解以用户登陆状态效验业务为示例,定义注解
@AccessLimit进行统一拦截状态效验定义注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/***
* 自定义登录效验注解
*
* @author deng
* @date 2024/6/5
*/
public AccessLimit {
/**
* 是否需要登录
*
* @return 是否需要登录
*/
boolean needLogin() default true;
}定义了注解
@AccessLimit作用于方法上,注解的生命周期为最高级别 (可通过反射机制读取)needLogin为注解元素,默认值为true代表需要登录,如传入值为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
40/***
* 登录注解拦截验证类
* @author deng
* @date 2024/6/5
*/
public class AccessInterceptor implements HandlerInterceptor {
/***
* 前置拦截
* @param request
* @param response
* @param handler
* @return
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 判断当前处理器类型为 HandlerMethod
if (handler instanceof HandlerMethod) {
// 获取处理器方法信息
HandlerMethod hm = (HandlerMethod) handler;
// 判断是否需要登录
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
// 无注解则放行
if (accessLimit == null) {
return true;
}
// 获取AccessLimit注解元素信息
boolean needLogin = accessLimit.needLogin();
// 是否需要登录
if (needLogin) {
/*
* 效验登录状态(登录状态在线则放行,不在线抛错返回)
*/
System.out.println("=======进行登录状态效验=======");
}
}
return true;
}
}统一请求拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/***
* 统一入口拦截器
* @author deng
* @date 2024/6/5
*/
public class WebConfig implements WebMvcConfigurer {
private final AccessInterceptor accessInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(accessInterceptor);
}
}控制器方法
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/***
* @author deng
* @date 2024/6/5
*/
public class AnnotationController {
/***
* 经过登录拦截方法
*/
public void test() {
System.out.println("进入方法test");
}
/***
* 未经过登录拦截方法
*/
public void test2() {
System.out.println("进入方法test2");
}
}
实例二(日志记录)
AOP切面+自定义日志记录注解以记录用户行为操作日志业务为示例,定义注解
@Log进行统一行为操作记录定义注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/***
* 自定义操作日志记录注解
* @author deng
* @date 2024/6/18
*/
public Log {
/**
* 模块
*/
String title() default "";
/**
* 功能
*/
String businessType() default "其它";
}定义注解
@Log作用于方法上,注解的生命周期为最高级别 (可通过反射机制读取) ,注解元素为title、businessType切面处理
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/***
* 操作日志记录切面处理
* @author deng
* @date 2024/6/18
*/
public class LogAspect {
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog) {
try {
// *========操作日志=========*//
// 获取方法名称
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
// 获取注解信息
String title = controllerLog.title();
String businessType = controllerLog.businessType();
System.out.println("切面处理 => 当前请求控制器:" + className + ",当前请求方法:" + methodName + ",当前模块:" + title + ",当前功能:" + businessType);
/*
* 进行用户操作日志记录,保存数据库
*/
System.out.println("进行用户操作日志记录");
} catch (Exception exp) {
// 记录本地异常日志
exp.printStackTrace();
}
}
}控制器方法
1
2
3
4
5
6
7
8/***
* 经过操作日志记录方法
*/
public void test3() {
System.out.println("进入方法test3");
}






