第四十七天:Spring03 AOP快速入门+xml配置+注解配置+原理

1. 引入案例(计算器记录日志)(理解)

系统辅助功能:记录日志

系统核心业务功能:加减乘除四则运算

1.1 实现方式1:直接耦合

直接在系统核心功能代码中编写系统辅助功能代码

缺点

  1. 系统辅助功能代码严重入侵了系统核心业务功能代码
  2. 记录日志代码重复冗余,复用性差

1.2 实现方式2:工具类

把日志抽取到日志工具类中,在系统核心功能代码中调用工具类中方法记录日志

解决的问题

  1. 提高了复用性

缺点

  1. 系统辅助功能代码严重入侵了系统核心业务功能代码

1.3 实现方式3:动态代理

使用动态代理完成记录日志的功能

解决的问题

  1. 提高了复用性
  2. 解耦了系统辅助功能代码和核心业务代码

缺点

  1. 太难写
  2. 如果目标对象没有实现任何接口,jdk动态代理就无能为力 了

解决思路

  • 使用AOP

相关代码

  • 实现方式1代码MyMathCalculatorWithLog.java

    public class MyMathCalculatorWithLog implements Calculator {
    
        @Override
        public int add(int i, int j) {
            System.out.println("【add】方法开始运行了,算式是【" + i + " + " + j + "】,时间:" + new Timestamp(System.currentTimeMillis()));
            int result = i + j;
            System.out.println("【add】方法计算结束了,结果是【" + result + "】,时间:" + new Timestamp(System.currentTimeMillis())+"\r\n");
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            System.out.println("【sub】方法开始运行了,算式是【" + i + " - " + j + "】,时间:" + new Timestamp(System.currentTimeMillis()));
            int result = i - j;
            System.out.println("【sub】方法计算结束了,结果是【" + result + "】,时间:" + new Timestamp(System.currentTimeMillis())+"\r\n");
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            System.out.println("【mul】方法开始运行了,算式是【" + i + " * " + j + "】,时间:" + new Timestamp(System.currentTimeMillis()));
            int result = i * j;
            System.out.println("【mul】方法计算结束了,结果是【" + result + "】,时间:" + new Timestamp(System.currentTimeMillis())+"\r\n");
            return result;
        }
    
        @Override
        public int div(int i, int j) {
    
            int result = i;
            try {
                System.out.println("【div】方法开始运行了,算式是【" + i + " ÷ " + j + "】,时间:" + new Timestamp(System.currentTimeMillis()));
                result = i / j;
                System.out.println("【div】方法计算结束了,结果是【" + result + "】,时间:" + new Timestamp(System.currentTimeMillis())+"\r\n");
            } catch (Exception e) {
                System.out.println("【div】方法出现异常了,异常信息是【" + e.getMessage() + "】,时间:" + new Timestamp(System.currentTimeMillis()));
            } finally {
                System.out.println("【div】方法结束了,不知道有没有计算成功,时间:" + new Timestamp(System.currentTimeMillis())+"\r\n");
            }
            return result;
        }
    }
    
  • 实现方式2:日志记录功能抽取到工具类中

    工具类logUtils.java

    public class LogUtils {
    
    
    
        public static void logStart(String methodName, Object... args) {
            System.out.println("[" + methodName + "]方法开始计算了,参数是" + Arrays.asList(args) + ",时间:" + new Timestamp(System.currentTimeMillis()));
        }
    
        public static void logReturn(String methodName, Object result) {
            System.out.println("【" + methodName + "】方法计算结束了,结果是【" + result + "】,时间:" + new Timestamp(System.currentTimeMillis()) + "\r\n");
        }
    
        public static void logException(String methodName, Exception e) {
            System.out.println("【" + methodName + "】方法出现异常了,异常信息是" + e.getMessage() + ",时间:" + new Timestamp(System.currentTimeMillis()));
        }
    
        public static void logEnd(String methodName) {
            System.out.println("【" + methodName + "】方法结束了,不知道有没有计算成功,时间:" + new Timestamp(System.currentTimeMillis()) +
                    "\r\n");
        }
    }
    

    实现类:MyMathCalculatorWithLogByUtils.java

    public class MyMathCalculatorWithLogByUtils implements Calculator {
    
        @Override
        public int add(int i, int j) {
            LogUtils.logStart("add", i, j);
            int result = i + j;
            LogUtils.logReturn("add", result);
            return result;
        }
    
        @Override
        public int sub(int i, int j) {
            LogUtils.logStart("sub", i, j);
            int result = i - j;
            LogUtils.logReturn("sub", result);
            return result;
        }
    
        @Override
        public int mul(int i, int j) {
            int result = i * j;
            return result;
        }
    
        @Override
        public int div(int i, int j) {
    
            int result = i;
            try {
                LogUtils.logStart("div", i, j);
                result = i / j;
                LogUtils.logReturn("div", result);
            } catch (Exception e) {
                LogUtils.logException("div", e);
            } finally {
                LogUtils.logEnd("div");
            }
            return result;
        }
    }
    
  • 实现方式3:使用动态代理记录日志

    public class MyCalculatorProxyFactory {
    
        /*
            使用动态代理对目标对象进行增强
                1. 调用目标对象的目标方法,实现原有的基础功能
                2. 在调用目标方法前后,添加增强的逻辑
            Proxy.newProxyInstance()
    
    
            动态代理特点:
                目标对象中所有的目标方法都会被拦截并增强
    
         */
    
        public static Calculator getProxyInstance(Calculator target){
            return (Calculator) Proxy.newProxyInstance(
                    // 类加载器,使用目标对象的类加载器即可
                    target.getClass().getClassLoader(),
                    // 目标对象已经实现的所有接口的字节码对象数组
                    target.getClass().getInterfaces(),
                    // 调用处理器(重点) 目标方法的调用、增强逻辑都通过该对象实现
                    new InvocationHandler() {
                        /**
                         * 目标方法的调用、增强逻辑都通过该对象实现
                         * @param proxy     代理对象,我们不用,给系统用
                         * @param method    目标方法封装的对象
                         * @param args      目标方法的实参
                         * @return          目标方法的返回值,该值必须return出去,否则外界无法获取该值(在有时)
                         * @throws Throwable
                         */
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                            // 目标方法的调用
                            Object returnValue = null;
                            try {
                                System.out.println(method.getName() + "方法开始执行了,参数是:" + Arrays.toString(args));
                                //LogUtils.logStart(method.getName(), Arrays.toString(args));
                                returnValue = method.invoke(target, args);
    
                                System.out.println(method.getName() +"方法执行成功了,结果是:" + returnValue);
                            } catch (Exception e) {
                                System.out.println(method.getName() +"方法执行异常了,异常信息是:" + e.getMessage());
                                e.printStackTrace();
                            } finally {
                                System.out.println(method.getName() +"方法执行结束了,成功与否我不管");
                            }
    
    
                            // 该值必须return出去,否则外界无法获取该值(在有时)
                            return returnValue;
                        }
                    }
            );
        }
    }
    

    测试类:

    /**
     * @Description: 动态代理记录日志效果测试类
     * main方法内分割线以上是使用目标对象计算,分割线以下是获取代理对象并计算;
     * 代理对象计算时有记录日志的功能
     */
    public class ProxyCalculatorlogTest {
    
        public static void main(String[] args) {
            Calculator calculator = new MyMathCalculator();
            System.out.println(calculator.add(1, 2));
            System.out.println(calculator.div(2, 1));
    
            System.out.println("---------------------------");
    
            // JDK动态代理 生成一个代理对象(实现了和目标对象/被代理对象一样的接口),使用代理对象调用方
            
            Calculator proxyInstance = MyCalculatorProxyFactory.getProxyInstance(calculator);
            //
            proxyInstance.add(1, 2);
            proxyInstance.div(1, 0);
        }
    }
    

