SpringBoot.07.SpringBoot切面编程之AOP

Posted 潮汐先生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot.07.SpringBoot切面编程之AOP相关的知识,希望对你有一定的参考价值。

SpringBoot.07.SpringBoot切面编程之AOP

前言

SpringBoot是对spring和springmvc的进一步封装,因此在SpringBoot中同样支持Spring中的AOP切面编程,不过在SpringBoot中为了快速开发仅仅提供了注解方式的切面编程。

AOP介绍

概念

面向切面编程(AOP是Aspect Oriented Program的首字母缩写)。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是函数式编程的一种衍生泛型。利用AOP可以对业务逻辑的各个部分进行隔离,从而是的业务逻辑各部分之间的耦合度降低,提高程序的重用性,有人提高开发效率。

AOP采取横向抽取机制,补充了传统**纵向继承体系(OOP)**无法解决的重复性代码优化(性能监视、事务管理、安全检查、缓存),将业务逻辑和系统处理的代码(关闭连接、事务管理、操作日志记录)解耦。这样维护起来更加方便。

纵向集成体系

横向抽取机制

相关术语

  • Joinpoint连接点。所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点
  • Pointcut切入点。所谓切入点是指我们要对哪些Joinpoint进行拦截的定义
  • Advice通知|增强。所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。通知分为前置通知后置通知异常通知最终通知环绕通知(切面要完成的功能)
  • Introduction引介。引介是一种特殊的通知。在不修改类代码的前提下,Introduction可以在运行期为类动态地添加一些方法或Field
  • Target目标对象。代理的目标对象
  • Weaving织入。是指把增强应用到目标对象来创建新的代理对象的过程
  • Proxy代理。一个类被AOP织入增强后,就产生一个结果代理类
  • Aspect切面。是切入点和通知的结合,以后咱们自己来编写和配置的
  • Advisor通知器、顾问。和Aspect很相似

AOP使用

在SpringBoot中使用AOP较之于传统的SSM框架就比较简单了。只需要引用spring-boot-starter-aop这一个依赖就可以了。

小试牛刀

1.新建Module

新建Module - springboot-06-aop,按照下图所示填写信息:

点击下一步选择依赖Spring Web,点击Finish。如下图所示:

2.项目配置

我们以springboot-05-logback中的代码为例演示AOP的使用。首先我们规整一下项目,然后将springboot-05-logback中的代码拷贝到本次案例。如下图所示:

3.pom.xml

我们在pom.xml中引入AOP的依赖(记得刷新)。完整的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/>
    </parent>

    <groupId>com.christy</groupId>
    <artifactId>springboot-06-aop</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-06-aop</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- spring-boot-starter-test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.6</version>
        </dependency>

        <!-- mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

        <!-- mybatis-spring-boot-starter
          由于springboot整合mybatis版本中默认依赖mybatis 因此不需要额外引入mybati版本,否则会出现冲突
         -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <!-- 每次新建的项目如果需要开启热部署都需要引入该依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- Spring AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.MyAspect.java

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * @Author Christy
 * @Date 2021/9/14 10:25
 *
 * @Aspect 该注解作用于类上,代表这个类是一个切面
 **/
@Aspect
@Component
@Slf4j
public class MyAspect 
    /**
     *
     * @Before 该注解作用方法上 代表这个方法是一个前置通知方法 该方法是没有返回值的
     * execution() 切点的函数
     *  第一个*:代表的是方法返回值 *即代表所有的返回值
     *  com.christy.service:包名,代表切面切入的位置
     *  包名后的第一个*:代表的是该包下面的所有类
     *  包名后的第二个*:代表的是类中的所有方法
     *  (..):代表方法中的任意|所有参数
     *  execution(* com.christy.service.*.*(..)):
     *      意思就是切面的切点作用在包com.christy.service下的所有类中的所有方法
     */
    @Before("execution(* com.christy.service.*.*(..))")
    public void before(JoinPoint joinPoint)
        log.info("这是一个前置通知Before");
        // 目标对象
        log.info("目标对象:" + joinPoint.getTarget());
        // 方法名
        log.info("方法名:" + joinPoint.getSignature());
        // 方法中的参数
        log.info("方法中的参数:" + joinPoint.getArgs());
    

    /**
     *
     * @After 该注解作用方法上 代表这个方法是一个后置(最终)通知方法
     *        在目标对象方法之后执行通知,有没有异常都会执行
     *        该方法是没有返回值的
     */
    @After("execution(* com.christy.service.*.*(..))")
    public void after(JoinPoint joinPoint)
        log.info("这是一个后置通知After");
        // 目标对象
        log.info("目标对象:" + joinPoint.getTarget());
        // 方法名
        log.info("方法名:" + joinPoint.getSignature());
        // 方法中的参数
        log.info("方法中的参数:" + joinPoint.getArgs());
    

    /**
     *
     * @AfterReturning 该注解作用方法上 代表这个方法是一个后置通知方法
     *                 在目标对象方法之后执行通知,有异常则不执行了
     *                 该方法可以获取目标方法的返回值
     */
    @AfterReturning(value = "execution(* com.christy.service.*.*(..))",returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result)
        log.info("这是一个后置结果通知AfterReturning");
        // 目标对象
        log.info("目标对象:" + joinPoint.getTarget());
        // 方法名
        log.info("方法名:" + joinPoint.getSignature());
        // 方法中的参数
        log.info("方法中的参数:" + joinPoint.getArgs());
        // 目标方法返回值
        log.info("目标方法返回值:",result);
    

    /**
     *
     * @AfterReturning 该注解作用方法上 代表这个方法是一个后置异常通知方法
     *                 在目标对象方法抛出异常之后执行通知
     * throwing = "ex" 声明ex的类型会限制目标方法抛出指定类型的异常
     *                 这里将ex的类型声明为Throwable说明不限制类型
     */
    @AfterThrowing(value = "execution(* com.christy.service.*.*(..))",throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex)
        log.info("这是一个后置异常通知AfterThrowing");
        // 目标对象
        log.info("目标对象:" + joinPoint.getTarget());
        // 方法名
        log.info("方法名:" + joinPoint.getSignature());
        // 方法中的参数
        log.info("方法中的参数:" + joinPoint.getArgs());
        // 方法中抛出的异常
        log.info("方法中抛出的异常:", ex);
    

    /**
     *
     * @Around 该注解作用方法上 代表这个方法是一个环绕通知方法
     */
    @Around("execution(* com.christy.service.*.*(..))")
    public Object Around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable 
        log.info("这是一个环绕通知Around");
        // 目标对象
        log.info("目标对象:" + proceedingJoinPoint.getTarget());
        // 方法名
        log.info("方法名:" + proceedingJoinPoint.getSignature());
        // 方法参数
        log.info("方法参数:" + proceedingJoinPoint.getArgs());
        // 放行执行目标方法
        Object proceed = proceedingJoinPoint.proceed();
        log.info("目标方法执行之后回到环绕通知");
        // 返回目标方法返回值 如果不返回目标方法是获取不到返回值的
        return proceed;
    

