AOP面向切面编程

spring AOP 即面向切面编程。在不修改源码的基础上,对我们的已有方法进行增强。

AOP概述

AOP的作用和优势

作用:

    在程序运行期间,不修改源码对已有方法进行增强。

优势:

    减少重复代码     

    提高开发效率     

    维护方便

AOP实现原理

​ 使用动态代理技术

  • JDK动态代理: 必须需要接口的
  • Cglib动态代理: 不需要接口的,只需要类就好了

AOP的具体应用

在AOP 这种思想还没有出现的时候,我们解决 切面的问题思路无非有以下两种:

  1. 方式一:通过静态方法实现(缺点:需要修改源码,后期不好维护)

    ​ 把需要添加的代码抽取到一个地方,然后在需要添加那些方法中引用

  2. 方式二:通过继承方案来解决(缺点:需要修改源码,继承关系复杂,后期不好维护)

    ​ 抽取共性代码到父类, 子类在需要的位置,调用父类方法。

上述两种方式的缺点

​ 都会打破原来代码的平静(也就是必须要修改代码 , 或者就是必须事先固定好。) 如果我们想在原有代码基础上扩展。 并且不改动原来的代码, 就可以使用AOP了。

​ 其实AOP 字面直译过来是面向切面编程,其实通俗一点它就是在不修改类的源码的基础之上对我们的具体某个方法进行了增强而已。在之前我们对某个方法进行增强无非是两种手段 ,一种是装饰者模式 、 一种是代理模式 (静态代理 & 动态代理) 。 AOP 的底层使用的是动态代理方式 。

实现

​ AOP 的底层动态代理实现有两种方案。

​ 一种是使用JDK的动态代理。 这种是早前我们在前面的基础增强说过的。 这一种主要是针对有接口实现的情况。 它的底层是创建接口的实现代理类, 实现扩展功能。也就是我们要增强的这个类,实现了某个接口,那么我就可以使用这种方式了. 而另一种方式是使用了cglib 的动态代理,这种主要是针对没有接口的方式,那么它的底层是创建被目标类的子类,实现扩展功能.

JDK方式

要求: 必须有接口

package com.jwang.proxy;

import com.jwang.dao.AccountDao;
import com.jwang.pojo.Account;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

/**
* 包名:com.jwang.proxy
*
* @author Leevi
* 日期2020-11-11 14:53
*/
public class JDKProxyTest {
public static void main(String[] args) {
//使用jdk的动态代理,来代理AccountDao接口
AccountDao accountDao = (AccountDao) Proxy.newProxyInstance(AccountDao.class.getClassLoader(), new Class[]{AccountDao.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy就是代理对象
//method就是需要被代理的方法
//args就是方法的参数
if (method.getName().equals("findAll")) {
//执行查询所有的sql语句
String sql = "select * from account";
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql:///day29?characterEncoding=utf8", "root", "123");
PreparedStatement pstm = conn.prepareStatement(sql);
ResultSet rst = pstm.executeQuery();
List<Account> accountList = new ArrayList<>();
while (rst.next()) {
int id = rst.getInt("id");
String name = rst.getString("name");
double money = rst.getDouble("money");
Account account = new Account();
account.setId(id);
account.setName(name);
account.setMoney(money);
accountList.add(account);
}
return accountList;
}
return null;
}
});

System.out.println(accountDao.findAll());
}
}

CgLib方式【了解】

  • 添加坐标
<dependencies>
<!--Spring核心容器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--SpringAOP相关的坐标-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

<!--Spring整合单元测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
  • 使用CgLib方式实现: 第三方的代理机制,不是jdk自带的. 没有实现接口的类产生代理,使用的是字节码的增强技术,其实就是产生这个类的子类。

    不需要有接口

package com.jwang.proxy;

import com.jwang.service.AccountService;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

/**
* 包名:com.jwang.proxy
*
* @author Leevi
* 日期2020-11-11 15:06
*/
public class CglibProxyTest {
public static void main(String[] args) {
//使用cglib的动态代理,代理AccountService对象
AccountService accountService = new AccountService();
Enhancer enhancer = new Enhancer();

//设置enhancer的父类
enhancer.setSuperclass(AccountService.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//o 表示代理对象
// method表示方法
//objects表示参数
if (method.getName().equals("save")) {
//先进行权限校验
System.out.println("校验添加权限。。。");
//再调用save()方法
method.invoke(accountService,objects);
return null;
}
//其它方法就不增强
return method.invoke(accountService,objects);
}
});