2. AOP相关概念

2.1 AOP概念&作用

AOP(Aspect Oriented Programming)是一种思想,面向切面编程

作用:在不修改源码的前提下,在程序运行过程中对方法进行增强。

解耦、方便维护、开发效率高、代码复用。

底层是动态代理,分为两种:

JDK动态代理,基于接口的代理,能对接口或者接口的实现类进行增强

cglib动态代理,基于父类的代理,该类不能被final修饰。

Spring底层会根据目标对象的特性判断选用其中一个:如果实现了接口,用JDK;否则使用cglib。我们也可以强制Spring使用cglib。

不管使用任何一种实现,都不需要我们写代码。只需要配置即可.

OOP(Object Oriented Programming)是一种思想,面向对象编程

2.2 AOP优势

耦合低:分离业务代码和系统辅助代码,高内聚低耦合,易维护

复用强:系统辅助代码复用性更高

易拓展:插拔式组件设计,拓展简单

2.3 核心名词概念

Target(目标对象):被代理的目标对象

Proxy (代理):增强后的对象,是在程序运行期间动态生成的。

JoinPoint(连接点):有可能被拦截并增强的方法,目标对象中所有的方法都是连接点;

Pointcut(切入点):也叫切点,真正被拦截并且增强的方法;

Advice(通知/ 增强):就是增强方法/增强的内容;

Aspect(切面):通知/增强 + 切点(概念比较模糊)

Weaving(织入):把通知、切点配置在一起并最终运行产生代理对象的过程就是织入,是一个过程概念。

1603771935666

2.4 AOP的应用场景

  • 日志记录处理
  • 事务管理
  • 权限校验
  • 信息/邮件发送
  • 性能监控优化
  • ……

2.5 实现AOP需要做的事情

  1. 编码。核心业务代码、系统辅助代码
  2. 配置。切点表达式;装配Bean进入Spring容器

3. AOP快速入门

3.1 导入 AOP 相关依赖坐标

<properties>
    <!-- 明确maven使用jdk1.8编译该模块 -->
    <project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>

    <!-- 统一模块中依赖版本-->
    <spring.version>5.1.9.RELEASE</spring.version>
    <aspectj.version>1.9.4</aspectj.version>
    <junit.version>4.12</junit.version>
</properties>



<dependencies>
    <!--
        Spring-context依赖
        会依赖导入spring-aop
     -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!-- SpringAOP依赖aspectj -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>${aspectj.version}</version>
    </dependency>

    <!-- spring测试 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>

