IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 从零开始 Spring Boot 26:AOP -> 正文阅读

[Java知识库]从零开始 Spring Boot 26:AOP

从零开始 Spring Boot 26:AOP

spring boot

图源:简书 (jianshu.com)

AOP全称为Aspect Oriented Programming,即面向切面编程。

一次HTTP请求大概可以用下图表示:

image-20221028145710752

如果用MVC的观点划分Spring Boot应用内的消息流转,大概可以用下图表示:

image-20221031091951331

假设我们要在Spring Boot应用每次处理请求前后都加上日志,可能会这么做:

package cn.icexmoon.books2.book.controller;
// ...
@RestController
@RequestMapping("/book/book")
public class BookController {
    @Autowired
    private BookService bookService;

    @GetMapping("/{id}")
    public Book getBookInfo(@PathVariable int id) {
        MyLogUtil.doLogging("before getBookInfo request");
        try {
            return bookService.getBookById(id);
        } finally {
            MyLogUtil.doLogging("after getBookInfo request");
        }
    }

    @Data
    @NoArgsConstructor
    private static class PageBooksDTO {
        @NotNull
        private BookQueryDTO query;
        private Integer page;
        private Integer limit;
    }

    @PostMapping("/page")
    public List<Book> pageBooks(@RequestBody PageBooksDTO dto) {
        MyLogUtil.doLogging("before pageBooks request");
        try {
            return bookService.pageBooks(dto.getPage(), dto.getLimit(), dto.getQuery());
        } finally {
            MyLogUtil.doLogging("after pageBooks request");
        }
    }

    @PostMapping("/add")
    public Result addBook(@RequestBody BookDTO dto) {
        MyLogUtil.doLogging("before addBook request");
        try {
            Integer newId = bookService.addBook(dto);
            return Result.success(newId);
        } finally {
            MyLogUtil.doLogging("after addBook request");
        }
    }

    @PostMapping("/multip-add")
    public Result addBooks(@RequestBody List<BookDTO> dtos) {
        MyLogUtil.doLogging("before addBooks request");
        try {
            return Result.success(bookService.addBooks(dtos));
        } finally {
            MyLogUtil.doLogging("after addBooks request");
        }
    }

    @PostMapping("/edit/{id}")
    public Result editBook(@RequestBody BookDTO dto, @PathVariable Integer id) {
        MyLogUtil.doLogging("before editBook request");
        try {
            bookService.editBook(dto, id);
            return Result.success();
        } finally {
            MyLogUtil.doLogging("after editBook request");
        }
    }
}

虽然这样很有效,但有几个缺点:

  • 方式繁琐,需要在每个需要记录日志的请求方法中添加代码。
  • 日志记录代码和业务代码混合在一起,让代码显得不够简洁。

如果我们可以在请求调用和返回的某个阶段“切一刀”下去,对消息进行拦截,然后执行特定的处理后再拼装回去,岂不是就可以在不影响现有代码的情况下完成类似上边的功能了?

image-20221031092131885

这就是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;
// ...
@Aspect
@Component
public class LoggingAspect {
    @Around("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;
// ...
@MapperScan("cn.icexmoon.books2.*.mapper")
@SpringBootApplication
@EnableAspectJAutoProxy
public class Books2Application implements CommandLineRunner {

    // ...
}

这样就会自动检索项目下用@Aspect注解标注的类,并在运行时生成相应的动态代理以实现AOP的功能。

现在,对任意Controller下的public方法调用都会有日志输出了。

这里所说的拦截器是AOP中针对具体方法调用的拦截器,和Spring Boot中针对Controller层的请求和响应的拦截器不是同一个概念。

使用注解

虽然上边的方式可以完成我们想要的功能,但是有时候可能我们只是想为特定的方法调用添加一些“额外功能”而非所有方法调用,当然我们可以在拦截器注解中添加复杂的通配符来匹配我们的目标方法,但使用注解是一种更方便的做法。

假设我们需要统计方法调用的执行时长,先创建一个注解:

package cn.icexmoon.books2.system.annotation;
// ...
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FuncClock {
}

创建处理这个注解的切面类:

package cn.icexmoon.books2.system.aspect;
// ...
@Aspect
@Component
public class FuncClockAspect {
    @Around("@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的值是注解:

@Around("@annotation(funcClock)")

需要注意的是,这里@annotation的属性值是funcClock,即clockFunc方法的形参名称,而不是FuncClock注解的名称。

现在只需要给需要记录响应时长的方法调用添加上FuncClock注解就行了:

    @PostMapping("/page")
    @FuncClock
    public List<Book> pageBooks(@RequestBody 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使用的是最后一种方式。

这里尝试手动添加一个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

为了测试,将BookControllerbookService属性的依赖注入方式修改为构造器注入,并且使用代理:

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有一些潜在问题需要注意,否则可能出现一些意料不到的问题,详细内容可以阅读:

谢谢阅读。

最终的示例代码见learn_spring_boot/ch26 (github.com)

参考资料

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-11-05 00:12:25  更:2022-11-05 00:16:34 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年3日历 -2025/3/10 18:52:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码