自定义注解基本概念和使用方式
作者:?abc!
1. 概念
1.1 元注解
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明
Java5.0定义的元注解:java.lang.annotation包
- @Target:描述了注解修饰的对象范围
- @Retention:表示注解保留时间长短
- @Documented:表示是否将此注解的相关信息添加到javadoc文档中
- @Inherited:是否允许子类继承该注解,只有在类上使用时才会有效,对方法,属性等其他无效
1.1.1 Target
描述了注解修饰的对象范围
取值在java.lang.annotation.ElementType定义,常用的包括:
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述方法变量
- TYPE:用于描述类、接口或enum类型
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- TYPE_PARAMETER:类型参数,表示这个注解可以用在 Type的声明式前,jdk1.8引入
- TYPE_USE:类型的注解,表示这个注解可以用在所有使用Type的地方(如:泛型,类型转换等),jdk1.8引入
ElementType 源码:
public enum ElementType { /** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */ FIELD, /** Method declaration */ METHOD, /** Formal parameter declaration */ PARAMETER, /** Constructor declaration */ CONSTRUCTOR, /** Local variable declaration */ LOCAL_VARIABLE, /** Annotation type declaration */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE, /** * Type parameter declaration * * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * * @since 1.8 */ TYPE_USE }
1.1.1.1 示例
注解Table 可以用于注解类、接口(包括注解类型) 或enum声明
注解NoDBColumn仅可用于注解类的成员变量。
@Target(ElementType.TYPE) public @interface Table { /** * 数据表名称注解,默认值为类名称 * @return */ public String tableName() default "className"; } @Target(ElementType.FIELD) public @interface NoDBColumn { }
1.1.2 Retention
表示注解保留时间长短
取值在java.lang.annotation.RetentionPolicy中,取值为:
- SOURCE:在源文件中有效,编译过程中会被忽略
- CLASS:随源文件一起编译在class文件中,运行时忽略
- RUNTIME:在运行时有效
只有定义为RetentionPolicy.RUNTIME时,我们才能通过注解反射获取到注解。
1.1.2.1 示例
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Column { public String name() default "fieldName"; public String setFuncName() default "setField"; public String getFuncName() default "getField"; public boolean defaultDBValue() default false; }
1.1.3 Documented
描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
Documented是一个标记注解,没有成员
表示是否将此注解的相关信息添加到javadoc文档中
1.1.4 Inherited
定义该注解和子类的关系,使用此注解声明出来的自定义注解,在使用在类上面时,子类会自动继承此注解,否则,子类不会继承此注解。
注意:
- 使用Inherited声明出来的注解,只有在类上使用时才会有效,对方法,属性等其他无效。
- @Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
- 类型标注的annotation的Retention是RetentionPolicy.RUNTIME,如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
1.1.4.1 示例
@Inherited public @interface Greeting { public enum FontColor{ BULE,RED,GREEN}; String name(); FontColor fontColor() default FontColor.GREEN; }
1.2 自定义注解
@interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数
- 方法的名称就是参数的名称,
- 返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)
- 可以通过default来声明参数的默认值。
1.2.1 使用格式
public @interface 注解名 {定义体}
1.2.2 支持数据类型
注解参数可支持数据类型:
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String类型
- Class类型
- enum类型
- Annotation类型
- 以上所有类型的数组
Annotation类型里面的参数该怎么设定:
- 只能用public或默认(default)这两个访问权修饰.例如,String value();这里把方法设为defaul默认类型、
- 参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组
例如,String value();这里的参数成员就为String;
如果只有一个参数成员,最好把参数名称设为"value",后加小括号.
例:下面的例子FruitName注解就只有一个参数成员。
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FruitName { String value() default ""; }
1.2.3 注解元素的默认值
注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null
- 使用空字符串或0作为默认值是一种常用的做法
- 这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。
1.3 为什么要使用自定义注解
语义清晰:自定义注解可以使代码的意图更加明确和可读。
例如,使用 @Transactional 注解可以清晰地表明某个方法需要事务支持,而不需要查看AOP配置或切面代码。
- 简化配置:可以简化配置,减少样板代码。通过注解,开发者可以直接在代码中声明需要的行为,而不需要在外部配置文件中进行复杂的配置
- 增强可维护性:注解使得代码更加模块化和可维护。开发者可以通过注解快速定位和理解代码的行为,而不需要深入理解AOP的实现细节
- 灵活性:自定义注解可以与AOP结合使用,提供更灵活的解决方案。例如,可以定义多个注解来表示不同的切面逻辑,然后在切面中根据注解类型进行不同的处理。
2. 使用注意
2.1 不生效情况
保留策略不正确:注解可能在运行时不可见。
- 解决方法:确保注解的保留策略设置为 RetentionPolicy.RUNTIME,这样注解在运行时可通过反射获取。
目标元素不正确:目标元素(Target Element)设置不正确,注解可能无法应用到期望的程序元素上。
- 解决方法:确保注解的目标元素设置正确,例如 ElementType.METHOD、ElementType.FIELD 等
未启用AOP:如果使用AOP来处理注解,但未启用AOP支持,注解处理逻辑将不会生效
解决方法:确保在Spring Boot应用的主类上添加 @EnableAspectJAutoProxy 注解。
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication @EnableAspectJAutoProxy public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Spring Boot的自动配置机制会根据类路径中的依赖和配置文件中的属性自动配置许多常见的功能。
例如,spring-boot-starter-aop 依赖会自动启用AOP支持
切面未被Spring管理:如果切面类未被Spring管理,AOP切面将不会生效。
- 解决方法:确保切面类上添加了 @Component 注解,或者通过其他方式将其注册为Spring Bean。
import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class MyAspect { // 切面逻辑 }
注解处理逻辑有误:如果注解处理逻辑有误,注解可能不会按预期生效。
- 解决方法:检查注解处理逻辑,确保正确处理注解。例如,使用反射获取注解时,确保方法签名和注解类型正确。
import java.lang.reflect.Method; public class Main { public static void main(String[] args) throws Exception { Method method = MyClass.class.getMethod("myMethod"); MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); if (annotation != null) { // 处理注解 } } }
注解未正确应用:如果注解未正确应用到目标元素上,注解将不会生效。
- 解决方法:确保注解正确应用到目标元素上,例如方法、字段、类等。
public class MyClass { @MyAnnotation public void myMethod() { // 方法实现 } }
2.2 其他
自定义注解可以在Java和Spring项目中使用。具体来说:
- Java:Java本身提供了注解的功能,允许开发者定义和使用自定义注解。自定义注解可以用于代码文档、编译时检查、运行时行为等。
- Spring:Spring框架广泛使用注解来配置和管理Bean、事务、AOP等。你可以在Spring项目中定义自定义注解,并结合Spring的功能(如AOP、依赖注入等)来实现特定的业务逻辑。
因此,自定义注解既可以用于纯Java项目,也可以用于Spring项目。具体取决于你的需求和项目类型。
3. 实例
3.1 自定义注解 实现赋值和校验
定义两个注解,一个用来赋值,一个用来校验。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD,ElementType.METHOD}) @Inherited public @interface InitSex { enum SEX_TYPE {MAN, WOMAN} SEX_TYPE sex() default SEX_TYPE.MAN; }
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD,ElementType.METHOD}) @Inherited public @interface ValidateAge { /** * 最小值 */ int min() default 18; /** * 最大值 */ int max() default 99; /** * 默认值 */ int value() default 20; }
定义User类
@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; }
测试调用
public static void main(String[] args) throws IllegalAccessException { User user = new User(); initUser(user); boolean checkResult = checkUser(user); printResult(checkResult); } 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()); } } }
3.2 自定义注解+拦截器 实现登录校验
如果方法上加了@LoginRequired,则提示用户该接口需要登录才能访问,否则不需要登录。
定义自定义注解:LoginRequired
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LoginRequired { }
定义两个简单的接口
@RestController public class testController { @GetMapping("/sourceA") public String sourceA(){ return "你正在访问sourceA资源"; } @LoginRequired @GetMapping("/sourceB") public String sourceB(){ return "你正在访问sourceB资源"; } }
实现spring的HandlerInterceptor 类,重写preHandle实现拦截器,登录拦截逻辑
@Slf4j public class SourceAccessInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("进入拦截器了"); // 反射获取方法上的LoginRequred注解 HandlerMethod handlerMethod = (HandlerMethod)handler; LoginRequired loginRequired = handlerMethod.getMethod().getAnnotation(LoginRequired.class); if(loginRequired == null){ return true; } // 有LoginRequired注解说明需要登录,提示用户登录 response.setContentType("application/json; charset=utf-8"); response.getWriter().print("你访问的资源需要登录"); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { }}
实现spring类WebMvcConfigurer,创建配置类把拦截器添加到拦截器链中
@Configuration public class InterceptorTrainConfigurer implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SourceAccessInterceptor()).addPathPatterns("/**"); } }
3.3 自定义注解+AOP 实现日志打印
切面需要的依赖包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
自定义注解@MyLog
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyLog{ }
定义切面类
@Aspect // 1.表明这是一个切面类 @Component public class MyLogAspect { // 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名 // 切面最主要的就是切点,所有的故事都围绕切点发生 // logPointCut()代表切点名称 @Pointcut("@annotation(me.zebin.demo.annotationdemo.aoplog.MyLog)") public void logPointCut(){}; // 3. 环绕通知 @Around("logPointCut()") public void logAround(ProceedingJoinPoint joinPoint){ // 获取方法名称 String methodName = joinPoint.getSignature().getName(); // 获取入参 Object[] param = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); for(Object o : param){ sb.append(o + "; "); } System.out.println("进入[" + methodName + "]方法,参数为:" + sb.toString()); // 继续执行方法 try { joinPoint.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } System.out.println(methodName + "方法执行结束"); } }
使用
@MyLog @GetMapping("/sourceC/{source_name}") public String sourceC(@PathVariable("source_name") String sourceName){ return "你正在访问sourceC资源"; }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。