spring-aop笔记

2、Spring-AOP

2.1、代理【Proxy】

  • 动态代理:

    • 特点:字节码谁用谁创建,谁用谁加载

    • 作用:不修改源码的基础上对方法增强

    • 分类:

      • 基于接口的动态代理

      • 基于子类的动态代理

    • 基于接口的动态代理

      • 涉及的类:Proxy

      • 提供者:JDK官方

    • 如何创建代理对象:

      • 使用Proxy类中的newProxyInstance方法

    • 创建代理对象的要求:

      • 被代理类最少实现一个接口,如果没有则不能使用

    • newProxyInstance方法的参数:

      • ClassLoader:类加载器

        • 它是用于加载代理对象字节码的.和被代理对象使用相同的类加载器。固定写法。

      • Class[]

        • 它是用于让代理对象和被代理对象有相同方法。固定写法。

      • InvocationHandler

        • 它是让我们写如何代理。我们一般都是写一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。

        • 此接口的实现类都是谁用谁写

来测试一下:

2.1.1、未使用代理

1.定义一个接口,要被代理的接口:Producer【提供者】

 //生产者
 public interface Producer {
     //销售
     public void saleProduct(float money);
     //售后
     public void afterService(float money);
 }

2.定义一个类,ProducerImpl【提供者】(要被代理的角色)

 //生产者
 public class ProducerImpl implements Producer {
     //销售
     public void saleProduct(float money){
         System.out.println("销售产品,拿到钱:" + money);
    }
     //售后
     public void afterService(float money){
         System.out.println("售后服务,拿到钱:" + money);
    }
 }

3.定义一个类,Client【消费者】(未使用代理,直接面向厂家)

 //模拟一个消费者(未使用代理)
 public class Client {
     public static void main(String[] args){
         IProducer producer = new Producer();
         producer.saleProduct(1000f);
 
    }
 }

结果:image-20200805211427287

2.1.2、使用了代理

1.定义一个类,ProxyMan【代理人】(不收中介费的时候)

 public class ProxyMan implements InvocationHandler {
 
     private Object target;
 
     public void setTarget(Object target) {
         this.target = target;
    }
 
     public Object getProxy(){
         return Proxy.newProxyInstance(this.getClass().getClassLoader() ,target.getClass().getInterfaces() ,this);
    }
 
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         return method.invoke(target ,args);
    }
 }

2.定义一个类,Client【消费者】(使用了代理)

 //模拟一个消费者
 public class Client {
 
     public static void main(String[] args){
         IProducer producer = new Producer();
 //       producer.saleProduct(1000f);
         //代理模式
         ProxyMan pm = new ProxyMan();
         pm.setTarget(producer);
         IProducer producerPoxy = (IProducer)pm.getProxy();
         producerPoxy.afterService(1000f);
 
    }
 }

结果:image-20200805211914489

3.当代理人收代理费时(抽取两成):

 public class ProxyMan implements InvocationHandler {
 
     private Object target;
 
     public void setTarget(Object target) {
         this.target = target;
    }
 
     public Object getProxy(){
         return Proxy.newProxyInstance(this.getClass().getClassLoader() ,target.getClass().getInterfaces() ,this);
    }
 
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         Object value = null;
         Float money = (Float) args[0];
         if("saleProduct".equals(method.getName())) {
             value = method.invoke(target, money * 0.8f);
        }
         if("afterService".equals(method.getName())){
             value = method.invoke(target ,args);
        }
         return value;
    }
 }

结果:image-20200805212348480

被成功抽取两成!

在不改变原有代码的时候,改变传输的信息。不仅仅可以改变值,还可以增加其他东西,代理模式大多数情况用在增加日志等功能。

 

 

 

 

2.2、Spring中的AOP

