图源:
这篇文章是我早期学习AOP的理解,关于Spring AOP,在学习官方文档后,我写了一篇更全面深入的文章,感兴趣的可以阅读。
AOP全称为Aspect Oriented Programming,即面向切面编程。
一次HTTP请求大概可以用下图表示:
如果用MVC的观点划分Spring Boot应用内的消息流转,大概可以用下图表示:
假设我们要在Spring Boot应用每次处理请求前后都加上日志,可能会这么做:
package cn.icexmoon.books2.book.controller;
// ...
"/book/book")
(public class BookController {
private BookService bookService;
"/{id}")
( public Book getBookInfo( int id) {
MyLogUtil.doLogging("before getBookInfo request");
try {
return bookService.getBookById(id);
} finally {
MyLogUtil.doLogging("after getBookInfo request");
}
}
private static class PageBooksDTO {
private BookQueryDTO query;
private Integer page;
private Integer limit;
}
"/page")
( public List<Book> pageBooks( PageBooksDTO dto) {
MyLogUtil.doLogging("before pageBooks request");
try {
return bookService.pageBooks(dto.getPage(), dto.getLimit(), dto.getQuery());
} finally {
MyLogUtil.doLogging("after pageBooks request");
}
}
"/add")
( public Result addBook( BookDTO dto) {
MyLogUtil.doLogging("before addBook request");
try {
Integer newId = bookService.addBook(dto);
return Result.success(newId);
} finally {
MyLogUtil.doLogging("after addBook request");
}
}
"/multip-add")
( public Result addBooks( List<BookDTO> dtos) {
MyLogUtil.doLogging("before addBooks request");
try {
return Result.success(bookService.addBooks(dtos));
} finally {
MyLogUtil.doLogging("after addBooks request");
}
}
"/edit/{id}")
( public Result editBook( BookDTO dto, Integer id) {
MyLogUtil.doLogging("before editBook request");
try {
bookService.editBook(dto, id);
return Result.success();
} finally {
MyLogUtil.doLogging("after editBook request");
}
}
}
虽然这样很有效,但有几个缺点:
-
方式繁琐,需要在每个需要记录日志的请求方法中添加代码。
-
日志记录代码和业务代码混合在一起,让代码显得不够简洁。
如果我们可以在请求调用和返回的某个阶段“切一刀”下去,对消息进行拦截,然后执行特定的处理后再拼装回去,岂不是就可以在不影响现有代码的情况下完成类似上边的功能了?
这就是AOP,切下去的断面就是切面(Aspect)。
可以将整个负责消息流转的程序看作一整个吐司面包,AOP就是切一刀,在断面上涂上需要的果酱后接回去。面包还是面包,不过中间多了果酱。
拦截器
Spring Boot 默认并不包含 AOP 的相关类库,要添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建处理切面的类:
package cn.icexmoon.books2.system.aspect;
// ...
public class LoggingAspect {
"execution(public * cn.icexmoon.books2.*.controller.*.*(..))")
( public Object logRequest(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
MyLogUtil.doLogging(String.format("before [%s] request", methodName));
try {
return pjp.proceed();
} finally {
MyLogUtil.doLogging(String.format("after [%s] request", methodName));
}
}
}
切面类需要用@Component
注解注入成JavaBean,并且用@Aspect
注解声明这是一个处理切面的类。
此外,还需要用一些特殊的注解,比如@Around
来声明拦截哪些方法调用和具体由哪个方法来执行处理。具体有以下注解可选:
-
@Before
,这种拦截器先执行拦截代码,再执行目标代码。如果拦截器抛异常,那么目标代码就不执行了 -
@After
,这种拦截器先执行目标代码,再执行拦截器代码。无论目标代码是否抛异常,拦截器代码都会执行 -
@AfterReturning
,和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码 -
@AfterThrowing
,和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码 -
@Around
,能完全控制目标代码是否执行,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能
@Around
相当于完全代理了目标方法调用,所以需要一个ProceedingJoinPoint
类型的入参,以决定是否要通过ProceedingJoinPoint.proceed
方法执行原调用,而@Before
则只是在调用前执行一段代码,不影响后续的原始调用(如果没有异常抛出),所以不需要入参。
最后还要在入口类上添加@EnableAspectJAutoProxy
注解来开启AspectJ的自动代理功能:
package cn.icexmoon.books2;
// ...
"cn.icexmoon.books2.*.mapper")
(
public class Books2Application implements CommandLineRunner {
// ...
}
这样就会自动检索项目下用@Aspect
注解标注的类,并在运行时生成相应的动态代理以实现AOP的功能。
现在,对任意Controller
下的public
方法调用都会有日志输出了。
这里所说的拦截器是AOP中针对具体方法调用的拦截器,和Spring Boot中针对Controller层的请求和响应的拦截器不是同一个概念。
使用注解
虽然上边的方式可以完成我们想要的功能,但是有时候可能我们只是想为特定的方法调用添加一些“额外功能”而非所有方法调用,当然我们可以在拦截器注解中添加复杂的通配符来匹配我们的目标方法,但使用注解是一种更方便的做法。
假设我们需要统计方法调用的执行时长,先创建一个注解:
package cn.icexmoon.books2.system.annotation;
// ...
ElementType.METHOD)
(RetentionPolicy.RUNTIME)
(public @interface FuncClock {
}
创建处理这个注解的切面类:
package cn.icexmoon.books2.system.aspect;
// ...
public class FuncClockAspect {
"@annotation(funcClock)")
( public Object clockFunc(ProceedingJoinPoint pjp, FuncClock funcClock) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long timeLong = System.currentTimeMillis() - start;
String methodName = pjp.getSignature().getName();
String clsName = pjp.getThis().getClass().getName();
System.out.println(String.format("%s.%s is executed in %d ms", clsName, methodName, timeLong));
}
}
}
因为我们将会拦截所有标记了@FuncClock
注解的方法调用,所以在切面相应的处理方法中会获取一个FuncClock
类型的入参,通过该入参可以获取方法的注解,以读取其中设定的属性等,当然这里FuncClock
本身不包含任何属性。
现在是依靠注解而非方法路径来进行拦截,所以拦截器注解中value
的值是注解:
"@annotation(funcClock)") (
需要注意的是,这里@annotation
的属性值是funcClock
,即clockFunc
方法的形参名称,而不是FuncClock
注解的名称。
现在只需要给需要记录响应时长的方法调用添加上FuncClock
注解就行了:
"/page")
(
public List<Book> pageBooks( PageBooksDTO dto) {
MyLogUtil.doLogging("before pageBooks request");
try {
return bookService.pageBooks(dto.getPage(), dto.getLimit(), dto.getQuery());
} finally {
MyLogUtil.doLogging("after pageBooks request");
}
}
请求后就可以看到控制台输出类似下面的信息:
cn.icexmoon.books2.book.controller.BookController$$EnhancerBySpringCGLIB$$a3d6d63f.pageBooks is executed in 319 ms
这里的类名cn.icexmoon.books2.book.controller.BookController$$EnhancerBySpringCGLIB$$a3d6d63f
是通过反射从调用方法的this
引用获取的,可以看到此时响应方法调用的已经不是原始的BookController
类的对象了,而是JVM通过CGLIB生成的动态代理类的对象。
虽然这里展示的都是拦截
Controller
层的方法调用,但实际上AOP可以拦截任意的方法调用。
实现原理
AOP的实现可以是多种多样的,包括在编译阶段生成代码、在字节码阶段生成字节码或者在运行时通过反射创建动态代理,Spring Boot使用的是最后一种方式。
关于代理模式的概念可以阅读。
Java中实现动态代理的方式可以阅读。
这里尝试手动添加一个BookService
的动态代理。
先创建负责承载代理后的方法调用逻辑的类型:
package cn.icexmoon.books2.system.proxy;
// ...
abstract public class AbsAspect {
protected final Object subject;
/**
* @param subject 被代理的原始对象
*/
protected AbsAspect(Object subject) {
this.subject = subject;
}
/**
* 被代理后执行的调用
*
* @param method 调用方法
* @param args 参数
* @return
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
abstract public Object call(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException;
}
具体的子类型将会以匿名局部类的方式创建,所以这里是抽象基类。此外,要执行被代理后的逻辑,所以要持有一个被代理类型对象的引用。
要使用动态代理,核心是实现InvocationHandler
类,为了模拟上边的多种类型的拦截器,这里同样创建一个工具类,以提供和对应类型拦截器同样效果的InvocationHandler
:
package cn.icexmoon.books2.system.proxy;
// ...
public class IHManager {
public static InvocationHandler getAroundInvocationHandler(AbsAspect aspect){
return new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return aspect.call(method, args);
}
};
}
public static InvocationHandler getBeforeInvocationHandler(Object subject, AbsAspect aspect){
return new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try{
aspect.call(method, args);
}
catch (Exception e){
return null;
}
return method.invoke(subject, args);
}
};
}
}
参考拦截器的定义,这里实现了2个拦截器对应的InvocationHandler
,其中getAroundInvocationHandler
返回的IH将完全代理(覆盖)原始调用,而getBeforeInvocationHandler
返回的IH会在执行完自定义行为后再执行原始调用,并且会在自定义行为抛出异常后停止后续调用。
接下来就是使用动态代理创建一个实际的代理对象:
package cn.icexmoon.books2.system.proxy;
// ...
public class BookServiceProxy {
public static BookService getBookServiceAroundProxy(BookService bookService) {
return (BookService) Proxy.newProxyInstance(BookServiceImpl.class.getClassLoader(),
new Class<?>[]{BookService.class},
IHManager.getAroundInvocationHandler(new AbsAspect(bookService) {
@Override
public Object call(Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
System.out.println("before book service is called.");
try{
return method.invoke(subject, args);
}
finally {
System.out.println("after book service is called.");
}
}
}));
}
}
主要的代理逻辑这里都定义在匿名局部类AbsAspect
中,具体是调用前、调用后还是“Around”则取决于使用的IHManager
类方法返回的不同InvocationHandler
。
为了测试,将BookController
中bookService
属性的依赖注入方式修改为构造器注入,并且使用代理:
package cn.icexmoon.books2.book.controller;
// ...
@RestController
@RequestMapping("/book/book")
public class BookController {
private BookService bookService;
public BookController(BookService bookService) {
this.bookService = BookServiceProxy.getBookServiceAroundProxy(bookService);
}
// ...
}
其它代码保持原状,重新运行程序并调用接口后就能看到,在正常返回结果的同时,控制台输出:
before book service is called. ... after book service is called.
可以看到手动实现动态代理也不是很麻烦,更多的是要抽象出很多东西带来的理解上的困难。因此使用AOP更为简洁直观。
这里之所以用代理Service而非Controller作为示例,是因为动态代理要求被代理类要实现某个接口,以接口类型来代理。
注意事项
使用AOP有一些潜在问题需要注意,否则可能出现一些意料不到的问题,详细内容可以阅读:
-
。
谢谢阅读。
最终的示例代码见。
文章评论