5.测试

我们运行test包下的类UserTest.java中的方法testFindAll()。具体的代码如下:

import com.christy.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

public class UserTest extends BaseTest

    private UserService userService;
    @Autowired
    public UserTest(UserService userService) 
        this.userService = userService;
    

    @Test
    public void testFindALl()
        userService.findAll().forEach(user -> System.out.println(user.toString()));
    

5.1 正常运行

在正常情况下的运行流程:环绕通知->前置通知->方法->后置结果通知->后置通知->环绕通知。如下图所示:

正常情况下访问,通知和方法都能正确执行

5.2 方法中抛出异常

我们修改UserServiceImpl中的方法findAll,在里面抛出一个异常。如下所示:

import com.christy.entity.User;
import com.christy.mapper.UserMapper;
import com.christy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Author Christy
 * @Date 2021/9/2 14:56
 **/
@Service
public class UserServiceImpl implements UserService 

    private UserMapper userMapper;
    @Autowired
    public UserServiceImpl(UserMapper userMapper) 
        this.userMapper = userMapper;
    

    @Override
    public List<User> findAll() 
        int i = 1/0;
        return userMapper.findAll();
    

启动项目,然后浏览器访问http://localhost:8805/user/findAll。控制台打印如下:

在抛出异常情况下,AfterReturning是不执行的afterThrowing会执行。

注解方式

注解方式相较于上面的案例,他的控制粒度更细。什么意思呢?比如我不想某个包下的所有类中的所有方法都使用AOP。我希望指定的方法使用AOP。这个怎么做呢?看下面的案例

1.MyAspectAnnotations.java

import java.lang.annotation.*;

/**
 * @Author Christy
 * @Date 2021/9/14 14:44
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspectAnnotations 
    String value();

2.MyAspect2.java

import com.christy.config.annotations.MyAspectAnnotations;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Author Christy
 * @Date 2021/9/14 14:47
 **/
@Aspect
@Component
@Slf4j
public class MyAspect2 

    /**
     * @PointCut: 该注解在切面类中定义一个通用的切入点
     *  @annotation: 用于匹配当前执行方法持有指定注解的方法;
     **/
    @Pointcut("@annotation(com.christy.config.annotations.MyAspectAnnotations)")
    public void customerPointCut() 
    

    /**
     *
     * @After 该注解作用方法上 代表这个方法是一个后置(最终)通知方法
     *        在目标对象方法之后执行通知,有没有异常都会执行
     *        该方法是没有返回值的
     */
    @After("customerPointCut()")
    public void after(JoinPoint joinPoint)
        log.info("这是一个后置通知After");

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        MyAspectAnnotations myAspectAnnotations = method.getAnnotation(MyAspectAnnotations.class);
        String value = myAspectAnnotations.value();
        log.info("我是使用自定义通知注解返回的消息:",value);
    

3.UserServiceImpl.java

import com.christy.config.annotations.MyAspectAnnotations;
import com.christy.entity.User;
import com.christy.mapper.UserMapper;
import com.christy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @Author Christy
 * @Date 2021/9/2 14:56
 **/
@Service
public class UserServiceImpl implements UserService 

    private UserMapper userMapper;
    @Autowired
    public UserServiceImpl(UserMapper userMapper)