Spring动态代理

1. 静态代理设计模式

1.1 为什么需要静态代理设计模式

  1. 在JavaEE分层开发中,哪个层次对于我们来讲最重要?

    • Service层
  2. Service层中包含了哪些代码?

    • **核心功能:**业务运算、DAO调用

    • **额外功能:**事务、日志、性能…

      额外功能指的是:不属于业务、可有可无、代码量很小的那部分代码
      
  3. 额外功能写在Service层中好不好?

    • 从Service层调用者的角度(Controller):需要在Service中写额外功能
    • 从软件设计者的角度:Service层中不需要额外功能
  4. 以卖房为案例,房东向房屋中介代理自己的房子,自己只负责签合同和收钱(核心功能),其他的带看房屋、宣传(额外功能),引入房屋中介这个代理类。

    在这里插入图片描述

2. 代理设计模式

2.1 概念

  • 通过代理类,为原始类(目标)增加额外的功能
  • 好处:有利于原始类(目标)的维护

2.2 名词解释

  • **目标类(原始类):**指的是业务类(核心功能—>业务运算、DAO调用)
  • **目标方法(原始方法):指的是目标类(原始类)**中的方法
  • **额外功能(附加功能):**日志、事务、性能

2.3 代理开发的核心要素

代理类 = 目标类(原始类) + 额外功能 + 目标类(原始类)实现相同的接口

//房东(目标类)
public interface UserService{
    m1();
    m2();
}
//实现目标类中的方法(业务运算、DAO调用)
public class UserServiceIMpl implements UserService{
    m1();
    m2();
}
//房屋中介(代理类),实现与目标类相同的接口
public class UserServiceProxy implements UserService{
    m1();
    m2();
}

2.4 静态代理编码

**静态代理:**为每一个原始类,手工编写一个代理类

public interface UserService {
    public void register(User user);
    public User login(String username, String password);
}
public class UserServiceImpl implements UserService {

    @Override
    public void register(User user) {
        System.out.println("UserServiceImpl register");
    }

    @Override
    public User login(String username, String password) {
        System.out.println("UserServiceImpl login");
        return null;
    }
}
public class UserServiceProxy implements UserService {
    private UserService userService = new UserServiceImpl();

    @Override
    public void register(User user) {
        System.out.println("---log---");
        userService.register(user);

    }
    @Override
    public User login(String username, String password) {
        System.out.println("---log---");
        return userService.login(username, password);
    }
}

调用者调用的就是代理类UserServiceProxy,既有核心功能,又增加了额外功能,并且原来的UserServiceImpl并没有发生变化。

2.5 静态代理存在的问题

  1. 每一个原始类,都需要一个代理类,导致静态代理文件数量过多,不利于项目管理。

    UserServiceImpl --> UserServiceProxy
    OrderServiceIMpl --> OrderServiceProxy
    
  2. 代理类中的额外功能修改复杂,维护性差。

2. Spring的动态代理开发

概念:通过代理类为原始类(目标类)增加额外功能。

好处:有利于原始类(目标类)的维护。

2.1 搭建开发环境

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.1.14.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.3</version>
</dependency>

2.2 Spring动态代理的开发步骤

  1. 创建原始对象(目标对象)

    public class UserServiceImpl implements UserService {
    
        @Override
        public void register(User user) {
            System.out.println("UserServiceImpl register");
        }
    
        @Override
        public User login(String username, String password) {
            System.out.println("UserServiceImpl login");
            return null;
        }
    }
    
    <bean id="userService" class="edu.hzb.proxy.SpringDramaticProxy.UserServiceImpl"/>
    
  2. 实现额外功能MethodBeforeAdvice接口

    public class Before implements MethodBeforeAdvice {
        /**
        	把需要在原始方法执行之前的额外功能,卸载before接口中
        */
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("---method before advice log---");
        }
    }
    
    <bean id="before" class="edu.hzb.proxy.SpringDramaticProxy.Before"/>
    
  3. 定义切入点

    <aop:config>
        <!-- 在所有的方法中都添加切入点,都加入额外功能-->
            <aop:pointcut id="pc" expression="execution(* *(..))"/>
         </aop:config>
    

    切入点:额外功能在方法中加入的位置

    由程序员根据自己的需要,决定将额外功能加入给哪个原始方法

  4. 2、3整合,将额外功能切入到方法中

    <aop:config>
            <!-- 在所有的方法中都添加切入点,都加入额外功能-->
            <aop:pointcut id="pc" expression="execution(* *(..))"/>
            <aop:advisor advice-ref="before" pointcut-ref="pc"/>
         </aop:config>
    
  5. 调用

    目的:获得Spring工厂创建的动态代理对象,并且进行调用

    注意:

    • Spring工厂通过原始对象的id值获得的是代理对象,而不是原始对象
    • 获得代理对象后,可以通过声明接口类型,进行对象的存储
    @Test
        public void test2(){
            ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
            edu.hzb.proxy.SpringDramaticProxy.UserService userService = (edu.hzb.proxy.SpringDramaticProxy.UserService) context.getBean("userService");
            //虽然没有创建代理类,但是此时userService的运行类型确实是代理类类型:class com.sun.proxy.$Proxy12
            System.out.println(userService.getClass());
            userService.register(new User());
            userService.login("zhangsan", "zhangsan");
        }
    

    2.3 动态代理细节分析

    1. Spring创建的动态代理类在哪里?

      Spring框架在运行时,通过动态字节码技术,在JVM中创建的,运行在JVM内部,等程序结束后,会和JVM一起消失。
      
    2. 什么叫动态字节码技术?

      通过第三方动态字节码框架,在JVM中创建对应类的字节码,进而创建对象,当虚拟机结束后,动态字节码也会跟着消失。
      
    3. 结论:

      1. 动态代理不需要定义类文件,都是JVM运行过程中创建的,所以不会造成静态代理出现类文件数量过多,影响项目管理的问题
      2. 动态代理编程简化代理的开发。在额外功能不改变的前提下,创建其他目标类(原始类)对象时,只需要指定原始(目标)对象即可
      在这里插入图片描述
      1. 动态代理额外功能的维护性大大提高,当额外功能发生变化时,不用修改代码,重新实现MethodBeforeAdvice接口即可,遵循"关闭修改,打开扩展"。