2.2.1、AOP相关术语:

  • Joinpoint(连接点):

    • 所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。

  • Pointout(切入点):

    • 所谓切入点是指我们要对那些Joinpoint进行拦截的定义。

  • Advice(通知/增强)

    • 所谓通知是指拦截到Joinpoint之后所要做的事情就是通知。

    • 通知类型:前置通知,后置通知,异常通知,环绕通知。

  • Introduction(引介):

    • 引介是一种特殊的通知,在不修改类代码的前提下,Introduction可以在运行期为类动态的添加一些方法或Field。

  • Target(目标对象)

    • 代理的目标对象(也就是说被代理的对象)

  • Weaving(织入):

    • 是指把增强应用到目标对象来创建新的代理对象的过程。

    • spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

  • Proxy(代理):

    • 一个类被AOP织入增强后,就产生一个结果代理类。

  • Aspect(切面):

    • 是切入点和通知(引介)的结合。

 

2.2.2、AOP的配置内容

spring中基于XML的AOP配置步骤:

  1. 把通知Bean也较给spring来管理

     <!-- 配置Logger类 -->
     <bean id="logger" class="com.gzk.utils.Logger"></bean>

     

  2. 使用aop:config标签表明开始AOP的配置

  3. 使用aop:aspect标签表明配置切面

    • id属性:是给切面提供一个唯一标识

    • ref属性:是指定通知类bean的ID

  4. 在aop:aspect标签的内部使用对应标签来配置通知的类型

    • 我们现在示例是让pringLog方法在切入点方法执行之前:所以是前置通知

    • aop:before:表示配置前置通知

      • method属性:用于指定Logger类中哪个方法是前置通知

      • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    • aop:after-returning:表示配置后置通知

      • method属性:用于指定Logger类中哪个方法是后置通知

      • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    • aop:after-throwing:表示配置异常通知

      • method属性:用于指定Logger类中哪个方法是异常通知

      • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    • aop:after:表示配置最终通知

      • method属性:用于指定Logger类中哪个方法是最终通知

      • pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

    • 切入点表达式的写法:

      • 关键字:execution(表达式)

      • 表达式:

        • 访问修饰符 返回值 包名.包名...类名.方法名(参数列表)

      • 标准表达式写法(例):

        • public void com.gzk.servicce.UserServiceImpl.test()

      • 访问修饰符可以省略

        • void com.gzk.servicce.UserServiceImpl.test()

      • 返回值可以使用通配符,表示任意返回值

        • * com.gzk.servicce.UserServiceImpl.test()

      • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*

        • * *.*.*.UserServiceImpl.test()

      • 包名可以使用..表示当前包及其子包

        • * *..UserServiceImpl.test()

      • 类名和方法名都可以使用*来实现通配

        • * *..*.*()

      • 参数列表:

        • 可以直接写数据类型:

          • 基本类型直接写名称:int(例)

            • * *..*.*(int)

          • 引用类型写包名.类名的方式:java.lang.String(例)

            • * *..*.*(java.lang.String)

        • 可以使用通配符*表示任意类型,但是必须有参数

          • * *..*.*(*)

        • 可以使用..表示有无参数均可,有参数可以使任意类型

          • * *..*.*(..)

      • 全通配写法:

        • * *..*.*(..)

      • 实际开发中,很少用全通配写法,因为会全部被代理,切入点表达式的通常写法:

        • 切到业务层或者要被代理的层(或包)实现类下的所有方法:

          • * com.gzk.service.*.*(..)

 

2.2.3、测试(非注解)

测试四种常用通知类型【前置通知,后置通知,异常通知,最终通知】

1.随意配置mapper和service包下的类,用作测试

 public interface UserMapper {
     void test();
 }
 public class UserMapperImpl implements UserMapper {
     public void test() {
         System.out.println("测试中。。。");
    }
 }
 public interface UserService {
     void test();
 }
 public class UserServiceImpl implements UserService {
 
     private UserMapper userMapper;
 
     public void setUserMapper(UserMapper userMapper) {
         this.userMapper = userMapper;
    }
 
     public void test() {
         int i = 1/0;
         userMapper.test();
    }
 }

2.将UserServiceImpl设为被代理的类