3.2编写目标接口和目标类

  • 目标接口

    package com.itheima.calc;
    /**
     * @Description: 计算器接口
     */
    public interface Calculator {
    	/**
    	 * 加
    	 */
    	public int add(int i, int j);
    
    	/**
    	 * 减
    	 */
    	public int sub(int i, int j);
    
    	/**
    	 * 乘
    	 */
    	public int mul(int i, int j);
    
    	/**
    	 * 除
    	 */
    	public int div(int i, int j);
    
    }
    
  • 目标接口实现类

    package com.itheima.calc;
    /**
     * @Description: 计算器接口实现类
     */
    public class MyMathCalculator implements Calculator{
    
    	@Override
    	public int add(int i, int j) {
    		int result = i + j;
    		return result;
    	}
    
    	@Override
    	public int sub(int i, int j) {
    		int result = i - j;
    		return result;
    
    	}
    
    	@Override
    	public int mul(int i, int j) {
    		int result = i * j;
    		return result;
    	}
    
    	@Override
    	public int div(int i, int j) {
    
    		int result = i / j;
    
    		return result;
    	}
    
    }
    

3. 3 编写 通知/增强 类

  • 通知类

    package com.itheima.advice;
    
    /**
     * @Description: 计算机的通知类
     */
    public class CalculatorAdvice {
    
        public  void logStart() {
            System.out.println("方法开始计算了xxxxx");
        }
    }
    

3.4 将目标类和切面(通知/增强)类的对象创建权交给 spring

  • beans.xml

    <!-- 装配  目标calculator-->
    <bean class="com.itheima.calc.MyMathCalculator" id="myMathCalculator"/>
    
    
    <!-- 装配  通知advice -->
    <bean class="com.itheima.advice.CalculatorAdvice" id="advice"/>
    

3.5 在 beans.xml 中配置织入关系

  • beans.xml

    <!--
    aspect oriented programming
    -->
    <!-- 配置织入 -->
    <aop:config >
        <!--
            配置切面
            切面 = 切点 + 通知
            通知引入  ref属性指向Sprig容器中已经存在的一个通知对象
         -->
        <aop:aspect ref="advice">
            <!-- 切点 -->
            <!--<aop:pointcut id="pt" expression="execution(* *..*(..))"/>-->
            <!-- 
    			配置前置通知
     			aop:xxxx	表示在什么时机增强,前后/最终/异常
    			method      当前这个时机,到底要如何增强,通知类中的某个通知方法
    			pointcut[-ref]	明确切点
    		-->
            <aop:before method="logStart" pointcut="execution(* *..*(..))"/>
        </aop:aspect>
    </aop:config>
    

