SSM-Spring(三)-代理、AOP

代理简介

  1. 业务层(Service)的主要内容
    1. 业务代码:业务运算、dao操作等(必要)
      1. 最终的结果需要通过几个数据库查询然后计算得出
    2. 附加代码:事务、日志、性能监控、异常处理等(可选)
      1. 查询几个数据库必须有条件,同时成功、失败等
      2. 计算结果完成还需要将结果记录日志
      3. 对整段业务代码进行trycatch

    图1

  2. 存在问题:在业务层加入附加代码会显得很臃肿、累赘,但很多时候又不得不加
  3. 代理
    1. 不修改目标类的目标方法代码的前提下,为目标方法增加额外功能
    2. 代理的必要条件: 代理类中必须也有同样的目标方法
      1. 代理类实现跟目标类同样的接口
      2. 若目标类没有实现(implements)接口,代理类继承目标类

    图1

  4. 代理的实现方案
    1. 静态代理(Static Proxy)
      1. 开发人员手动编写代理类(创建对应的*.java文件)
      2. 基本上,一个目标类就要编写一个代理类
    2. 动态代理(Dynamic Proxy)::程序运行过程动态生成代理类的字节码
  5. 使用代理的好处:业务层(service)只需要关注返回给客户端数据的封装即可,其他有代理类实现。

静态代理

  1. 业务层service
    1. UserService

       //接口
       public interface UserService {
           boolean login(String username, String password);
       }
       //实现类
       public class UserServiceImpl implements UserService{
           //业务方法
           @Override
           public boolean login(String username, String password) {
               System.out.println("09 - UserServiceImpl - login - " + username + "_" + password);
               return false;
           }
       }
      
    2. SkillService

       //只有目标实现类,没有接口
       public class SkillService {
           public boolean save(Object skill) {
               System.out.println("SkillServiceImpl - save");
               return false;
           }
       }
      
  2. 手动实现对应业务层类的代理类
    1. UserServiceProxy

       //代理类实现跟目标类同样的接口
       public class UserServiceProxy implements UserService {
           private UserService target;
              
           public void setTarget(UserService target) {
               this.target = target;
           }
              
           @Override
           public boolean login(String username, String password) {
               System.out.println("日志------------------1");
              
               boolean result = target.login(username, password);
              
               System.out.println("日志------------------2");
               return result;
           }
       }
      
    2. SkillServiceProxy

       //若目标类没有实现接口,代理类继承目标类
       public class SkillServiceProxy extends SkillService {
           private SkillService target;
              
           public void setTarget(SkillService target) {
               this.target = target;
           }
              
           @Override
           public boolean save(Object skill) {
               System.out.println("SkillServiceProxy - 1");
               boolean result = target.save(skill);
               System.out.println("SkillServiceProxy - 2");
               return result;
           }
       }
      
  3. applicationContext.xml中注册类

     <!--代理对象1-->
     <bean id="userService" class="com.zh.proxy.UserServiceProxy">
         <!--设置目标类(委托方) -->
         <property name="target">
             <bean class="com.zh.service.impl.UserServiceImpl"/>
         </property>
     </bean>
     <!--代理对象2-->
     <bean id="skillService" class="com.zh.proxy.SkillServiceProxy">
         <!--设置目标类(委托方) -->
         <property name="target">
             <bean class="com.zh.service.SkillService"/>
         </property>
     </bean>
    
  4. UserTest

     @Test
     public void test() {
         // 创建容器
         ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
         //此时拿到的是代理对象UserServiceProxy,不是UserServiceImpl对象
         UserService userService = ctx.getBean("userService", UserService.class);
         service.login("123", "456");
         SkillService skillService = ctx.getBean("skillService", SkillService.class);
         service.save(null);
    
         // 关闭容器
         ctx.close();
     }
    

动态代理(Dynamic Proxy)

  1. 动态代理的常见实现方案有2种
    1. JDK自带的API
      1. 本质:代理类实现跟目标类一样的接口
    2. 开源项目GGLib(Code Generation Library)
      1. 本质:代理类继承目标类
      2. Spring已经集成了GGLib

