文章目录
前言-切面的执行顺序
参考文章,点击这里
注意:方法的执行的代码如下所示(表示的是正在代理的方法,逻辑的执行):
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
//执行方法
Object result = point.proceed();
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning
===============分割线==============
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing:异常发生
java.lang.RuntimeException: 异常发生
1. 准备工作
首先这里创建了一个简单的springboot项目:
各个类的内容如下所示:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
}
@Component
public class UserDao {
public User findUserById(Integer id) {
if(id > 10) {
return null;
}
return new User(id, "user-" + id);
}
}
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
public User findUserById(Integer id) {
return userDao.findUserById(id);
}
}
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("user/{id}")
public User findUser(@PathVariable("id") Integer id) {
return userService.findUserById(id);
}
}
注解的使用规则
//@注解名(变量1=变量1值,变量2=变量2值,...)
//如果注解中拥有数组类型,假设是String类型,那么赋值方式可以如下
//@注解名(String数组名称={"tset1","test2","test3"})
@TestAnnotation(name="Taro")
//因为我们注解中的age()是拥有默认值的,所以这边可以不为age()赋值
//如果我们的注解中只有一个成员变量,且成员变量的名称为value()
//那么可以使用如下赋值方式
//@注解名(属性值)
//如果我们的注解中没有成员变量,那么此时的注解被称为标识注解
注解中可以定义的数据类型是受到限制的,除了基本类型之外,String,Enums,Annotation,Class还有这些类型的数组。
语法注意:
参考博文,点击这里
-
1.访问修饰符必须为public,不写默认为public;
-
2.该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;
-
3.该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为value(后面使用会带来便利操作);
-
4.()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;
-
5.default代表默认值,值必须和第2点定义的类型一致;
-
6.如果没有默认值,代表后续使用注解时必须给该类型元素赋值。
可以看出,注解类型元素的语法非常奇怪,即又有属性的特征(可以赋值),又有方法的特征(打上了一对括号)。但是这么设计是有道理的,在后续的代码示例中我们可以看到:注解在定义好了以后,使用的时候操作元素类型像在操作属性,解析的时候操作元素类型像在操作方法。
@Target
主要的参数类型包括以下几种
ElementType.TYPE 用于类,接口,枚举但不能是注解
ElementType.FIELD 作用于字段,包含枚举值
ElementType.METHOD 作用于方法,不包含构造方法
ElementType.PARAMETER 作用于方法的参数
ElementType.CONSTRUCTOR 作用于构造方法
ElementType.LOCAL_VERIABLE 作用于本地变量或者catch语句
ElementType.ANNOTATION_TYPE 作用于注解
ElementType.PACKAGE 作用于包
@Retention
主要的参数类型包括以下几种
RetentionPolicy.SOURCE 注解存在于源代码中,编译时会被抛弃
RetentionPolicy.CLASS 注解会被编译到class文件中,但是JVM会忽略
RetentionPolicy.RUNTIME JVM会读取注解,同时会保存到class文件中
通知类型
- @Before:前置通知,在调用目标方法之前执行通知定义的任务
- @After:后置通知,在目标方法执行结束后,无论执行结果如何都执行通知定义的任务
- @After-returning:后置通知,在目标方法执行结束后,如果执行成功,则执行通知定义的任务
- @After-throwing:异常通知,如果目标方法执行过程中抛出异常,则执行通知定义的任务
- @Around:环绕通知,在目标方法执行前和执行后,都需要执行通知定义的任务
切点表达式
参考博文,点击这里
切点的功能是指出切面的通知应该从哪里织入应用的执行流。切面只能织入公共方法。
在Spring AOP中,使用AspectJ的切点表达式语言定义切点其中excecution()是最重要的描述符,其它描述符用于辅助excecution()。
excecution()的语法如下
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
这个语法看似复杂,但是我们逐个分解一下,其实就是描述了一个方法的特征:
问号表示可选项,即可以不指定。
excecution(* com.tianmaying.service.BlogService.updateBlog(..))
-
modifier-pattern:表示方法的修饰符
-
ret-type-pattern:表示方法的返回值
-
declaring-type-pattern?:表示方法所在的类的路径
-
name-pattern:表示方法名
-
param-pattern:表示方法的参数
-
throws-pattern:表示方法抛出的异常
注意事项 -
其中后面跟着“?”的是可选项。
-
在各个pattern中,可以使用"*"来表示匹配所有。
-
在param-pattern中,可以指定具体的参数类型,多个参数间用“,”隔开,各个也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String)表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型。
-
可以用(…)表示零个或多个任意的方法参数。
使用&&符号表示与关系,使用||表示或关系、使用!表示非关系。在XML文件中使用and、or和not这三个符号。
2. 使用注解执行固定的操作
现在我们已经有了这样的一个简单的web项目了,直接访问localhost:8080/user/6后,显然会得到一个如下的json串
{
"id": 6,
"name": "user-6"
}
但是我们不满足于此,这个项目也未免太简陋了,现在我们就来为它增加一个日志的功能(不要说使用log4j等日志框架,我们的目的是学习自定义注解)
假设我们现在的目的是,在调用controller中的findUser方法前,先在控制台输出一句话。好了那就开始做吧,我们先创建一个annotation包,里面创建我们自定义的注解类
KthLog:
package com.example.demo.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface KthLog {
String value() default "";
}
这里注解类上的三个注解称为元注解,其分别代表的含义如下:
@Documented:
注解信息会被添加到Java文档中
@Retention:
注解的生命周期,表示注解会被保留到什么阶段,可以选择编译阶段、类加载阶段,或运行阶段
@Target:
注解作用的位置,ElementType.METHOD表示该注解仅能作用于方法上
然后我们可以把注解添加到方法上:
@KthLog("这是日志内容")
@RequestMapping("user/{id}")
public User findUser(@PathVariable("id") Integer id) {
return userService.findUserById(id);
}
这个注解目前是没有任何作用的,因为我们仅仅是对注解进行了声明,并没有在任何地方来使用这个注解,注解的本质也是一种广义的语法糖,最终还是要利用Java的反射来进行操作
不过Java给我们提供了一个AOP机制,可以对类或方法进行动态的扩展,想较深入地了解这一机制的可以参考我的这篇文章:从源码解读Spring的AOP
我们创建切面类,如下:
@Component
@Aspect
public class KthLogAspect {
@Pointcut("@annotation(com.example.demo.annotation.KthLog)")
private void pointcut() {}
@Before("pointcut() && @annotation(logger)")
public void advice(KthLog logger) {
System.out.println("--- Kth日志的内容为[" + logger.value() + "] ---");
}
}
其中@Pointcut声明了切点(这里的切点是我们自定义的注解类),@Before声明了通知内容,在具体的通知中,我们通过@annotation(logger)拿到了自定义的注解对象,所以就能够获取我们在使用注解时赋予的值了。这里如果对于切点和通知等概念不了解的,建议先去查阅一些aop的知识再回来看本文较好,本文更注重于实践,而不是概念的讲解
然后我们现在再来启动web服务,在浏览器上输入localhost:8080/user/6(使用JUnit单元测试也可以),会发现控制台成功输出:
3. 使用注解获取更详细的信息
刚才我们使用自定义注解实现了在方法调用前输出一句日志,但是我们并不知道这是哪个方法、哪个类输出的,如果有两个方法都加上了这个注解,且value的值都一样,那我们该怎么区分这两个方法呢?比如现在我们给UserController类中添加了一个方法:
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@KthLog("这是日志内容")
@RequestMapping("user/{id}")
public User findUser(@PathVariable("id") Integer id) {
return userService.findUserById(id);
}
@KthLog("这是日志内容")
@RequestMapping("compared")
public void comparedMethod() { }
}
如果我们调用comparedMethod()方法,显然会得到和刚才一样的输出结果,这时候我们就需要对注解做进一步改造,其实很简单,只需要在切面类的advice()方法中添加一个JoinPoint参数即可,如下:
@Before("pointcut() && @annotation(logger)")
public void advice(JoinPoint joinPoint, KthLog logger) {
System.out.println("注解作用的方法名: " + joinPoint.getSignature().getName());
System.out.println("所在类的简单类名: " + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("所在类的完整类名: " + joinPoint.getSignature().getDeclaringType());
System.out.println("目标方法的声明类型: " + Modifier.toString(joinPoint.getSignature().getModifiers()));
}
然后我们再来执行一遍刚才的流程,看看会输出什么结果:
现在我们再将这些内容放到日志中,顺便修改一下日志的格式,如下:
@Before("pointcut() && @annotation(logger)")
public void advice(JoinPoint joinPoint, KthLog logger) {
System.out.println("["
+ joinPoint.getSignature().getDeclaringType().getSimpleName()
+ "][" + joinPoint.getSignature().getName()
+ "]-日志内容-[" + logger.value() + "]");
}
3.1 相关方法的补充:
MethodSignature signature = (MethodSignature) point.getSignature();//(signature是信号,标识的意思):获取被增强的方法相关信息
Method method = signature.getMethod();
PermissionData pd = method.getAnnotation(PermissionData.class);
4. 使用注解修改参数和返回值
我们把之前添加的compare()方法删去,现在我们的注解需要对方法的参数作出修改,以findUser()方法为例,假设我们传入的用户id是从1开始计数,后端则是从0开始计数,我们的@KthLog注解的开发者喜欢“多管闲事”,想要帮助其他人减轻一点压力,那该怎么做呢?
在这个应用场景中,我们需要做的有两件事:将传入的id减1,给返回的user类中的id加1。这就涉及到如何拿到参数的问题。因为我们需要管理方法执行前和执行后的操作,所以我们使用@Around环绕注解,如下:
@Around("pointcut() && @annotation(logger)")
public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) {
System.out.println("["
+ joinPoint.getSignature().getDeclaringType().getSimpleName()
+ "][" + joinPoint.getSignature().getName()
+ "]-日志内容-[" + logger.value() + "]");
Object result = null;
try {
result = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
这里除了将@Before改为@Around之外,还将参数中的JoinPoint改为了ProceedingJoinPoint,不过不用担心,JoinPoint能做的ProceedingJoinPoint都能做。这里通过调用proceed()方法,执行了实际的操作,并获取到了返回值,那么接下来对于返回值的操作相信就不用我再多说了,现在问题就是如何获取到参数
ProceedingJoinPoint继承了JoinPoint接口,在JoinPoint中,存在一个getArgs()方法,用于获取方法参数,返回的是一个Object数组,与之匹配的则是proceed(args)方法,这两个方法结合起来,就能够实现我们的目的:
@Around("pointcut() && @annotation(logger)")
public Object advice(ProceedingJoinPoint joinPoint, KthLog logger) {
System.out.println("["
+ joinPoint.getSignature().getDeclaringType().getSimpleName()
+ "][" + joinPoint.getSignature().getName()
+ "]-日志内容-[" + logger.value() + "]");
Object result = null;
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if(args[i] instanceof Integer) {
args[i] = (Integer)args[i] - 1;
break;
}
}
try {
result = joinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
if(result instanceof User) {
User user = (User) result;
user.setId(user.getId() + 1);
return user;
}
return result;
}
这里为了代码的鲁棒性做了两次参数类型校验,接着我们重新执行之前的测试,这里为了让结果更明显,我们在UserDao处添加一些输出,来显示实际执行的参数和返回的值各自是什么:
@Component
public class UserDao {
public User findUserById(Integer id) {
System.out.println("查询id为[" + id + "]的用户");
if(id > 10) {
return null;
}
User user = new User(id, "user-" + id);
System.out.println("返回的用户为[" + user.toString() + "]");
return user;
}
}
现在我们访问http://localhost:8080/user/6,来看控制台打印的结果:
我们发现在url上输入的6,在后端被转换成了5,最终查询的用户也是id为5的用户,说明我们参数转换成功了,然后我们来看浏览器得到的响应结果:
返回的用户id是6,而不是后端查询的5,说明我们对返回值的修改也成功了
5. 总结
在Web项目(这里特指Spring项目)中使用自定义注解开发,其原理还是依赖于Spring的AOP机制,这一点就与我们普通的Java项目有所区别。当然,如果是开发其他框架而需要使用自定义注解时,则需要自己实现一套机制,不过原理本质上都是大同小异,无非是将一些模板操作进行了封装
通过自定义的注解,我们不仅能够在方法执行前后进行扩展,同时还可以获取到作用方法的方法名,所在类等信息,更重要的是还能够修改参数和返回值,这几点应用下来基本就囊括了绝大部分自定义注解的功能。了解到这里,完全就能够自己动手来写一个自定义注解来简化我们的项目