3.新建一个utils包,创建一个Logger类【代理类】

 public class Logger {
 
     public void beforeLogger(){
         System.out.println("前置通知");
    }
 
     public void afterReturningLogger(){
         System.out.println("后置通知");
    }
 
     public void afterThrowingLogger(){
         System.out.println("异常通知");
    }
 
     public void afterLogger(){
         System.out.println("最终通知");
    }
 
 }

4.配置xml文件

 <?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
         https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
 
     <bean id="userMapper" class="com.gzk.mapper.UserMapperImpl"/>
     
     <!-- 配置被代理类UserServiceImpl -->
     <bean id="userService" class="com.gzk.service.UserServiceImpl">
         <property name="userMapper" ref="userMapper"/>
     </bean>
 
     <!-- 配置代理类Logger -->
     <bean id="logger" class="com.gzk.utils.Logger"/>
 
     <!-- 配置AOP -->
     <aop:config>
         <!-- 配置切面 -->
         <aop:aspect id="aspectLogger" ref="logger">
             <!-- 配置通知类型,并且建立通知方法和切入点方法的关联 -->
             <!-- 配置前置通知 -->
             <aop:before method="beforeLogger" pointcut="execution(* com.gzk.service.*.*(..))"/>
             <!-- 配置后置通知 -->
             <aop:after-returning method="afterReturningLogger" pointcut="execution(public void com.gzk.service.UserServiceImpl.test())"/>
             <!-- 配置异常通知 -->
             <aop:after-throwing method="afterThrowingLogger" pointcut="execution(* com.gzk.service.*.*(..))"/>
             <!-- 配置最终通知 -->
             <aop:after method="afterLogger" pointcut="execution(public void com.gzk.service.UserServiceImpl.test())"/>
 
         </aop:aspect>
     </aop:config>
 
 </beans>

5.测试:

 public class UserTest {
 
     @Test
     public void test(){
         ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
         UserService userService = context.getBean("userService", UserService.class);
         userService.test();
    }
 
 }

6.结果

image-20200806155742021

当发生异常时:

image-20200806155825453

 

2.2.4、通用化切入点配置

 <?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
         https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
 
     <bean id="userMapper" class="com.gzk.mapper.UserMapperImpl"/>
     <bean id="userService" class="com.gzk.service.UserServiceImpl">
         <property name="userMapper" ref="userMapper"/>
     </bean>
 
     <!-- 配置Logger类 -->
     <bean id="logger" class="com.gzk.utils.Logger"/>
 
     <!-- 配置AOP -->
     <aop:config>
         <!-- 配置切面 -->
         <aop:aspect id="aspectLogger" ref="logger">
             <!-- 配置通知类型,并且建立通知方法和切入点方法的关联 -->
             <!-- 配置前置通知 -->
             <aop:before method="beforeLogger" pointcut-ref="point-cut"/>
             <!-- 配置后置通知 -->
             <aop:after-returning method="afterReturningLogger" pointcut-ref="point-cut"/>
             <!-- 配置异常通知 -->
             <aop:after-throwing method="afterThrowingLogger" pointcut-ref="point-cut"/>
             <!-- 配置最终通知 -->
             <aop:after method="afterLogger" pointcut-ref="point-cut"/>
 
             <!-- 配置切入点表达式 -->
             <!--
                 注意:
                     此标签写在aop:aspect标签内部只能当前标签使用
                     它还可以写在aop:aspect标签外面,则所有切面都可以使用它
                     若是写在aop:aspect标签外面,这必须按照aop的规则和顺序约束,要把<aop:poincut>写在最前面,也就是aop:config下面
             -->
             <aop:pointcut id="point-cut" expression="execution(* com.gzk.service.*.*(..))"/>
 
         </aop:aspect>
     </aop:config>
 
 </beans>

2.2.5、环绕通知

1.将其他通知注释掉,写一个环绕通知:

 <aop:around method="aroundLogger" pointcut-ref="point-cut"/>

2.在Logger类中加入环绕方法:

 public void aroundLogger(){
     System.out.println("环绕通知");
 }

3.结果:image-20200806163001414

 

