一、@interface 关键字
我们想定义一个自己的注解 需要使用 @interface 关键字来定义。
如定义一个叫 MyAnnotation 的注解:
public @interface MyAnnotation { }
二、元注解
光加上 @interface 关键字 还不够,我们还需要了解5大元注解
@Retention
@Target
@Documented
@Inherited(JDK8 引入)
@Repeatable(JDK8 引入)
1) @Retention 指定注解的生命周期
@Retention(RetentionPolicy.SOURCE)
其中Retention是一个枚举类:
- RetentionPolicy.SOURCE : 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃(.java文件)
- RetentionPolicy.CLASS :注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期(.class文件)
- RetentionPolicy.RUNTIME: 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在(内存中的字节码)
2) @Target指定注解可以修饰的元素类型
@Target(ElementType.Field)
- ElementType.ANNOTATION_TYPE - 标记的注解可以应用于注解类型。
- ElementType.CONSTRUCTOR - 标记的注解可以应用于构造函数。
- ElementType.FIELD - 标记的注解可以应用于字段或属性。
- ElementType.LOCAL_VARIABLE - 标记的注解可以应用于局部变量。
- ElementType.METHOD - 标记的注解可以应用于方法。
- ElementType.PACKAGE - 标记的注解可以应用于包声明。
- ElementType.PARAMETER - 标记的注解可以应用于方法的参数。
- ElementType.TYPE - 标记的注解可以应用于类的任何元素。
3)@Documented
指定注解会被JavaDoc工具提取成文档。默认情况下,JavaDoc是不包括文档的
4)@Inherited
表示该注解会被子类继承,注意,仅针对类,成员属性、方法并不受此注释的影响。
5)@Repeatable
表示注解可以重复使用,为了解决同一个注解不能重复在同一类/方法/属性上使用的问题。
其中最常用的就是 @Retention 跟 @Target。
三、简单实现
例如实现一个简单,在标记注解的地方打印一句日志。
定义一个 MyAnnotation 注解,并且定义一个属性 message 默认值是 ”aaa“。先将该注解加到字段上,看能不能获取到。
//注解用于字段上 @Target(ElementType.FIELD) //运行时使用 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String message() default "aaa"; }
定义一个Student类用于测试:
@Data public class Student { @JSONField(ordinal =0) @MyAnnotation(message = "AAAAAAAAA") public String name; @MyAnnotation(message = "AAAAAAAAA") public Integer score; }
在字段上标注该注解,然后编写一个main方法获取该注解的属性:
public static void main(String[] args) { Class<?> studentClass = Student.class; Field[] fields = studentClass.getDeclaredFields();//获取所有的类成员变量字段 for (Field field : fields) { String fieldName = field.getName(); //获取该类成员变量的名字 System.out.println("成员变量名是:" + fieldName); Annotation[] annotations = field.getAnnotations(); //获取该类成员变量上所有声明周期是运行时的注解 for (Annotation annotation : annotations) { Class<? extends Annotation> annotationType = annotation.annotationType(); String annotationName = annotationType.getSimpleName();//注解的简短名称 System.out.println(" 使用的注解是:" + annotationName); //判断该注解是不是 MyAnnotation 注解,是的话打印其 id 和 describe 属性 if (annotationType.equals(MyAnnotation.class)) { MyAnnotation myAnnotation = field.getAnnotation(MyAnnotation.class); String message = myAnnotation.message(); System.out.println(" MyAnnotation注解中的message是:" + message); } } System.out.println(); } }
执行后打印的内容:
以上就是一个注解的简单实现。
四、使用切面执行自定义注解逻辑
在开发中一般加上注解之后会自动执行一些逻辑,大部分实现的原理是使用切面来实现注解的逻辑的。
1) 首先将刚才的注解修改成放在方法上的:
//注解用于方法 @Target(ElementType.METHOD) //运行时使用 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String message() default "aaa"; }
2) 定义一个切面类:
@Component @Aspect @Slf4j public class MyAnnotationAspect { /* * 这是一个切入点 * */ @Pointcut("@annotation(com.demo.aaa.annotation.MyAnnotation)") public void cutMethod(){ } /** * 切点之前 */ @Before("cutMethod()") public void before(JoinPoint joinPoint) throws Throwable { log.info("============ before =========="); } /** * 切点之后 */ @After("cutMethod()") public void after() throws Throwable { log.info("============ after =========="); } /** * 切点返回内容后 */ @AfterReturning("cutMethod()") public void afterReturning() throws Throwable { log.info("============ afterReturning =========="); } /** * 切点抛出异常后 */ @AfterThrowing("cutMethod()") public void afterThrowing() throws Throwable { log.info("============ afterThrowing =========="); } @Around("cutMethod() && @annotation(myAnnotation)") public Object around(ProceedingJoinPoint point, MyAnnotation myAnnotation) throws Throwable { log.info("============ around1 =========="); Object obj= point.proceed(point.getArgs()); log.info("============ around2 =========="); return obj; } }
在使用aop之前需要先引入一个依赖:
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>
简单说一下各个注解代表什么含义:
- @Aspect:作用是把当前类标识为一个切面供容器读取 ,也就是加上这个注解,spring才知道你这是一个切面类,用于处理切点的逻辑的。
- @Pointcut:切入点,@Pointcut切点表达式非常丰富,可以将 方法(method)、类(class)、接口(interface)、包(package) 等作为切入点,非常灵活,常用的有@annotation、@within、execution等方式,上面的示例使用的是@annotation方式,意思就是说被Spring扫描到方法上带有@annotation中的注解 就会执行切面通知。
- @Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;
- @AfterReturning:该注解标注的方法在业务模块代码执行之后执行;
- @AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;
- @After:该注解标注的方法在所有的 Advice 执行完成后执行,无论业务模块是否抛出异常,类似于 finally 的作用;
- @Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,通知的第一个参数必须是 ProceedingJoinPoint 类型。在通知体内,调用 ProceedingJoinPoint 的 proceed () 方法使得连接点方法执行如果不调用 proceed () 方法,连接点方法则不会执行。无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用; 如果切面中使用了@Around 注解,如果不调用 ProceedingJoinPoint 的 proceed () 方法的话,那么 @Before 和 @After 直接标注的方法也不会被触发。@Around 注解标注的方法,在 ProceedingJoinPoint 的 proceed () 方法 前的逻辑是比@Before的逻辑还要靠前, 在proceed () 方法之后的逻辑比 @After 的逻辑还要靠后。
- Joint Point:JointPoint是程序运行过程中可识别的点,这个点可以用来作为AOP切入点。JointPoint对象则包含了和切入相关的很多信息。比如切入点的对象,方法,属性等。我们可以通过反射的方式获取这些点的状态和信息,用于追踪tracing和记录logging应用信息。
3)将注解放入到接口方法中测试:
@GetMapping("/aaa") @MyAnnotation(message = "成功拉!!!!!!!!!!!!") public void test() { System.out.println("执行代码逻辑"); }
调用接口之后打印
上面就是自定义注解最简单的示例。
五、切点表达式
我们定义切点除了使用 @Pointcut() 之外,我们还有丰富的切点表达式可以定义切点。
1)切点表达式简介
2)通配符合与逻辑运算符
@AspectJ 支持三种通配符:
逻辑运算符: 切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点。
3)切点表达式:
1.arg() :匹配切入点方法的参数类型,匹配的上才是切点。
语法:args(param-pattern) param-pattern:参数类型的全路径。
注意:要先匹配到某些类,不然会报错,也就是不能单独用
示例:
@Pointcut("args(java.lang.String)") //这样就是错的,不能单独使用要匹配到某些类 @Pointcut("within(com.example.demo.service.impl.UserServiceImpl) && args(java.lang.String,java.lang.String)") //要像这样使用 within 先匹配到某个具体的类,在使用args匹配到某个类型参数的方法
2.@args:匹配切入点方法上的参数的类上,参数的类必须要有指定的注解
语法:@args(annotation-type) annotation-type:注解类型的全路径
注意:也不能单独使用,必须先指定到类,而且匹配参数个数至少有一个且为第一个参数的类含有该注解才能匹配的上
示例:
@Pointcut("within(com.demo.RedisTest) && @args(com.demo.aaa.annotation.MyAnnotation)")
3.within:
匹配切入点的指定类的任意方法,不能匹配接口。
语法:within(declaring-type) 参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕
注意: 这个是指定到具体的类
示例:
//within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕。如下是within表达式的语法: @Pointcut(within(declaring-type-pattern)) //within表达式只能指定到类级别,如下示例表示匹配com.spring.service.BusinessObject中的所有方法: @Pointcut(within(com.spring.service.BusinessObject)) //within表达式路径和类名都可以使用通配符进行匹配,比如如下表达式将匹配com.spring.service包下的所有类,不包括子包中的类: @Pointcut(within(com.spring.service.*)) //如下表达式表示匹配com.spring.service包及子包下的所有类: @Pointcut(within(com.spring.service..*))
4.@within:
表示匹配带有指定注解的类。
语法:@within(annotation-type) 注解的全类名
注意:这个是指定到带有某个注解的类
示例:
//如下所示示例表示匹配使用com.spring.annotation.BusinessAspect注解标注的类: @within(com.spring.annotation.BusinessAspect)
5.@annotation() :匹配带有指定注解的连接点
语法:@annotation(annotation-type) annotation-type:注解类型的全路径
示例:
@Pointcut("@annotation(com.test.annotations.LogAuto)")
6.execution() 用于匹配是连接点的执行方法,Spring 切面粒度最小是达到方法级别,而 execution 表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的配置,所以是使用最广泛的。
用法:
- modifiers-pattern:方法的可见性修饰符,如 public,protected,private;
- ret-type-pattern:方法的返回值类型,如 int,void 等;
- declaring-type-pattern:方法所在类的全路径名,如 com.spring.Aspect;
- name-pattern:方法名,如 getOrderDetail();
- param-pattern:方法的参数类型,如 java.lang.String;
- throws-pattern:方法抛出的异常类型,如 java.lang.Exception;
示例:
modifiers-pattern:方法的可见性修饰符,如 public,protected,private; ret-type-pattern:方法的返回值类型,如 int,void 等; declaring-type-pattern:方法所在类的全路径名,如 com.spring.Aspect; name-pattern:方法名,如 getOrderDetail(); param-pattern:方法的参数类型,如 java.lang.String; throws-pattern:方法抛出的异常类型,如 java.lang.Exception; 示例: // 匹配目标类的所有 public 方法,第一个 * 代表返回类型,第二个 * 代表方法名,..代表方法的参数 execution(public * *(..)) // 匹配目标类所有以 User 为后缀的方法。第一个 * 代表返回类型,*User 代表以 User 为后缀的方法 execution(* *User(..)) // 匹配 User 类里的所有方法 execution(* com.test.demo.User.*(..)) // 匹配 User 类及其子类的所有方法 execution(* com.test.demo.User+.*(..)) : // 匹配 com.test 包下的所有类的所有方法 execution(* com.test.*.*(..)) // 匹配 com.test 包下及其子孙包下所有类的所有方法 execution(* com.test..*.*(..)) : // 匹配 getOrderDetail 方法,且第一个参数类型是 Long,第二个参数类型是 String execution(* getOrderDetail(Long, String))
六、切面中获取各个参数
示例:
@Around(value = "@annotation(basisLogAnnotation)") public Object demoAop(ProceedingJoinPoint proceedingJoinPoint, final BasisLogAnnotation basisLogAnnotation) throws Throwable { logger.debug("执行前:"); Object object = proceedingJoinPoint.proceed(); //执行连接点方法,object:方法返回值 logger.debug("执行后:"); // 类名 String className = proceedingJoinPoint.getTarget().getClass().getName(); //方法名 String methodName = proceedingJoinPoint.getSignature().getName(); //参数(我这里是对象,具体根据个人的参数类型来强转) BasisUser basisUser = (BasisUser)proceedingJoinPoint.getArgs()[0]; return object; }