动态代理-JDK

  1. 本质就是通过JDK自带的API动态的实现一个对象,这个对象与目标类有相同的实现方法(通过与目标类实现相同的接口
  2. 目标类,service业务层

     //接口
     public interface UserService {
         boolean login(String username, String password);
     }
     //实现
     public class UserServiceImpl implements UserService {
         @Override
         public boolean login(String username, String password) {
             System.out.println("UserServiceImpl - login");
             return false;
         }
     }
    
  3. applicationContext.xml中注册类

     <bean id="userService" class="com.zh.service.impl.UserServiceImpl"/>
    
  4. UserTest

     @Test
     public void test() {
         // 创建容器
         ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    
         // 目标对象
         UserServiceImpl target = ctx.getBean("userService", UserServiceImpl.class);
    
         // 代理对象
         //JDK的API,通过传递类加载器、需要实现的接口、附加block,动态生成一个对象
         UserService userService = (UserService) Proxy.newProxyInstance(
                 getClass().getClassLoader(), // 类加载器
                 target.getClass().getInterfaces(), // 代理类需要实现的接口(目标类的接口)
                 (Object proxy, Method method, Object[] args) -> { // 附加代码(代理类的具体实现)
                     // proxy:代理对象
                     // method:目标方法
                     // args:目标方法的参数
    
                     System.out.println("proxy - 1");
    
                     // 调用目标对象的目标方法(核心业务代码)
                     Object result = method.invoke(target, args);
    
                     System.out.println("proxy - 2");
    
                     return result;
                 });
    
         //通过代理对象调用目标方法,会调用它的block代码
         userService.login("123", "456");
    
         // 关闭容器
         ctx.close();
     }
    
  5. 分析:
    1. 上面的方法实现,如果有n个类,那么动态的实现需要写n段相同的代码
    2. 如果n个类要同时实现,比如service层的类都要实现呢?
    3. 此时可以用上节讲的生命周期方法拦截所有的对象,一次性处理
    4. 经过排查可以使用postProcessAfterInitialization方法中进行拦截

统一封装设置动态代理

  1. 创建LogProcessor类

     //com.zh.processor.LogProcessor
     /**
      * 会拦截每一个bean的生命周期
      * 监听初始化操作,每个bean对象只会执行一次
      */
     public class LogProcessor implements BeanPostProcessor {
         //初始化完毕,然后生成代理
         @Override
         public Object postProcessAfterInitialization(Object target, String beanName) throws BeansException {
             return Proxy.newProxyInstance(
                     getClass().getClassLoader(), // 类加载器
                     target.getClass().getInterfaces(), // 代理类需要实现的接口(目标类的接口)
                     new LogInvocationHandler(target)); // 附加代码;
         }
        
        
         //封装一个类
         private static class LogInvocationHandler implements InvocationHandler {
             private final Object target;
             public LogInvocationHandler(Object target) {
                 this.target = target;
             }
             @Override
             public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        
                 System.out.println("proxy - 1");
        
                 // 调用目标对象的目标方法(核心业务代码)
                 Object result = method.invoke(target, args);
        
                 System.out.println("proxy - 2");
        
                 return result;
             }
         }
     }
    
  2. applicationContext.xml中注册

     <bean class="com.zh.processor.LogProcessor"/>
    
  3. UserTest

     @Test
     public void test3() {
         // 创建容器
         ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    
         //此时获取的就是代理对象userService
         UserService userService = ctx.getBean("userService", UserService.class);
         userService.login(null, null);
     }
    

动态代理-GGLib

  1. 本质是使用Spring内部集成GGLib的API来动态的实现一个对象,这个对象与目标类有相同的实现方法(通过继承目标类来实现
  2. 目标类,service业务层

     //只有实现,没有接口
     public class SkillService {
         public boolean save(Object skill) {
             System.out.println("SkillService - save");
             return false;
         }
     }
    
  3. 统一封装拦截所有对象创建,动态实现代理

     public class LogProcessor2 implements BeanPostProcessor {
         @Override
         public Object postProcessAfterInitialization(Object target, String beanName) throws BeansException {
             //过滤哪些对象需要设置代理
             //if (beanName.equals("person")) return target;
             if (!beanName.endsWith("Service")) return target;
        
             Enhancer enhancer = new Enhancer();
             // 可以省略,内部自动获取类加载器
             // enhancer.setClassLoader(getClass().getClassLoader());
             //代理类继承目标类
             enhancer.setSuperclass(target.getClass());
             //附加代码
             enhancer.setCallback(new LogMethodInterceptor(target));
             //返回代理
             return enhancer.create();
         }
        
         //MethodInterceptor方法拦截器接口,callback的参数
         private static class LogMethodInterceptor implements MethodInterceptor {
             private final Object target;
             public LogMethodInterceptor(Object target) {
                 this.target = target;
             }
             @Override
             public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        
                 String name = method.getName();
                 // method.getReturnType();
                 // method.getParameterTypes();
                 System.out.println("1-------------------");
                 Object result = method.invoke(target, args);
                 System.out.println("2-------------------");
                 return result;
             }
         }
     }
    
  4. applicationContext.xml中注册

     <bean id="skillService" class="com.zh.service.SkillService"/>
     <bean class="com.zh.processor.LogProcessor2"/>
    
  5. UserTest

     // 创建容器
     ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
     SkillService skillService = ctx.getBean("skillService", SkillService.class);
     skillService.save(null);
     UserService userService = ctx.getBean("userService", UserService.class);
     userService.login(null, null);
    