3. Spring动态代理详解

3.1 额外功能详解

MethodBeforeAdvice分析

MethodBeforeAdvice接口作用:额外功能运行在原始方法执行之前,进行额外功能操作。

public class Before implements MethodBeforeAdvice {
    /**
     *作用:将运行在原始方法执行之前需要运行的额外功能,书写在Before方法中
     *参数:
     * Method:额外功能需要添加的原始方法
     *         login
     *         register
     * Object[] : 额外功能增加给的那个原始方法的参数
     *         (String, String)
     *         (User)
     * Object:额外功能增加的那个原始对象
     *         UserServiceImpl
     *
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("---new method before advice log---");
    }
}

Before方法在实战中,会根据需要进行使用,不一定会用到,也可能都用不到(大概率不会用)

MethodInterceptor方法拦截器

MethodInterceptor接口作用:额外功能可以根据需要运行在原始方法执行 前后

public class Around implements MethodInterceptor {
    /**
    *作用:额外功能卸载invoke方法中
    *参数:MethodInvocation(等价于Method):额外功能所增加给的那个原始方法
    *原始方法的执行:methodInvocation.proceed();
    *返回值:原始方法的返回值
    */
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        Object ret = methodInvocation.proceed();
        return ret;
    }
}
  1. 额外功能运行在原始方法执行之

    public class Around implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            System.out.println("额外功能执行在原始方法之前");
            //这一行是执行原始方法,不写就不会执行原始方法
            Object ret = methodInvocation.proceed();
            return ret;
        }
    }
    
  2. 额外功能运行在原始方法执行之

    public class Around implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            //这一行是执行原始方法,不写就不会执行原始方法
            Object ret = methodInvocation.proceed();
            System.out.println("额外功能执行在原始方法之后");
            return ret;
        }
    }
    
  3. 额外功能运行在原始方法执行前、后

    public class Around implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            System.out.println("额外功能执行在原始方法之前");
            //这一行是执行原始方法,不写就不会执行原始方法
            Object ret = methodInvocation.proceed();
            System.out.println("额外功能执行在原始方法之后");
            return ret;
        }
    }
    
  4. 额外功能运行在原始方法抛出异常的时候

    public class Around implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            Object ret = null;
            try {
                ret = methodInvocation.proceed();
            } catch (Throwable e) {
                System.out.println("额外功能运行在原始方法抛出异常的时候");
                e.printStackTrace();
            }
            return ret;
        }
    }
    
  5. MethodInterceptor接口的invoke可以影响原始方法的返回值

    public class Around implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
            Object ret = methodInvocation.proceed();
            //return ret;原始方法的返回结果是ret,但是我们可以认为影响方法的返回值,让它不返回ret
            return false;
        }
    }
    

3.2 切入点详解

切入点决定了额外功能加入的位置(主要是方法中)

<!-- 定义一个切入点pc,execution(* *(..))表示匹配所有的方法-->
<aop:pointcut id="pc" expression="execution(* *(..))"/>
  1. exexution():切入点函数
  2. * *(..):切入点表达式