3.6 测试代码

  • LogAOPTest.java

    /**
     * @Description: 测试类
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:beans.xml")
    //@ContextConfiguration(classes = {SpringConfig.class})
    public class LogAOPTest {
    
    
        // 使用接口类型的引用接收
        @Autowired
        Calculator myMathCalculator;
    
        // 需求:使用springAOP为MyMathCalculator类的所有方法添加记录日志的功能
        // 只需要完成在方法开始前记录日志即可。
        @Test
        public void test01() {
            System.out.println("myMathCalculator.add(1, 2) = " + myMathCalculator.add(1, 2));
    
        }
    }
    

4. AOP的XML配置

4.1 基本配置

<!--
aspect oriented programming
-->
<!-- 配置织入(可以配置多组同时生效) -->
<aop:config >
    <!--
        配置切面  (可以配置多组同时生效) 
        切面 = 切点 + 通知
        通知引入  ref属性指向Sprig容器中已经存在的一个通知对象
     -->
    <aop:aspect ref="advice">
        <!-- 切点 -->
        <!--<aop:pointcut id="pt" expression="execution(* *..*(..))"/>-->
        <!-- 配置前置通知 -->
        <aop:before method="logStart" pointcut="+"/>
    </aop:aspect>
</aop:config>

4.2 切点表示式

  • 明确哪些连接点最终成为切点

    <!--
        切点表达式:
            格式:关键字([访问权限修饰符] 返回值类型 包名.类名.方法名(参数类型列表) [异常类型])
            execution(int com.itheima.*.*(int, int))
            访问权限修饰符/异常类型 可以省略不写
            返回值类型、包名、类名、方法名、参数类型列表中可以使用* 统配代表任意
            类名中+表示匹配子类类型
            从后往前依次确定方法、类、包
            .表示当前包下的类
            .. 表示当前包及其子包下的类
            参数类型列表可以使用..代表任意参数类型及个数
    -->
    
    
    切割特定某个方法: execution(public void com.itheima.service.UserServiceImpl.method())  
    	切割所有方法: execution(* *..*.*(..))
    
    其他:
    execution(* *(..))
    execution(* *..*(..))
    execution(* *..*.*(..))
    execution(public * *..*.*(..))
    execution(public int *..*.*(..))
    execution(public void *..*.*(..))
    execution(public void com..*.*(..))
    execution(public void com..service.*.*(..))
    execution(public void com.itheima.service.*.*(..))
    execution(public void com.itheima.service.User*.*(..))
    execution(public void com.itheima.service.*Service.*(..))
    execution(public void com.itheima.service.UserService.*(..))
    execution(public User com.itheima.service.UserService.find*(..))
    execution(public User com.itheima.service.UserService.*Id(..))
    execution(public User com.itheima.service.UserService.findById(..))
    execution(public User com.itheima.service.UserService.findById(int))
    execution(public User com.itheima.service.UserService.findById(int,int))
    execution(public User com.itheima.service.UserService.findById(int,*))
    execution(public User com.itheima.service.UserService.findById(*,int))
    execution(public User com.itheima.service.UserService.findById())
    execution(List com.itheima.service.*Service+.findAll(..))
    
  • 切点表达式的分类

    根据切点表达式的定义位置分类

    <aop:config>
        <!--配置公共切入点-->
        <aop:pointcut id="pt1" expression="execution(* *(..))"/>
        <aop:aspect ref="myAdvice">
            <!--配置局部切入点-->
            <aop:pointcut id="pt2" expression="execution(* *(..))"/>
            <!--引用公共切入点-->
            <aop:before method="logAdvice" pointcut-ref="pt1"/>
            <!--引用局部切入点-->
            <aop:before method="logAdvice" pointcut-ref="pt2"/>
            <!--直接配置切入点-->
            <aop:before method="logAdvice" pointcut="execution(* *(..))"/>
        </aop:aspect>
    </aop:config>
    
  • 切点表达式经验

细心,在配置修改、代码修改前后一定要反复确认检查

企业开发命名规范严格遵循规范文档进行
⚫ 先为方法配置局部切入点
⚫ 再抽取类中公共切入点
⚫ 最后抽取全局切入点
⚫ 代码走查过程中检测切入点是否存在越界性包含
⚫ 代码走查过程中检测切入点是否存在非包含性进驻
⚫ 设定AOP执行检测程序,在单元测试中监控通知被执行次数与预计次数是否匹配
⚫ 设定完毕的切入点如果发生调整务必进行回归测试
(以上规则适用于XML配置格式)

4.3通知分类

  • Spring原生支持的通知类型

    前置、后置(返回)、异常、引介、环绕,没有最终。

  • Aspectj支持的通知类型

    前置、后置(返回)、异常、引介、环绕、最终。

  • 各种通知的执行时机和顺序

    // 四种基本类型的通知执行的时机
    try{
        // 前置通知   <aop:before method="前置通知方法名" pointcut-ref="yyy"/>
        // 目标方法
        // 后置通知  <aop:after-returning method="xxx" pointcut-ref="yyy"/>
    }catch(Exception e){
        // 异常通知  <aop:after-throwing method="xxx" pointcut-ref="yyy"/>
    }finally{
        // 最终通知  <aop:after method="xxx" pointcut-ref="yyy"/>
    }
    
    // 环绕通知一个打4个 <aop:around method="环绕通知的方法" pointcut-ref="yyy"/>
    // 环绕通知的用法
    public Object around(ProceedingJoinPoint pjp) {
        Object ret = null;
      	try{
            System.out.println("前置");
            //调用目标方法,就是切点方法
            ret = pjp.proceed();
            System.out.println("后置" + ret);
    
        } catch (Throwable throwable) {
            System.out.println("异常" + throwable.getMessage());
        } finally {
            System.out.println("最终" + ret);
        }
        return ret;
    }
    
  • 环绕通知注意事项

    1. 环绕通知最强大;也最特殊:通知方法内部需要手动调用目标方法
    2. 形参上需要添加ProceedingJoinPoint的对象,封装的切点(当前的目标方法)
    3. 手动调用目标方法时的返回值需要return出去,并且类型要是Object类型否则外界无法获取该值
    4. 调用目标方法时,需要处理异常,建议使用try…catch,并且使用最大号的异常throwable来处理

4.4 通知的执行顺序

  • 基本通知,只有一组通知的顺序

    没有异常:前置、后置、最终<后面两个的顺序取决于这两者的配置顺序>)

    有异常:前置、异常、最终<后面两个的顺序取决于这两者的配置顺序>>

  • 环绕通知,,只有一组通知的顺序

    没有异常:前置、后置、最终

    有异常:前置、异常、最终

  • 多组通知的顺序

    配置的顺序即为执行顺序

4.5 通知中获取目标方法的方法名

所有类型通知都可以获取方法名

  • 四种基本通知

    通知方法上,第一个形参写成JoinPonit jp,方法中使用该对象获取方法名

    public void before(JoinPonit jp){
        // 获取方法名
        jp.getSignature().getName();
    }
    
  • 环绕通知

    通知方法上,第一个形参写成ProceedingJoinPonit pjp,方法中使用该对象获取方法名

    public void before(ProceedingJoinPonit pjp){
        // 获取方法名
        pjp.getSignature().getName();
    }
    

4.6 通知中获取目标方法的实参

所有类型通知都可以获取实参

  • 四种基本通知

    通知方法上,第一个形参写成JoinPonit jp,方法中使用该对象获取方法名

    public void before(JoinPonit jp){
        // 获取实参数据
        Object[]  args = jp.getArgs();
    }
    
  • 环绕通知

    通知方法上,第一个形参写成ProceedingJoinPonit pjp,方法中使用该对象获取方法名

    public void before(ProceedingJoinPonit pjp){
        // 获取实参数据
        Object[]  args = pjp.getArgs();
    }
    
  • argsName的方式不推荐使用

4.7 通知中获取目标方法的返回值

  • 可以获取目标方法返回值的通知类型:环绕通知后置通知

  • 后置通知

    通知方法上,添加一个形参Object xxx,方法中可以直接使用,JoinPonit /ProceedingJoinPonit必须是第一个参数

    public void afterReturningxxx(JoinPonit jp, Object xxx){
        // 直接使用xxx
    }
    

    <aop:after-returning>上添加returning属性,值与通知方法形参名一致。

    <aop:after-returning method="afterReturningxxx" pointcut-ref="pt" returning="xxx"/>
    

​ 调用目标方法时生成的返回值,就会自动赋值到了xxx上面,可以在通知方法中使用了。

  • 环绕通知

    在环绕通知的方法内部直接获取并使用即可

    // **********************
    // 环绕通知方法返回值类型必须定义,而且建议使用Object
    public Object around(ProceedingJoinPoint pjp) {
            // 返回值和异常对象可以直接获取
            // 方法名和方法参数通过切点对象获取
            pjp.getSignature().getName();
            pjp.getArgs();
    
            Object ret = null;
            try {
                System.out.println(pjp.getSignature().getName() + "的环绕前置" + Arrays.toString(pjp.getArgs()));
                // ************************
                //对原始方法的调用,这里可以直接获取返回值并使用
                // ********************
                ret = pjp.proceed();
                System.out.println("后置" + ret);
    
            } catch (Throwable throwable) {
                System.out.println("异常" + throwable.getMessage());
            } finally {
                System.out.println("最终" + ret);
            }
        	// ***************************************** 
        	// 目标方法调用生成的返回值一定要return出去 
            return ret;
        }
    

4.8 通知中获取目标方法的异常对象

  • 可以获取目标方法异常对象的通知类型:环绕通知异常通知

  • 异常通知

    通知方法上,添加一个形参Throwable xxx,方法中可以直接使用,JoinPonit /ProceedingJoinPonit必须是第一个参数

    public void afterThrowingxxx(JoinPonit jp, Throwable xxx){
        // 直接使用xxx
    }
    

    <aop:after-throwing>上添加returning属性,值与通知方法形参名一致。

    <aop:after-throwing method="afterThrowingxxx" pointcut-ref="pt" throwing="xxx"/>
    

​ 调用目标方法时如果产生了异常,就会自动赋值到了通知方法的形参xxx上面,就可以在通知方法中使用。

  • 环绕通知

    在环绕通知的方法内部直接获取并使用即可

    public Object around(ProceedingJoinPoint pjp) {
            // 返回值和异常对象可以直接获取
            // 方法名和方法参数通过切点对象获取
            pjp.getSignature().getName();
            pjp.getArgs();
    
            Object ret = null;
            try {
                System.out.println(pjp.getSignature().getName() + "的环绕前置" + Arrays.toString(pjp.getArgs()));
                //对原始方法的调用
                ret = pjp.proceed();
                System.out.println("后置" + ret);
    
            } catch (Throwable throwable) {
                // ********************************
                // 这里直接可以获取该异常对象,并使用
                System.out.println("异常" + throwable.getMessage());
            } finally {
                System.out.println("最终" + ret);
            }
        	// 目标方法调用生成的返回值一定要return出去 
            return ret;
        }
    
  • 注意

    使用环绕通知处理异常对象的时候,捕获后会不打印默认的日志堆栈信息,如果想要打印,可以:

    throwable.printStackTrace();
    或者
    throw new RuntimeException(throwable);  
    

5. AOP的注解配置

5.1 注解配置和XML配置对比

1603798584726

1603799286984

5.2 注解配置AOP步骤

  1. 导入依赖坐标

    <!-- 与xml配置完全相同 -->
    
  2. 开启aspectj的自动代理

    • <aop:aspectj-autoproxy/>@Aspect成对出现

    已经学习过的成对出现如下:

    • <context:component-scan/>@Component成对出现
  • Spring配置文件beans.xml

    <!-- 开启aspectj的自动代理 -->
    <aop:aspectj-autoproxy/>
    <!-- 开启组件扫描 -->
    <context:component-scan base-package="com.itheima"/>
    
    
  1. 在切面类及其方法上添加 各种注解实现功能

    切面 = 切点 + 通知,通过注解配置的时候体现的更加直观。

    切面类AOPAdvice.java

    @Component
    @Aspect
    public class AOPAdvice {
    
        // 切点最终体现为一个方法,无参无返回值,无实际方法体内容,但不能是抽象方法
        @Pointcut("execution(* *..*(..))")
        public void pt(){}
    
        // 引用切入点时必须使用方法调用名称,方法后面的()不能省略
        @Before("pt()")
        public void before(){
            System.out.println("前置before...");
        }
    }
    
  • 切点表达式可以单独写在一个类中,在其他类中使用

    public class AOPPointcut {
        @Pointcut("execution(* *..*(..))")
        public void pt1(){}
    }
    
  @Component
  @Aspect
  public class AOPAdvice {
  
      // 引用切入点时必须使用方法调用名称,方法后面的()不能省略
      @Before("AOPPointcut.pt()")
      public void before(){
          System.out.println("前置before...");
      }
  }

对应内容的获取与xml配置思路一致。

4.3 通知的执行顺序

通常是要么只用环绕通知,或者只使用基本通知(前置、后置、异常、最终),而不是两者混用。

  • 基本通知,只有一组通知的顺序(绝对不要用,因为会先释放资源,再返回结果,可以会有问题)

    没有异常:前置、最终、后置

    有异常:前置、最终、异常

  • 环绕通知,只有一组的通知顺序(工作中推荐用法)

    没有异常:前置、后置|异常、最终

  • 多组通知的顺序

    受类名和方法名影响,按照两者的字典顺序排序。

4.4 全注解实现AOP

使用注解开启AOP自动代理(同时组件扫描也建议使用注解实现,这样可以干掉xml配置文件)

  • @EnableAspectJAutoProxy@Aspect成对出现

  • @ComponentScan("basePackage")@Component成对出现

相应的,xml+注解配置的成对出现如下

  • <aop:aspectj-autoproxy/>@Aspect成对出现

  • <context:component-scan/>@Component成对出现

  • 主配置类SpringConfig.java配置如下(全注解配置的环境下)

    @ComponentScan("com.itheima") // 组件扫描
    @EnableAspectJAutoProxy			// 开启AOP自动代理
    public class SpringConfig {
    }
    
  • 切面类AOPAdvice.java(与XML实现方式一样)

    @Component
    @Aspect
    public class AOPAdvice {
    
        // 切点最终体现为一个方法,无参无返回值,无实际方法体内容,但不能是抽象方法
        @Pointcut("execution(* *..*(..))")
        public void pt(){}
    
        // 引用切入点时必须使用方法调用名称,方法后面的()不能省略
        @Before("pt()")
        public void before(){
            System.out.println("前置before...");
        }
    }
    
  • 测试代码

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes = SpringConfig.class)
    public class UserServiceTest {
        @Autowired
        private UserService userService;
    
        @Test
        public void testSave(){
            int ret = userService.save(888, 666);
            Assert.assertEquals(100,ret);
        }
    }
    
  • 目标接口和目标接口实现类

    public interface UserService {
        int save(int i, int m);
    }
    
    @Service("userService")
    public class UserServiceImpl implements UserService {
    
        public int save(int i,int m){
            System.out.println("user service running..."+i+","+m);
            //System.out.println("1/0 = " + 1 / 0);
            return 100;
        }
    }
    
  • 运行结果

    环绕前around before...
    user service running...888,666
    环绕后around after-returning...
    环绕最终around after...
    

6. 强制使用cglib

强制Spring底层使用cglib动态代理。

配置方式

  • 纯xml配置:

    <aop:config proxy-target-class="true">
    
  • xml+注解配置

    <aop:aspectj-autoproxy proxy-target-class="true"/>
    
  • 纯注解配置:

    @EnableAspectJAutoProxy(proxyTargetClass = true)
    

效果验证

在测试类中,打印代理对象,代码如下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:beans.xml")
//@ContextConfiguration(classes = {SpringConfig.class})
public class LogAOPTest {


    @Autowired
    Calculator myMathCalculator;

    // 需求:使用springAOP为MyMathCalculator类的所有方法添加记录日志的功能
    // 只需要完成在方法开始前记录日志即可。
    @Test
    public void test01() {
        System.out.println(myMathCalculator.getClass());
    }
}

输出结果

jdk动态代理生成的代理对象。

class com.sun.proxy.$Proxy15

cglib动态代理生成的代理对象

class com.itheima.calc.MyMathCalculator$$EnhancerBySpringCGLIB$$3c0bc2aa

7. 执行性能统计案例

  1. 切面类RunTimeMonitorAdvice.java,使用环绕通知完成

    @Component
    @Aspect
    public class RunTimeMonitorAdvice {
    
        //切入点,监控业务层接口
        @Pointcut("execution(* com.itheima.service.*Service.find*(..))")
        public void pt(){}
    
        @Around("pt()")
        public Object runtimeAround(ProceedingJoinPoint pjp) throws Throwable {
            //获取执行签名信息
            Signature signature = pjp.getSignature();
            //通过签名获取执行类型(接口名)
            String className = signature.getDeclaringTypeName();
            //通过签名获取执行操作名称(方法名)
            String methodName = signature.getName();
    
            //执行时长累计值
            long sum = 0L;
    
            for (int i = 0; i < 10000; i++) {
                //获取操作前系统时间beginTime
                long startTime = System.currentTimeMillis();
                //原始操作调用
                pjp.proceed(pjp.getArgs());
                //获取操作后系统时间endTime
                long endTime = System.currentTimeMillis();
                sum += endTime-startTime;
            }
            //打印信息
            System.out.println(className+":"+methodName+"   (万次)run:"+sum+"ms");
            return null;
        }
    }
    
  2. dao层接口实现类/service层接口实现类

    // 略
    
  3. 配置类SpringConfig.java

    //@Configuration
    @ComponentScan("com.itheima")
    @PropertySource("classpath:jdbc.properties")
    @Import({JDBCConfig.class,MyBatisConfig.class})
    @EnableAspectJAutoProxy
    public class SpringConfig {
    }
    

    MyBatisConfig.java

  4. 测试类

    package com.itheima.service;
    
    import com.itheima.config.SpringConfig;
    import com.itheima.domain.Account;
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import java.util.List;
    
    //设定spring专用的类加载器
    @RunWith(SpringJUnit4ClassRunner.class)
    //设定加载的spring上下文对应的配置
    @ContextConfiguration(classes = SpringConfig.class)
    public class UserServiceTest {
    
        @Autowired
        private AccountService accountService;
    
        @Test
        public void testFindById(){
            Account ac = accountService.findById(2);
        }
    
        @Test
        public void testFindAll(){
            List<Account> list = accountService.findAll();
        }
    }
    
    
  5. 测试结果

    
    

8. AOP底层原理

8.1 JDK和cglib动态代理的区别

1603799455842

8.2 JDK动态代理

  • 目标接口和目标接口实现类Calculator.java

    public interface Calculator {
    
    	/**
    	 * 加
    	 */
    	public int add(int i, int j);
    
    	/**
    	 * 减
    	 */
    	public int sub(int i, int j);
    
    	/**
    	 * 乘
    	 */
    	public int mul(int i, int j);
    
    	/**
    	 * 除
    	 */
    	public int div(int i, int j);
    
    }
    
    /**
     * @Description: 计算器实现类,没有记录日志的功能
     */
    public class MyMathCalculator implements Calculator{
    
    	@Override
    	public int add(int i, int j) {
    		int result = i + j;
    		System.out.println("目标方法执行了:add方法调用成功111111");
    		return result;
    	}
    
    	@Override
    	public int sub(int i, int j) {
    		int result = i - j;
    		return result;
    
    	}
    
    	@Override
    	public int mul(int i, int j) {
    		System.out.println("目标方法执行了:mul方法调用成功111111");
    		int result = i * j;
    		return result;
    	}
    
    	@Override
    	public int div(int i, int j) {
    		System.out.println("目标方法执行了:div方法调用成功111111");
    		int result = i / j;
    		return result;
    	}
    
    }
    
  • JDK动态代理生成代理对象代码MyCalculatorProxyFactory.java

    /**
     * @Description:
     */
    public class MyCalculatorProxyFactory {
    
        /*
            使用动态代理对目标对象进行增强
                1. 调用目标对象的目标方法,实现原有的基础功能
                2. 在调用目标方法前后,添加增强的逻辑
            Proxy.newProxyInstance()
    
    
            动态代理特点:
                目标对象中所有的目标方法都会被拦截并增强
    
         */
    
        public static Calculator getProxyInstance(Calculator target){
            return (Calculator) Proxy.newProxyInstance(
                    // 类加载器,使用目标对象的类加载器即可
                    target.getClass().getClassLoader(),
                    // 目标对象已经实现的所有接口的字节码对象数组
                    target.getClass().getInterfaces(),
                    // 调用处理器(重点) 目标方法的调用、增强逻辑都通过该对象实现
                    new InvocationHandler() {
                        /**
                         * 目标方法的调用、增强逻辑都通过该对象实现
                         * @param proxy     代理对象,我们不用,给系统用
                         * @param method    目标方法封装的对象
                         * @param args      目标方法的实参
                         * @return          目标方法的返回值,该值必须return出去,否则外界无法获取该值(在有时)
                         * @throws Throwable
                         */
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
                            // 目标方法的调用
                            Object returnValue = null;
                            try {
                                System.out.println(method.getName() + "方法开始执行了,参数是:" + Arrays.toString(args));
                                //LogUtils.logStart(method.getName(), Arrays.toString(args));
                                returnValue = method.invoke(target, args);
    
                                System.out.println(method.getName() +"方法执行成功了,结果是:" + returnValue);
                            } catch (Exception e) {
                                System.out.println(method.getName() +"方法执行异常了,异常信息是:" + e.getMessage());
                                e.printStackTrace();
                            } finally {
                                System.out.println(method.getName() +"方法执行结束了,成功与否我不管");
                            }
    
    
                            // 该值必须return出去,否则外界无法获取该值(在有时)
                            return returnValue;
                        }
                    }
            );
        }
    }
    
  • 测试类代码

    /**
     * @Description: 动态代理记录日志效果测试类
     * main方法内分割线以上是使用目标对象计算,分割线以下是获取代理对象并计算;
     * 代理对象计算时有记录日志的功能
     */
    public class ProxyCalculatorlogTest {
    
        public static void main(String[] args) {
            Calculator calculator = new MyMathCalculator();
            System.out.println(calculator.add(1, 2));
            System.out.println(calculator.div(2, 1));
    
            System.out.println("---------------------------");
    
            // JDK动态代理 生成一个代理对象(实现了和目标对象/被代理对象一样的接口),使用代理对象调用方法
    
            Calculator proxyInstance = MyCalculatorProxyFactory.getProxyInstance(calculator);
            //
            proxyInstance.add(1, 2);
            //proxyInstance.div(1, 0);
    
            // proxyInstance.getClass().getSuperclass() = class java.lang.reflect.Proxy
            // proxyInstance.getClass().getInterfaces()[0] = interface com.itheima.calc.Calculator
            System.out.println("proxyInstance.getClass().getSuperclass() = " + proxyInstance.getClass().getSuperclass());
            System.out.println("proxyInstance.getClass().getInterfaces()[0] = " + proxyInstance.getClass().getInterfaces()[0]);
        }
    }
    