缺点分析

  1. 某些类不需要代理,需要进行拦截设置
  2. 不同的类有不同的附加代码,需要不同设置
  3. 所有的拦截都放在LogProcessor,代码极度冗余
  4. 解决办法—AOP:可以实现简单控制,哪些类需要实现代理,哪些方法需要实现附加方法

AOP(Aspect Oriented Programming)

  1. AOP又叫面向切面编程
  2. Spring使用AOP技术封装了动态代理的功能,使用起来非常简单
  3. 它依赖于AspectJ库

     <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjrt</artifactId>
         <version>1.9.6</version>
     </dependency>
     <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjweaver</artifactId>
         <version>1.9.6</version>
     </dependency>
    
  4. 附加代码可以写在下面两个类中
    1. MethodBeforeAdvice
    2. MethodInterceptor

MethodBeforeAdvice

  1. 新建一个类实现MethodBeforeAdvice接口,用来编写额外功能(会在目标方法执行之前执行)

     //com.zh.aop.LogAdvice
     public class LogAdvice implements MethodBeforeAdvice {
         @Override
         //只能在目标方法的前面调用
         public void before(Method method, Object[] args, Object target) throws Throwable {
             System.out.println("LogAdvice - before ------------------");
         }
     }
    
  2. applicationContext.xml中注册

     <bean id="userService"
           class="com.zh.service.impl.UserServiceImpl"/>
     <bean id="personService"
           class="com.zh.service.impl.PersonServiceImpl"/>
     <bean id="skillService"
           class="com.zh.service.SkillService"/>
     <!-- 附加代码-->
     <bean id="logAdvice" class="com.zh.aop.LogAdvice"/>
        
     <!-- 切面 -->
     <aop:config>
         <!-- 切入点:给哪些类的哪些方法增加附加代码? -->
         <!-- execution(* *(..))代表所有类的所有方法都会被切入 -->
         <aop:pointcut id="pc" expression="execution(* *(..))"/>
         <!-- 附加代码设置给指定的切入点 -->
         <aop:advisor advice-ref="logAdvice" pointcut-ref="pc"/>
     </aop:config>
    
  3. UserTest

     public void test() {
         // 创建容器
         ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
    
         SkillService skillService = ctx.getBean("skillService", SkillService.class);
         skillService.save(null);
    
         UserService userService = ctx.getBean("userService", UserService.class);
         userService.login(null, null);
         userService.register(null);
    
         PersonService personService = ctx.getBean("personService", PersonService.class);
         personService.run();
     }
    
  4. 打印:

     LogAdvice - before ------------------
     SkillService - save
     LogAdvice - before ------------------
     UserServiceImpl - login
     LogAdvice - before ------------------
     UserServiceImpl - register
     LogAdvice - before ------------------
     PersonServiceImpl - run
    

