代理引入、Spring动态代理、切入点表达式、切入点函数

1.代理引入

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

在 JavaEE 分层开发开发中,有一个很重要的层次–Service层

其中包含了:

核心功能(代码量较多):业务运算,DAO 调用
额外功能(附加功能,不属于业务,可有可无,代码量小):事务、日志、性能 …

额外功能直接写在 Service 层好不好?

Service 层的调用者的角度(Controller):需要在 Service 层书写额外功能,比如日志、事务等。
软件设计者:Service 层不需要额外功能,不然看上去过于臃肿,不方便管理。

拿现实生活中的例子来做对比,解决方案是 引入一个代理。
在这里插入图片描述

1.2 代理设计模式

核心开发要素:代理类 = 目标类+额外功能+目标类实现相同接口

名词解释:

目标类 / 原始类:指的是 业务类 (核心功能 --> 业务运算、DAO调用)

目标方法 / 原始方法:目标类(原始类)中的方法就是目标方法(原始方法)

额外功能 / 附加功能:日志、事务、性能 …

静态代理编码
接口:

package zyc.stu.Spring5_62_88.Service;

public interface UserService {
    void register();
    boolean login();
}

实现:

package zyc.stu.Spring5_62_88.Service;

public class UserServiceImpl implements UserService{
    public void register() {
        System.out.println("UserServiceImpl.register");
    }

    public boolean login() {
        System.out.println("UserServiceImpl.login");
        return true;
    }
}

代理类:

package zyc.stu.Spring5_62_88.StaticProxy;

import zyc.stu.Spring5_62_88.Service.UserService;
import zyc.stu.Spring5_62_88.Service.UserServiceImpl;

public class UserServiceProxy implements UserService {
    private UserServiceImpl userService = new UserServiceImpl();

    public void register() {
        System.out.println("--log--");
        userService.register();
    }

    public boolean login() {
        System.out.println("--log--");
        return userService.login();
    }
}

测试:

    @Test
    public void test1(){
        UserService userService = new UserServiceProxy();
        userService.register();
        userService.login();
    }

1.3 静态代理存在的问题

  • 静态类文件数量过多,不利于项目管理
    UserServiceImpl、UserServiceProxy OrderServiceImpl、OrderServiceProxy
  • 额外功能维护性差:在代理类中修改额外功能较为麻烦

2.Spring动态代理

以下引自简书:Spring AOP增强(Advice)

2.1 五种增强接口

  • MthodBeforeAdvice:目标方法实施前增强

  • AfterReturningAdvice:目标方法实施后增强

  • ThrowsAdvice:异常抛出增强

  • IntroductionAdvice:引介增强,为目标类添加新的属性和方法。可以构建组合对象来实现多继承

  • MethodInterceptor:方法拦截器,环绕增强,在方法的前后实施操作

AfterAdvice,BeforeAdvice当前是作为标记使用,内部无接口方法,为后来扩展使用。
Interceptor接口也不是直接使用,同样作为标记类,可使用其子接口。

2.2 接口对应的注解

@Before
@AfterReturning
@AfterThrowing
@After //相当于try-catch-finally中的final,一般用于释放资源
@Around

2.3 开发环境搭建

在pom文件中添加新的依赖

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.2.8.RELEASE</version>
        </dependency>
        <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>

2.4 动态代理开发步骤(只测试两个接口)

  1. 创建原始对象并在bean中注册

  2. 额外功能:MethodBeforeAdvice接口:额外的功能书写在接口的实现中

     实现接口,并将类注册在xml文件中
    
  3. 定义切入点:定义额外功能加入的位置

      根据需要决定额外功能加入给哪个原始方法
    
  4. Spring配置文件中配置

     注册代理类
     配置:将切入点和额外功能整合--->切面
    
  5. 组装(2.3.4步)

  6. 调用

     获得Spring工厂创建的动态代理对象,并调用
    

注意:Spring工厂通过原始对象的id获得的就是代理对象


2.3.1 MethodBeforeAdvice测试: 还是使用上面的Service类和接口

package zyc.stu.Spring5_62_88.DynamicProxy;

import org.springframework.aop.MethodBeforeAdvice;

import java.lang.reflect.Method;

public class Before implements MethodBeforeAdvice {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("----methodBeforeAdvice----");
    }
}

<bean id="userService" class="zyc.stu.Spring5_62_88.Service.UserServiceImpl"/>
<bean id="before" class="zyc.stu.DynamicProxy.Before"/>

    @Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext2.xml");
        //Spring工厂通过原始对象的id获得的就是代理对象,通过接口类型进行存储
        UserService userService = (UserService) context.getBean("userService");
        userService.register();
    }