//创建代理对象
AccountService proxyService = (AccountService) enhancer.create();

proxyService.save();
}
}

AOP术语

spring中的AOP要明确的事

  • 编译期

    ​ 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
    ​ 把公用代码抽取出来,制作成切面中的通知。(开发阶段最后再做)
    ​ 在配置文件中,声明切入点与通知间的关系,即切面

  • 运行期(Spring框架完成的)

    ​ Spring框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

​ 在spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

AOP中的术语

  • JoinPoint: 连接点(所有可以被增强的方法)

    ​ 类里面哪些方法可以被增强,这些方法称为连接点. 在spring的AOP中,指的是业务层的类的所有现有的方法。

  • Pointcut: 切入点(具体项目中真正已经被增强的方法)

    ​ 在类里面可以有很多方法被增强,但是实际开发中,我们只对具体的某几个方法而已,那么这些实际增强的方法就称之为切入点

  • Advice: 通知/增强 (具体用于增强方法的代码)

    ​ 增强的逻辑、称为增强,比如给某个切入点(方法) 扩展了校验权限的功能,那么这个校验权限即可称之为增强 或者是通知

    ​ 通知分为:

    ​ 前置通知: 在被增强的方法之前执行.

    ​ 后置通知: 在被增强的方法之后执行. 特点: 可以得到被增强方法的返回值

    ​ 异常通知: 在被增强的方法执行出现异常的时候执行. 如果方法没有异常,不会执行. 特点:可以获得异常的信息

    ​ 最终通知: 指的是无论是否有异常,总是被执行的。

    ​ 环绕通知:在方法之前和方法之后执行. 特点:可以阻止目标方法执行

  • Aspect: 切面(所有的通知都是在切面中的)

Spring中的AOP

基于XML的AOP配置

需求: 在service的增删改查方法调用之前进行权限的校验

  • 导入坐标
<dependencies>
<!--Spring核心容器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--SpringAOP相关的坐标-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