MethodInterceptor

  1. 新建一个类实现org.aopalliance.intercept.MethodInterceptor接口,用来编写额外功能(可以灵活设置,在目标代码前面还是后面插入)

     //com.zh.aop.LogInterceptor
     public class LogInterceptor implements MethodInterceptor {
         @Override
         public Object invoke(MethodInvocation invocation) throws Throwable {
             System.out.println("1 ----------------");
        
             // 调用目标对象的目标方法
             Object result = invocation.proceed();
        
             System.out.println("2 ----------------");
             return result;
         }
     }
    
  2. applicationContext.xml中附加代码修改为

     <bean id="logInterceptor" class="com.zh.aop.LogInterceptor"/>
     <!-- 切面 -->
     <aop:config>
         <aop:pointcut id="pc" expression="execution(* *(..))"/>
         <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/>
     </aop:config>
    
  3. 其余不变,打印如下

     1 ----------------
     SkillService - save
     2 ----------------
     1 ----------------
     UserServiceImpl - login
     2 ----------------
     1 ----------------
     UserServiceImpl - register
     2 ----------------
     1 ----------------
     PersonServiceImpl - run
     2 ----------------
    

动态代理的底层实现

  1. Spring动态代理的底层实现
    1. 如果目标类有实现接口,使用JDK实现
    2. 如果目标类没有实现接口,使用CGLib实现
    3. 在BeanPostProcessor的postProcessAfterInitialization方法中创建代理对象
      1. 参考AbstractAutowireCapableBeanFactory的applyBeanPostProcessorsBeforeInitialization方法
  2. 可以通过proxy-target-class属性修改底层实现方案
    1. true:强制使用CGLib
    2. false:按照默认做法
     <aop:config proxy-target-class="true">
     </aop:config>
    
  3. 注意:使用JDK肯定比使用CGLib效率高