3.2.1 切入点表达式
  1. 方法切入点

    **方法切入点:**指定方法加上额外功能

    语法:

    方法定义和切入点表达式定义类似:
    public void add(int i, int j);
          *      *(..)
     
    * *(..) --> 表示所有方法
    * 	--> 匹配方法的修饰符和返回值,比如(public void)
    * 	--> 方法名,比如add
    () 	--> 参数列表
    .. 	--> 匹配所有参数,对参数没有要求(是否有参数,参数个数,参数类型都没有要求)
    
    
    <!-- 定义login方法为切入点-->
    <aop:pointcut id="pc" expression="exexution(* login(..))"/>
    
    <!-- 定义register方法为切入点-->
    <aop:pointcut id="pc" expression="exexution(* register(..))"/>
    
    <!-- 定义login方法并且login方法有两个字符串类型的参数作为切入点-->
    <aop:pointcut id="pc" expression="execution(* login(String, String))"/>
    
    <!-- 定义非java.lang包下面的类型为方法参数,必须要写全限定类型-->
    <aop:pointcut id="pc" expression="execution(* register(com.hzb.proxy.User))"/>
    
    <!-- 精准定义某个方法作为切入点-->
    <aop:pointcut id="pc" expression="exexution(* com.edu.hzb.proxy.UserServiceImpl.login(String, String))"/>
    

    总结:

    • 非java.lang包下面的类型为方法参数,必须要写全限定类型
    • 精准定义某个方法,需要写到包名.类名.方法名
  2. 类切入点

    **类切入点:**指定特定类作为切入点(额外功能加入的位置),这个类中的所有方法,都会加上对应的额外功能

    语法:

    <!-- 类中的所有方法都加入额外功能-->
    <aop:pointcut id="pc" expression="executiion(* com.edu.hzb.proxy.UserServiceImpl.*(..))"/>
    
    <!-- 忽略包-->
    <!-- 类只存在一级包,例如com.UserServiceImpl,只有com是包名-->
    <aop:pointcut id="pc" expression="execution(* *.UserServiceImpl.*(..))"/>
    
    <!-- 类存在多级包,例如com.edu.hzb.proxy.UserServiceImpl,com.edu.hzb.proxy是包名-->
    <aop:pointcut id="pc" expression="exexution(* *..UserServiceImpl.*(..))"/>
    
  3. 包切入点(实战较多)

    **包切入点:**指定包作为额外功能加入的位置,包中的所有类及其方法都会加入额外的功能

    语法:

    <!-- 切入点是包中的所有类,必须在proxy中,不能在proxy包的子包中-->
    <aop:pointcut id="pc" expression="execution(* com.edu.hzb.proxy.*.*(..))"/>
    
    <!-- 切入点当前包及其子包都生效-->
    <aop:pointcut id="pc" expression="execution(* com.edu.hzb.proxy..*.*(..))"/>
    
3.2.2切入点函数
切入点函数类型

切入点函数:用于执行切入点表达式,包含以下几种:

  1. execution

    execution:最为重要的切入点函数,功能最全,可以执行方法切入点表达式、类切入点表达式、包切入点表达式

    弊端:execution执行切入点表达式,书写麻烦

    **注意:**其他的切入点函数,是为了简化execution的书写复杂度,功能上完全一致

  2. args

    args:主要用于方法参数的匹配

    <!-- 方法参数必须是两个字符串类型的参数-->
    <!--使用execution函数-->
    <aop:pointcut id="pc" expression="execution(* *(String,String))"/>
    
    <!--使用args函数-->
    <aop:pointcut id="pc" expression="args(String,String)"/>
    
  3. within

    within:主要用于进行类、包切入点表达式匹配

    <!--切入点为UserServiceImpl这个类-->
    <!-- 使用execution函数-->
    <aop:pointcut id="pc" expression="execution(* *..UserServiceImpl.*)"/>
    <!--使用within函数-->
    <aop:pointcut id="pc"  expression="with(*..UserServiceImpl)"
    
    <!-- 切入点为com.edu.hzb.proxy包-->
    <!--使用execution函数-->
    <aop:pointcut id="pc" expression="execution(* com.edu.hzb.proxy..*.*(..))"/>
    <!--使用within函数-->
    <aop:pointcut id="pc" expression="within(com.edu.hzb.proxy..*)"/>
    
  4. @annotation

    @annotation:为具有指定注解的方法加上额外功能

    //定义一个注解Log
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Log {
    }
    

    使用了Log注解的方法加上额外功能

    <aop:pointcut id="pc" expression="@annotation(edu.hzb.proxy.Log)"/>
    
切入点函数的逻辑运算

切入点的逻辑运算指的是整合多个切入点函数一起配合工作,进而完成更为复杂的需求。

  1. and与操作

    <!-- 切入点为login方法,并且参数为两个字符串类型-->
    
    <!-- 只使用execution函数-->
    <aop:pointcut id="pc" expression="execution(* login(String,String))"/>
    
    <!-- execution和args联合使用-->
    <aop:pointcut id="pc" expression="execution(* login(..)) and args(String,String)"/>
    

    **注意:**与操作不能用于同种类型的切入点函数

    <!-- 不存在名称同时为register和login的方法,这种写法是错误的-->
    <aop:pointcut id="pc" expression="execution(* login(..)) and execution(* register(..))"/>
    
  2. or或操作

    <!-- 切入点是register方法和login方法-->
    <aop:pointcut id="pc" expression="execution(* login(..)) or execution(* register(..))"/>
    

    注意:或操作是可以用于不同类型的切入点函数

学习哔哩哔哩《孙哥讲Spring5》视频所做的笔记

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值