MethodBeforeAdvice详解:
额外功能在运行在原始方法之前,进行额外的操作
三个参数和jdk动态代理中的InvocationHandler中的invoke方法的参数类似

2.3.2 MethodInterceptor测试:

切入点先暂时不管,让整个包的类和方法都接入事务

package zyc.stu.Spring5_62_88.DynamicProxy;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class Around implements MethodInterceptor {
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("----额外功能before----");
        Object proceed = methodInvocation.proceed();
        System.out.println("----额外功能after----");
        return proceed;
    }
}
    <bean id="around" class="zyc.stu.Spring5_62_88.DynamicProxy.Around"/>
    
    <aop:config>
        <!--Spring5_62_88所有的类下所有的方法,都作为切入点,加入额外的功能-->
        <aop:pointcut id="pc" expression="execution(* zyc.stu.Spring5_62_88.Service.*.*(..))"/>
        <!--组装:目的:把切入点 与 额外功能 整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>

测试:

    @Test
    public void test3(){
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext2.xml");
        UserService userService = (UserService) context.getBean("userService");
        userService.login();
    }

MethodInterceptor详解:
实现MethodInterceptor接口并实现invoke方法:

将额外功能写在invoke方法中,可以运行在原始方法之前、之后、前后
  1. 怎么保证原始方法的运行呢?
    参数:methodInvocation:额外功能所增加给的那个原始方法:底层是对之前method方法的封装
    methodInvocation.proceed(): 原始方法运行
    返回值:Object:原始方法执行后的返回值
  2. 什么样的额外功能运行在原始方法的之前和之后呢?
    事务:
    原始方法抛出异常:
  3. 什么情况下MethodInterceptor会影响原始方法的返回值?
    invoke方法的返回值不返回原始方法的返回值

2.5动态代理细节分析:

  1. Spring创建的动态代理类在哪里?
    此类Spring框架运行时,通过动态字节码技术,在JVM创建的运行在JVM内部,等程序结束后会和JVM一起消失。

  2. 什么叫动态代理技术?
    通过三个动态字节码框架ASM、Javassist、Cglib,在虚拟机中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失。

  3. 有什么好处?
    动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理的问题,代理类文件过多。

3.切入点表达式

3.1 切入点表达式剖析

  1. 切入点决定额外功能加入的位置(方法)

    execution() 切入点函数,内部写切入点表达式
    切入点表达式剖析: * *(. .)
    括号内连续两个点表示对参数没有要求,一般也不做要求

      *             *        (..)         
      修饰符+返回值     方法名     ()参数表 
    
  2. 切入点表达式举例

    例:* register(. .)
    register(String,String)

    例:register(Person person) Person为用户自定义,非Java.lang包下的类型,必须写全限定名
    register(zyc.stu.bean.Person)

    例:* register(String,…) 限定第一个参数为String,其他的不做要求

3.2 方法切入点

将所有东西都指明 (修饰符返回值没必要)

	修饰符+返回值  		包.类.方法(参数)
		*         zyc.stu.Proxy.UserServiceImpl.login(..)

3.3 类切入点

例:类中所有方法加入
	
	*	zyc.stu.Proxy.UserServiceImpl.*(..)

例:某三层包下的UserServiceImpl类中的login方法	(一个 * 只能处理一层包)
	
	*	*.*.*.UserServiceImpl.login(..)

例:某一级包下面乃至所有多级子包中 	(*.. 能处理多层包)
   *     *..UserServiceImpl.login(..)

3.4 包切入点

只能切Proxy包下的类的所有方法,不能切Proxy的子包下的类
  *         zyc.stu.Proxy.*.*(..)
        
Proxy包以及子包
  *         zyc.stu.Proxy..*.*()

4.切入点函数

4.1 三大切入点函数+@annotation

  1. execution:主要用于方法名的匹配,最重要的切入点函数,功能最全
    优点:可执行三种表达式

    方法切入点、类切入点、包切入点 
    

    缺点:

    书写复杂 可用其它切入点函数简化,功能上完全一致
    
  2. args:主要用于函数参数的匹配 只关注()里面的内容
    execution(* *(String,String))
    args(String,String)

  3. within: 类 包切入点表达式的匹配
    execution(* *..UserServiceImpl.*(..))
    within(*..UserServiceImpl)

  4. @annotation(zyc.stu.annotation.Log) : 自定义特殊注解Log,为具有Log注解的方法加入额外功能

4.2 切入点函数的逻辑运算

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

  • and:与操作,不能用于同种切入点函数
    案例:login 两个String参数
    execution(* login(String,String))
    execution(* login(..)) and args(String,String)
  • or:或操作
    案例:需求是只将register和login方法作为切入点,同包中还有a b c方法,
    execution(* login(.. )) or execution(* register(..))

版权声明:本文为qq_36928041原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。