<!--Spring整合单元测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
  • 导入约束

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">
    </beans>
  • 定义被增强的业务逻辑类和切面类(增强的)

    package com.jwang.service.impl;

    import com.jwang.service.UserService;
    import org.springframework.stereotype.Service;

    /**
    * 包名:com.jwang.service.impl
    *
    * @author Leevi
    * 日期2020-11-12 08:35
    * 目标1: 在执行service的所有方法之前加入权限校验
    * 目标2: 在执行完所有的方法之后都获取方法的返回值,并且进行日志打印 "方法执行完毕...,返回值为:", 如果方法出现异常,则不会执行后置通知
    * 目标3: 在执行所有方法出现异常之后,将异常信息写入到本地文件中
    * 目标4: 在执行所有方法之后,无论是否出现异常,均执行 "资源回收的操作......"
    * 目标5: 在执行query()方法的过程中,统计执行时间
    * 环绕通知还能修改切入点方法的返回值
    */
    @Service
    public class UserServiceImpl implements UserService {
    @Override
    public void add() {
    System.out.println("执行添加...");
    int number = 10/0;
    }

    @Override
    public void deleteById(int id) {
    //删除了id为
    System.out.println("执行删除..."+id);
    }

    @Override
    public void update() {
    System.out.println("执行修改...");
    }

    @Override
    public String query() {
    try {
    Thread.sleep(3000);
    System.out.println("执行查询...");
    return "张三";
    } catch (InterruptedException e) {
    e.printStackTrace();
    throw new RuntimeException(e.getMessage());
    }
    }
    }

    package com.jwang.aspect;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.springframework.stereotype.Component;

    /**
    * 包名:com.jwang.aspect
    * @author Leevi
    * 日期2020-11-12 08:37
    */
    @Component
    public class MyAspect {
    public void checkPermission(){
    System.out.println("进行权限校验....");
    }

    public void printResult(String returnValue){
    //1. 获取被增强方法的返回值,就是returnValue
    //2. 打印
    System.out.println("执行完毕,方法的返回值为:"+returnValue);
    }

    public void printError(Throwable errorMsg){
    errorMsg.printStackTrace();
    //使用流将字符串(异常信息),写入到本地磁盘文件中
    System.out.println("使用FileOutputStream将" + errorMsg.getStackTrace() + "写入到磁盘...");
    }

    public void close(){
    System.out.println("执行资源回收的操作...");
    }

    public Object totalTime(ProceedingJoinPoint joinPoint){
    try {
    //1. 获取当前时间
    long startTime = System.currentTimeMillis();
    //2. 执行切入点
    Object obj = joinPoint.proceed();
    //3. 获取执行完切入点之后的时间
    long endTime = System.currentTimeMillis();
    System.out.println("方法的总执行时间是:" + (endTime - startTime));
    return "hello:"+obj;
    } catch (Throwable throwable) {
    throwable.printStackTrace();
    throw new RuntimeException(throwable.getMessage());
    }
    }
    }
  • spring-aop.xml配置文件中进行aop配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--1. 包扫描-->
    <!--将UserServiceImpl、MyAspect进行IOC-->
    <context:component-scan base-package="com.jwang"/>

    <!--
    进行aop配置
    目标1: 在执行增删改查方法之前,都加入权限校验
    1. 切入点是: UserServiceImpl中的所有方法
    2. 通知是: MyAspect中的checkPermission方法
    3. 通知的类型是: 前置通知
    4. 切面是: MyAspect类的对象
    目标2: 在执行完所有的方法之后都获取方法的返回值,并且进行日志打印 "方法执行完毕...,返回值为:"
    1. 切入点是: UserServiceImpl中的所有方法
    2. 通知是: MyAspect中的printResult方法
    3. 通知的类型是: 后置通知
    4. 切面是: MyAspect类的对象
    目标3: 在执行所有方法出现异常之后,将异常信息写入到本地文件中
    1. 切入点是: UserServiceImpl中的所有方法
    2. 通知是: MyAspect中的printError方法
    3. 通知的类型是: 异常通知
    4. 切面是: MyAspect类的对象
    目标4: 在执行所有方法之后,无论是否出现异常,均执行 "资源回收的操作......"
    1.切入点是: UserServiceImpl中的所有方法
    2. 通知是: MyAspect中的close方法
    3. 通知的类型是: 最终通知
    4. 切面是: MyAspect类的对象
    目标5: 在执行query()方法的过程中,统计执行时间
    1. 切入点是: UserServiceImpl中的query方法
    2. 通知是: MyAspect中的totalTime方法
    3. 通知的类型是: 环绕通知
    4. 切面是: MyAspect类的对象
    -->
    <aop:config>
    <!--
    expression是切入点表达式,它的作用是用一个表达式描述切入点
    -->
    <aop:pointcut id="pt1" expression="execution(* com.jwang.service.impl.UserServiceImpl.*(..))"/>

    <aop:pointcut id="pt2" expression="execution(String com.jwang.service.impl.UserServiceImpl.query())"/>

    <!--
    配置一个切面
    -->
    <aop:aspect id="ap1" ref="myAspect">
    <!--
    配置通知去增强切入点
    -->
    <aop:before method="checkPermission" pointcut-ref="pt1"></aop:before>
    <!--
    配置后置通知, 后置通知有一个特殊的属性returning用于指定将切入点的返回值赋值给通知中的哪个参数
    -->
    <aop:after-returning returning="returnValue" method="printResult" pointcut-ref="pt1"></aop:after-returning>

    <!--
    配置异常通知
    -->
    <aop:after-throwing throwing="errorMsg" method="printError" pointcut-ref="pt1"></aop:after-throwing>

    <!--
    配置最终通知
    -->
    <aop:after method="close" pointcut-ref="pt1"></aop:after>
    <!--
    配置环绕通知
    -->
    <aop:around method="totalTime" pointcut-ref="pt2"></aop:around>
    </aop:aspect>
    </aop:config>
    </beans>

基于注解的AOP配置