8.3 cglib动态代理

code generation library代码生成库,使用该动态代理,很容易出现递归,最终造成内存溢出。

  • 目标类UserServiceImpl.java

    public class UserServiceImpl{
        public void save() {
            System.out.println("水泥墙");
        }
    }
    
  • CGlib创建代理对象代码UserServiceCglibProxy.java

    public class UserServiceCglibProxy {
    
        public static UserServiceImpl createUserServiceCglibProxy(Class clazz){
            //1. 创建Enhancer对象(可以理解为内存中动态创建了一个类的字节码对象)
            Enhancer enhancer = new Enhancer();
            //2. 设置Enhancer对象的父类是指定类型UserServerImpl
            enhancer.setSuperclass(clazz);
            //3. 设置回调,调用目标方法 
            enhancer.setCallback(new MethodInterceptor() {
                /**
                 *
                 * @param proxyObject       代理对象
                 * @param targetMethod      目标方法
                 * @param args              目标方法调用时传递的实参
                 * @param methodProxy       代理方法(增强后的方法)
                 * @return                  目标方法的返回值
                 * @throws Throwable
                 */
                public Object intercept(Object proxyObject, Method targetMethod, Object[] args, MethodProxy methodProxy) throws Throwable {
                    //通过调用父类的方法实现对原始方法的调用
                    // 用错了,可以会掉子类对象的代理方法(递归,没有出口)
                    Object ret = methodProxy.invokeSuper(proxyObject, args);
                    //后置增强内容,与JDKProxy区别:JDKProxy仅对接口方法做增强,cglib对所有非私有方法做增强,包括从Object类中继承的方法
                    if(targetMethod.getName().equals("save")) {
                        System.out.println("刮大白3");
                        System.out.println("贴墙纸3");
                    }
                    return ret;
                }
            });
            //使用Enhancer对象创建对应的对象
            return (UserServiceImpl) enhancer.create();
        }
    }
    
  • 测试代码

    public class App {
        public static void main(String[] args) {
            UserServiceImpl userServiceImpl = UserServiceCglibProxy.createUserServiceCglibProxy(UserServiceImpl.class);
            userServiceImpl.save();
            //userServiceImpl.getClass() = class base.cglib.UserServiceImpl$$EnhancerByCGLIB$$b1102cf3
            //userServiceImpl.getClass().getSuperclass() = class base.cglib.UserServiceImpl
            System.out.println("userServiceImpl.getClass() = " + userServiceImpl.getClass());
            System.out.println("userServiceImpl.getClass().getSuperclass() = " + userServiceImpl.getClass().getSuperclass());
        }
    }
    