切入点表达式

  1. 在上面的示例中,切入点写的是如下

     <aop:pointcut id="pc" expression="execution(* *(..))"/>
    
  2. execution(* *(..))代表所有类的所有方法都会被切入

     第一个*: 权限修饰符 返回值
     第二个*: 包名、类名、方法名
     (..):  参数
     举例: expression=execution(public String com.zh.service.UserService.login(String,String)
    
  3. 那么我们如何表达某个类的某个方法需要切入呢?—-切入点表达式;参考资料:点击
  4. 切入点表达式格式:

     expression="切入点指示符(表达式内容)"
    
  5. 常见的切入点指示符(pointcut designators)有

     execution: 通用表达式
     args: 专门用来指定参数
     within: 专门用来指定包
     @annotation: 专门用来指定有某个注解的方法
    
  6. 还可以使用&&(and)、||(or)、! 组合切入点表达式
  7. 常见的切入点表达式

     任意公共方法:execution(public * *(..))
     名字以set开头的任意方法: execution(* set*(..))
     UserService接口定义的任意方法:execution(* com.zh.service.UserService.*(..))
     service包中定义的任意方法:execution(* com.mj service.*.*(..))
     service包中定义的任意方法(包括子包) :execution(* com.zh.service..*.*(..))
     包含2个String参数的任意方法:execution(* *(String,String))
     只有1个Serializable参数的任意方法:args(java.io.Serializable)
         等价于: execution(* *(java.io.Serializable))
     service包中的任意方法:within(com.zh.service.*)
     service包中的任意方法(包括子包) :within(com.zh.service..*)
     带有自定义注解 (Hehe) 的方法:@annotation(com.zh.aop.Hehe)
    
  8. 举例:自对应一个注解,然后可以使用@annotation,专门筛选
    1. 自定义注解Log: 右击文件夹->new->Java Class ->选择Annotation,输入类名即可

       //com.zh.annotaion.Log
       /*
       限制注解范围:TYPE只能注解在类前面。METHOD:只能注解方法前面,FIELD:只能注解成员变量
       {ElementType.METHOD,ElementType.TYPE}:两者皆可
        */
       @Target({ElementType.METHOD,ElementType.TYPE})
       //注解什么时间有效:运行时RUNTIME、编译时SOURCE等
       @Retention(RetentionPolicy.RUNTIME)
       public @interface Log {}
      
    2. 其他类使用

       public class SkillService {
           private int age;
           @Log
           public boolean save(Object skill) {
               System.out.println("SkillService - save");
               return false;
           }
       }
      
    3. 切入点表达式

       <!--所有带@Log注解方法的所有类,都切入-->
       <aop:pointcut id="pc" expression="@annotation(com.zh.annotaion.Log)"/>
      

AOP细节问题

  1. 默认情况下,目标方法相互调用时,总共只会被切入1次。解决方法:拿到代理对象去调用目标方法
    1. 举例

       //xml切入
       <bean id="logInterceptor" class="com.zh.aop.LogInterceptor"/>
       <aop:config >
           <aop:pointcut id="pc" expression="execution(* *(..))"/>
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/>
       </aop:config>
              
       //目标方法
       public class UserServiceImpl implements UserService {
           @Override
           public boolean login(String username, String password) {
               System.out.println("UserServiceImpl - login");
               register("123");
               return false;
           }
              
           @Override
           @Log
           public boolean register(String username) {
               System.out.println("UserServiceImpl - register");
               return false;
           }
       }
              
       //测试
       ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
       UserService userService = ctx.getBean("userService", UserService.class);
       userService.login(null, null);
      
      1. login内部调用register,正常情况下打印结果应该是

         1 ----------------
         UserServiceImpl - login
         1 ----------------
         UserServiceImpl - register
         2 ----------------
         2 ----------------
        
      2. 实际结果

         1 ----------------
         UserServiceImpl - login
         UserServiceImpl - register
         2 ----------------
        
      3. 从实际结果来看,register并没有被切入,原因如下:

        1. ctx.getBean()返回的serService是动态生成的UserServiceImpl代理对象
        2. userService.login()调用的是代理对象LogInterceptor的invoke方法
        3. invoke方法内部先调用1,然后目标对象调用目标方法login:Object result = invocation.proceed();
        4. 因为是目标对象调用login,所以login内部的register也是目标对象调用,而不是代理对象去调用register
        5. 综上原因才会得到上面打印的结果
    2. 如果需要register也能被切入,那么login方法内部应该也用代理对象去调用register,那么如何拿到代理对象呢?—使用生命周期方法

      1. UserServiceImpl实现ApplicationContextAware、BeanNameAware接口,并实现代理方法

         //设置类的成员变量
         private ApplicationContext ctx;
         private String beanName;
                    
         //目的是为了拿到容器,拿到代理对象
         @Override
         public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
             ctx = applicationContext;
         }
         //为了拿到beanname
         @Override
         public void setBeanName(String name) {
             beanName = name;
         }
        
      2. login内部用代理对象去调用register

         @Override
         public boolean login(String username, String password) {
             System.out.println("UserServiceImpl - login");
             //register("123");
             //拿到代理对象去调用register
             ctx.getBean(beanName, UserService.class).register(username, password);
             return false;
         }
        
      3. 打印结果

         1 ----------------
         UserServiceImpl - login
         1 ----------------
         UserServiceImpl - register
         2 ----------------
         2 ----------------
        
  2. 配置多个pointcut、advisor
    1. 当指定切入某几个类时,可以这么写

       //写法1:写在一起
       <aop:config>
           <aop:pointcut id="pc1" expression="within(com.zh.service.impl.UserServiceImpl)"/>
           <aop:pointcut id="pc2" expression="within(com.zh.service.SkillService)"/>
      
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/>
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/>
           <aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/>
       </aop:config>
              
       //写法2:每个类单独写
       <aop:config>
           <aop:pointcut id="pc1" expression="within(com.mj.service.impl.UserServiceImpl)"/>
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc1"/>
           <aop:advisor advice-ref="logAdvice" pointcut-ref="pc1"/>
       </aop:config>
      
       <aop:config>
           <aop:pointcut id="pc2" expression="within(com.mj.service.SkillService)"/>
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc2"/>
       </aop:config>
      
    2. 可以使用and、or、! 来实现几个条件同时满足

       <aop:config>
           <!--
           在com.zh.service.impl.UserServiceImpl类中切入,而且必须有2个参数的目标方法,才会被切入
            -->
           <aop:pointcut id="pc"
                         expression="within(com.zh.service.impl.UserServiceImpl) and args(String, String)"/>
           <aop:advisor advice-ref="logInterceptor" pointcut-ref="pc"/>
       </aop:config>
      

      总结

  3. AOP表面上*实现了在不修改原来类的基础上改变原来类的某些方法,本质是新生成了一个代理类(继承、接口实现)提供给使用者为原来的方法增加一些附加代码。
  4. AOP仅仅是给原来类的方法新增一些附加代码,不会给原来类新增方法、属性。
  5. 注意: AOP以及上面的代理方式仅仅适用于spring的IOC容器创建bean对象的方式,不适用于手动通过new或者其他方式创建目标对象的方式。因为本质都是基于spring容器bean的生命周期方法来实现的。
Table of Contents