步骤

  1. 创建工程, 导入坐标
  2. 创建Dao和增强的类, 注册
  3. 开启AOP的注解支持
  4. 在增强类上面添加@Aspect
  5. 在增强类的增强方法上面添加
  • @Before()

  • @AfterReturning()

  • @Around()

  • @AfterThrowing()

  • @After()

  • 添加坐标

<dependencies>
<!--Spring核心容器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--SpringAOP相关的坐标-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

<!--Spring整合单元测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
  • 定义被增强的业务逻辑类

    package com.jwang.service.impl;

    import com.jwang.service.UserService;
    import org.springframework.stereotype.Service;

    /**
    * 包名:com.jwang.service.impl
    * @author Leevi
    * 日期2020-09-23 08:34
    * 目标1: 在执行所有方法之前,先进行权限校验
    * 切入点: UserServiceImpl类的所有方法
    * 前置通知: checkPermission()方法
    * 切面: MyAspect
    * 目标2: 在执行完所有方法之后,进行日志打印 "方法执行完毕...", 如果方法出现异常,则不会执行后置通知
    * 切入点:UserServiceImpl类的所有方法
    * 后置通知: printLog()方法
    * 切面: MyAspect
    * 目标3: 在执行所有方法出现异常之后,输出"方法出现异常,请稍后再试"
    * 切入点: UserServiceImpl类的所有方法
    * 异常通知: printException()
    * 切面: MyAspect
    * 目标4: 在执行所有方法之后,无论是否出现异常,均执行 "hello world...."
    * 切入点: UserServiceImpl类的所有方法
    * 最终通知: printHello()
    * 切面: MyAspect
    * 目标5: 在执行query()方法的过程中,统计执行时间
    * 切入点: query()方法
    * 环绕通知: total()
    * 切面: MyAspect
    */
    @Service
    public class UserServiceImpl implements UserService{
    @Override
    public void add() {
    System.out.println("执行添加...");
    int num = 10/0;
    }

    @Override
    public void delete() {
    System.out.println("执行删除...");
    }

    @Override
    public void update() {
    System.out.println("执行修改...");
    }

    @Override
    public void query() {
    try {
    Thread.sleep(3000);
    System.out.println("执行查询...");
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }
  • 定义切面类,在切面增强类上面使用注解 @Aspect并且在切面类中定义切入点方法

    package com.jwang.aspect;

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;

    /**
    * 包名:com.jwang.aspect
    * @author Leevi
    * 日期2020-09-23 08:36
    * 注解AOP配置:
    * 1. 给切面加上Aspect注解
    * 2. 在切面中,创建方法,配置切入点
    * Pointcut注解
    * 3. 配置各种通知
    * Before
    * AfterReturning
    * AfterThrowing
    * After
    * Around
    */
    @Component
    @Aspect
    public class MyAspect {
    @Pointcut("execution(* com.jwang.service.impl.UserServiceImpl.*(..))")
    public void pt1(){
    }
    @Pointcut("execution(void com.jwang.service.impl.UserServiceImpl.query())")
    public void pt2(){
    }

    @Before("pt1()")
    public void checkPermission(){
    System.out.println("进行权限校验....");
    }

    @AfterReturning("pt1()")
    public void printLog(){
    System.out.println("方法执行完毕.....");
    }
    @AfterThrowing("pt1()")
    public void printException(){
    System.out.println("方法出现异常,请稍后再试...");
    }
    @After("pt1()")
    public void printHello(){
    System.out.println("hello world");
    }

    @Around("pt2()")
    public void total(ProceedingJoinPoint joinPoint){
    //1. 获取开始时间
    long startMillis = System.currentTimeMillis();
    //2. 执行切入点的方法
    try {
    joinPoint.proceed();
    } catch (Throwable throwable) {
    throwable.printStackTrace();
    }
    //3. 获取结束时间
    long endMillis = System.currentTimeMillis();
    //4. 计算执行时间并打印
    System.out.println(endMillis - startMillis);
    }
    }
  • 创建配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/aop
    http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!--
    1. 包扫描
    -->
    <context:component-scan base-package="com.jwang"/>


    <!--
    要使用注解方式配置AOP:开启注解AOP的驱动
    -->
    <aop:aspectj-autoproxy />
    </beans>