8.4 织入时机

  1. 编译期织入

    运行速度最快

    灵活性最差

    编译即锁定

  2. 加载期织入

    运行速度较快

    灵活性适中

    多次加载可以变成实现逻辑

  3. 运行期织入(Spring默认选用)

    运行速度最慢

    灵活性最强

    每次执行都可以改变实现逻辑

            * @throws Throwable
            */
           public Object intercept(Object proxyObject, Method targetMethod, Object[] args, MethodProxy methodProxy) throws Throwable {
               //通过调用父类的方法实现对原始方法的调用
               // 用错了,可以会掉子类对象的代理方法(递归,没有出口)
               Object ret = methodProxy.invokeSuper(proxyObject, args);
               //后置增强内容,与JDKProxy区别:JDKProxy仅对接口方法做增强,cglib对所有非私有方法做增强,包括从Object类中继承的方法
               if(targetMethod.getName().equals("save")) {
                   System.out.println("刮大白3");
                   System.out.println("贴墙纸3");
               }
               return ret;
           }
       });
       //使用Enhancer对象创建对应的对象
       return (UserServiceImpl) enhancer.create();
    

    }
    }




- 测试代码

```java
public class App {
    public static void main(String[] args) {
        UserServiceImpl userServiceImpl = UserServiceCglibProxy.createUserServiceCglibProxy(UserServiceImpl.class);
        userServiceImpl.save();
        //userServiceImpl.getClass() = class base.cglib.UserServiceImpl$$EnhancerByCGLIB$$b1102cf3
        //userServiceImpl.getClass().getSuperclass() = class base.cglib.UserServiceImpl
        System.out.println("userServiceImpl.getClass() = " + userServiceImpl.getClass());
        System.out.println("userServiceImpl.getClass().getSuperclass() = " + userServiceImpl.getClass().getSuperclass());
    }
}

8.4 织入时机

  1. 编译期织入

    运行速度最快

    灵活性最差

    编译即锁定

  2. 加载期织入

    运行速度较快

    灵活性适中

    多次加载可以变成实现逻辑

  3. 运行期织入(Spring默认选用)

    运行速度最慢

    灵活性最强

    每次执行都可以改变实现逻辑

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值