很明显出问题了,被代理类中的方法没了,是什么问题呢?

  • 问题:

    • 当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了。

  • 分析:

    • 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码没有。

  • 解决:

    • spring框架为我们提供了一个接口,ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。

    • 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架会为我们提供接口的实现类供我们调用。

好了,问题找到且分析了,那我们开始写代码吧

xml配置文件不用变,但Logger代理类中的aroundLogger()方法就要改变。

 //环绕通知
 public Object aroundLogger(ProceedingJoinPoint point){
     Object[] args = point.getArgs();
     Object proceed;
     try {
         //前置通知
         System.out.println("前置通知");
         proceed = point.proceed(args);
         //后置通知
         System.out.println("后置通知");
         return proceed;
    }catch (Throwable e){
         //异常通知
         System.out.println("异常通知");
         throw new RuntimeException(e);
 
    }finally {
         //最终通知
         System.out.println("最终通知");
    }
 
 }

结果:image-20200806164245750

 

2.2.6、测试(注解)

1.通过注解方式随意配置mapper和service包下的类,用作测试

 public interface UserMapper {
     void test();
 }
 @Repository("userMapper")
 public class UserMapperImpl implements UserMapper {
     public void test() {
         System.out.println("测试中。。。");
    }
 }
 public interface UserService {
     void test();
 }
 @Service("userService")
 public class UserServiceImpl implements UserService {
 
     @Autowired
     private UserMapper userMapper;
 
     public void test() {
         userMapper.test();
    }
 }

2.将UserServiceImpl设为被代理的类

3.新建一个utils包,创建一个Logger类【代理类】(使用注解)

 @Component("logger")
 @Aspect
 public class Logger {
 
     @Pointcut("execution(* com.gzk.service.*.*(..))")
     public void pointcut(){}
 
     @Before("pointcut()")
     public void beforeLogger(){
         System.out.println("前置通知");
    }
 
     @AfterReturning("pointcut()")
     public void afterReturningLogger(){
         System.out.println("后置通知");
    }
 
     @AfterThrowing("pointcut()")
     public void afterThrowingLogger(){
         System.out.println("异常通知");
    }
 
     @After("pointcut()")
     public void afterLogger(){
         System.out.println("最终通知");
    }
 
     //环绕通知
     @Around("pointcut()")
     public Object aroundLogger(ProceedingJoinPoint point){
         Object[] args = point.getArgs();
         Object proceed;
         try {
             //前置通知
             System.out.println("环绕-前置通知");
             proceed = point.proceed(args);
             //后置通知
             System.out.println("环绕-后置通知");
             return proceed;
        }catch (Throwable e){
             //异常通知
             System.out.println("环绕-异常通知");
             throw new RuntimeException(e);
        }finally {
             //最终通知
             System.out.println("环绕-最终通知");
        }
 
    }
 
 }

4.此时有两种方式来配置 xml 文件,一种是xml,一种是java

4.1、xml为配置文件

 <?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"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
         https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
 
     <!-- 配置要扫描的包 -->
     <context:component-scan base-package="com.gzk"/>
 
     <!-- 开启aop自动注解 -->
     <aop:aspectj-autoproxy/>
 
 </beans>

4.2、java为配置文件,创建一个config包,在下面新建一个类

 @Configuration
 @ComponentScan(basePackages = "com.gzk")
 @EnableAspectJAutoProxy
 public class ApplicationConfig {
 }

5.测试

5.1、使用xml为配置类测试:

 public class UserTest {
 
     @Test
     public void test(){
         ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
         UserService userService = context.getBean("userService", UserService.class);
         userService.test();
    }
 
 }

5.2、使用java为配置类测试

 public class UserTest {
 
     @Test
     public void test(){
         ApplicationContext context = new AnnotationConfigApplicationContext(ApplicationConfig.class);
         UserService userService = context.getBean("userService", UserService.class);
         userService.test();
    }
 
 }

6.结果:

image-20200806172141820

posted @ 2020-08-08 14:53  沉默非沉默  阅读(229)  评论(0)    收藏  举报