Spring09:Spring代理模式+切入点表达式

1.静态代理设计模式

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

1.问题:

  • 在JavaEE分层开发中,哪个层次对我们来讲最重要
    DAO ---> Service ---> Controller
    
    JavaEE分层开发中,最为重要的是Service层。
    
  • Service层中包含了哪些代码?
    Service层中的代码主要包含两大类:核心功能 + 额外功能(附加功能)
    1.	核心功能:
    	业务运算
    	DAO调用
    2.	额外功能:比如事务,日志等,有如下特性
    	1.不属于核心业务
    	2.可有可无
    	3.代码量小。
    

在这里插入图片描述

  • 额外功能写在Service层中好不好?
    从Service层调用者的角度来看:需要在Service层书写额外功能,最起码要事务控制功能。
    从软件设计者的角度来看:额外功能是可有可无的,那么不需要时,又会删掉这些不需要的代码,又会设计源码的修改,不易于维护。所以Service层不需要额外的功能。
    
    那么这就产生了矛盾,这个矛盾我们要如何来解决呢?
    

2.解决:

让代理类代理Service层,来做这些额外功能,满足了额外功能的需要。并且调用原始类中的核心功能,也实现了核心业务。

额外功能相较于核心业务,客户需求是可有可无的,所以后期可能会经常修改,统一放到代理中便于维护,避免重复代码臃肿。如果后期我不满这个代理,也可以再创建新的代理,容易修改,利于维护。

目的是:核心代码和额外代码分离(设计层面要求剥离额外的功能只留下核心部分,还要抽取这些公共的功能部分便于维护)。

在这里插入图片描述

1.2.静态代理设计模式

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

2.好处:利于原始类(目标类)的维护。日后不会再因为额外功能的变化,而影响到原始类中的代码。

3.名词解释

1.	目标类 原始类
	指的是被代理,被增强的类

4.代理开发的核心要素

  • 代理类:同原始类实现的相同接口+ 额外功能 + 目标类(用来调用原方法)

5.为什么需要和目标类实现相同的接口:

个人的理解:多态实现迷惑调用者(感觉还是原来的对象)

  • 目的是:核心代码和额外代码分离(设计层面要求剥离额外的功能只留下核心部分,还要抽取这些公共的功能部分便于维护)

  • 我们不想动源码,但是又要在原功能上实现一些额外的功能,所以还是要使用原功能。怎么使用原来的方法:通过目标对象调用。

  • 你可能会想:虽然我想要使用原功能,但我可以不实现相同的接口,直接在代理类中再将对应的方法自己写一遍,调用一下不就行了吗?不也能达到既有原来的功能,也有额外的功能吗?但是此时,原来的方法还有什么用呢?原来Service中的核心代码不就没用了吗?原来的Service被弃用了,而且此时额外功能和核心功能一起转移到代理类中,这样从代码设计层面来说岂不是更差了。我们想要的是核心代码和额外代码分离,但是又要用到原来的方法:要有个属性是原目标对象。所以不能再实现一遍,而是要着眼于额外功能。

  • 代理对象是暴露给外界的,起到代替目标对象的作用。那么调用者用这个代理对象的目的,就是要用目标对象中原有的功能或方法,再加上代理类额外的功能。既然会用到目标对象中的原有方法,那么代理对象也要有同名方法,这样在外界的调用者来看,还是调用这个方法。怎么拥有同名的这些方法:实现相同的接口。

    //原来是目标对象调用方法
    obj.method();
    //现在要用被代理后的方法:但是在代码上,看起来调用的还是那个方法
    proxy.method(); //还是method(),但是是被代理后的。
    
  • 而且由于实现了同样的接口,那么代理对象也可以和接口形成多态,那么就可以用接口的引用来调用方法。原目标对象也是用接口的引用来调用方法,这样的代码看上去,用的还是原来的目标对象(迷惑调用者),更好的起到代替,增强目标对象的作用。

  • 而且我可以在代理类中提供对应方法新的实现:加入新的功能,并且调用原始方法。

  • 使用代理对象不会对原来的代码进行修改,就可以增加原来的功能。

1.3.静态代理编码

在这里插入图片描述
静态:这些代理类必须程序员自己写出来,有代理的源文件UserServiceProxy

1.4.静态代理缺点

1.缺点:

  • 1.有一个原始类要想被代理,就得有一个代理类。.静态代理类文件数量过多,不利于项目管理。每代理一个类,都要有一个静态代理。

  • 2.如果每个类都要加一个日志打印的功能,那么为每个类都要写一个静态代理。注意这里都是相同的日志打印功能,却要为每个类都要实现一个代理类,复用性很差。

  • 3.每个代理类只维系了一个特定的目标对象的引用,所以只能代理这一个目标对象(主要是因为:还需要调用原方法,但是由于只有一个原对象,所以只能调用这一个对象的原方法)。

  • 4.额外功能维护性差:如果后期需要对额外功能进行修改,那么每个代理类都要修改,维护非常麻烦。

2.本质:

  • 代理其实代理的是方法:代理对象因为继承了同样的接口,在需要调用原方法时,只能调用接口中定义的方法,为这些方法代理。

2.Spring动态代理

2.1开发步骤

1.引入jar包

        <!--Spring动态代理-->
        <!--解析切入点表达式-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
        <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.6</version>
        </dependency>

2.创建原始对象

public class UserServiceImpl implements UserService {
    
    
    @Override
    public void register(User user) {
    
    
        System.out.println("UserServiceImpl.register:--->业务运算+DAO调用");
    }

    @Override
    public boolean login(String name, String password) {
    
    
        System.out.println("UserServiceImpl.login:--->业务运算+DAO调用");
        return true;
    }
}

<bean id="userService" class="com.txl.UserServiceImpl"></bean>

3.额外功能:实现MethodBeforeAdvice接口

Spring在进行额外功能开发的时候,为我们提供了一个接口:MethodBeforeAdvice。我们需要将额外的功能写在这个接口的实现类中。

这些额外功能,会在原始方法运行之前执行。

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

<bean id="before" class="com.txl.dynamticproxy.Before"></bean>

4.定义切入点

  • 切入点:额外功能加入的位置,比如UserServiceImpl中的每个方法都加入日志打印功能,那么切入点就是UserServiceImpl中的每个方法。

  • 目的:由程序员根据自己的需要,决定额外功能加到哪些目标方法前后。

  • 好处:更灵活,不是某个类的所有方法都加额外功能。有我们自己决定哪些需要加上额外功能。

  • 简单的测试:所有方法都作为切入点,加上额外功能。

    <aop:config:引入新的命名空间

        <aop:config>
            <aop:pointcut id="pc" expression="execution(* *(..))"/>
        </aop:config>
    

    其中id表示这个切入点pointcut的id,expression是切入点表达式,表示哪些方法作为切入点。

5.组装整合3,4步骤:将切入点和额外功能进行整合。

为所有方法,都加上第3步定义的额外功能。

    <!--目标类-->
    <bean id="userService" class="com.txl.UserServiceImpl"></bean>
    <!--提供额外功能的类-->
    <bean id="before" class="com.txl.dynamticproxy.Before"></bean>

    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <!--将切入点和额外功能进行整合:所有的方法都加上before中的额外功能-->
        <aop:advisor advice-ref="before" pointcut-ref="pc"/>
    </aop:config>

6.获得Spring工厂创建的动态代理对象,并进行调用。

  • 代理对象在哪呢?我们也没有配置代理对象的bean啊

  • 注意。

    1.Spring规定,Spring工厂通过原始对象id值获得的是代理对象。
    2.底层代理对象也是实现了相同的接口,所以用接口引用接收代理对线。

2.2.Spring动态代理的细节

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

回顾一下上面的Spring动态代理的开发步骤:我们没有创建代理类,那么代理对象是怎么创建的呢?

Spring框架在运行时,通过动态字节码技术,在JVM中创建动态代理类。运行在虚拟机中,等程序结束后动态代理类会和虚拟机一起消失。所以在我们编码的过程中,看不到这个动态代理类。

2.什么是动态字节码技术

实际上Java运行一个类:是通过JVM运行这个类的字节码,通过字节码创建对象。

JVM怎么获得字节码呢:我们定义一个类,首先要写源文件.java文件,然后对.java文件进行编译生成.class文件,这个文件就存着这个类的字节码。

后续虚拟机运行这个类的时候,要先经过一个类加载的过程:即将.class文件加载到虚拟机内存中。加载进来之后,JVM就可以根据这个字节码文件来创建对象了。

那么动态字节码技术,就是我们不用再写类的源文件了,那么也就没有字节码文件了。但是JVM还是需要字节码来创建对象,那么字节码哪来呢?动态字节码通过一些第三方框架来完成的:ASM,Javaassist,Cglib等,这些第三方框架可以直接在JVM中生成字节码,这些通过第三方框架直接生成的字节码我们就称为动态字节码。后续虚拟机就可以根据这个字节码生成对象了。

动态代理类就是通过动态字节码创建的,不需要我们来写源码,编译成.class文件。而是第三方库生成字节码交给虚拟机,虚拟机再直接创建对象,等程序结束后动态代理类会和虚拟机一起消失。

这样也解决了静态代理的代理类过多的问题。

3.动态代理会简化代理的开发:

在额外功能不改变的前提下,创建其他目标类的代理对象时,只需指定目标对象为切入点即可。

4.动态代理额外功能的维护性大大增强。

额外功能变化时,直接改变额外功能在进行配置文件配置即可。

2.3.MethodBeforeAdvice分析

在这里插入图片描述
1.参数分析:

public class Before implements MethodBeforeAdvice {
    
    
    /**
     * 这些额外功能,会在原始方法运行之前执行。
     * @param method:原始方法对象
     * @param objects:原始参数Object对象数组
     * @param object:原始对象引用
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
    
    
        System.out.println("-----method before advice log------");
    }
}

3.这三个参数如何使用

MethodBeforeAdvice接口中的before方法的参数很少会用到。

2.4.MethodInterceptor:方法拦截器

MethodBeforeAdcie:只能在原始方法之前执行
MethodInterceptor:更强大,更灵活。在原方法的之前之后都可以拦截,添加额外功能。

日常开发更倾向于MethodInterceptor

1.注意

  • 导包:
    在这里插入图片描述

  • 参数:MethodInvocation类似于原始方法(封装的更高级一些)。所以额外功能写在原始方法的前后,就决定了额外功能的执行时机。

  • 返回值:原始方法的返回值。如果返回值是void,那么接收的结果是null。注意返回值的匹配:invoke方法返回null时,只能匹配void类型的原始方法

2.开发步骤:

  • 1.原始对象

    public class UserServiceImpl implements UserService {
          
          
        @Override
        public void register(User user) {
          
          
            System.out.println("UserServiceImpl.register:--->业务运算+DAO调用");
        }
    
        @Override
        public boolean login(String name, String password) {
          
          
            System.out.println("UserServiceImpl.login:--->业务运算+DAO调用");
            return true;
        }
    }
    
  • 2.注册原始对象bean

    <!--目标类-->
    <bean id="userService" class="com.txl.UserServiceImpl"></bean>
    
  • 3.继承MethodInterceptor接口方法拦截器,实现额外功能的类

    public class Around implements MethodInterceptor {
          
          
        @Override
        public Object invoke(MethodInvocation methodInvocation) throws Throwable {
          
          
    
            if(methodInvocation.getMethod().getName() == "login"){
          
          
                System.out.println("before-login-proxy-log-------");
                boolean flag = (boolean)methodInvocation.proceed();
                System.out.println("after-----------");
                return flag;
            }
            Object obj = methodInvocation.proceed();
            return obj;//注意返回值的匹配:如果返回null只能匹配void类型的原始方法
        }
    }
    
  • 4.注册提供额外功能的类:MethodInterceptor

    <!--提供额外功能的类:MethodInterceptor-->
    <bean id="around" class="com.txl.dynamticproxy.Around" />
    
  • 5.整合提供额外功能类(方法拦截器)和切面(原始方法)

    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="pc" expression="execution(* *(..))"/>
        <!--将切入点和额外功能进行整合-->
        <aop:advisor advice-ref="around" pointcut-ref="pc"/>
    </aop:config>
    
  • 6.测试代码

    @org.junit.Test
    public void test2(){
          
          
        ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml");
        //Spring规定,Spring工厂通过原始对象id值获得的是代理对象。
        UserService userService = (UserService)context.getBean("userService");
        userService.register(new User());
        userService.login("txl", "123456");
    }
    

3.思考

  • 什么样的额外功能,需要在原始方法执行之前,之后都要执行呢?

    事务:开启事务–>原始方法执行—>结束事务。

  • 怎么样实现:额外功能在原始方法抛出异常时才执行呢?

    原方法要先将异常跑出来,在MethodInterceptor类中try-catch捕获,在catch块中执行额外功能。
    在这里插入图片描述

4.MethodInterceptor影响原始方法的返回值。

注意MethodInterceptor中的invoke方法的返回值会被原始方法接收,所以一定要注意类型匹配。

3.切入点详解

3.1切入点语法

在这里插入图片描述
<aop:pointcut id="pc" expression="execution(* *(..))"/>

*  *(..)  --> 所有方法

* ---> 修饰符 返回值
* ---> 方法名
()---> 参数表
..---> 对于参数没有要求 (参数有没有,参数有几个都行,参数是什么类型的都行)

注意:非java.lang包中的类型,必须要写全限定名
* register(com.baizhiedu.proxy.User)

..可以和具体的参数类型连用
* login(String,..)  --> login(String),login(String,String),login(String,com.baizhiedu.proxy.User)

1.精准的切入点

修饰符+返回值 包 						类 					方法(参数)
*						com.txl.example.UserServiceImpl.login(String,String)

注意:修饰符+返回值和包之间要有一个空格。
<aop:pointcut id="pc" expression="execution(* com.txl.example.UserServiceImpl.login(String,String))"/>

2.类切入点

指定特定类作为切入点,自然这个类中的所有方法都会加上对应的额外功能。

类中的所有方法加入了额外功能 
* com.baizhiedu.proxy.UserServiceImpl.*(..)  

#忽略包:
1. 	类只存在一级包 ,类似: com.UserServiceImpl。
	筛选出所有一级包下的UserServiceImpl类
* *.UserServiceImpl.*(..)

2.	类存在多级包    com.baizhiedu.proxy.UserServiceImpl
	筛选出所有多级包下的所有UserServiceImpl类
* *..UserServiceImpl.*(..)

3.包切入点表达式:更具实战价值

指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能。

#切入点包中的所有类,必须在proxy中。在proxy包的子包中的类不生效。
* com.baizhiedu.proxy.*.*(..)
* 
#切入点当前包及其子包中的类都生效 
* com.baizhiedu.proxy..*.*(..) 

3.2切入点函数

切入点函数:用于执行切入点表达式

1.execution:最为重要的切入点函数,功能最全。执行切入点表达式,类切入点表达式,包切入点表达式。

弊端:写起来较麻烦。

2.args切入点函数:

作用:主要用于函数(方法) 参数的匹配

切入点:方法参数必须得是2个字符串类型的参数

execution(* *(String,String))

args(String,String)

3.within

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

切入点:UserServiceImpl这个类

execution(* *..UserServiceImpl.*(..))

within(*..UserServiceImpl)

execution(* com.baizhiedu.proxy..*.*(..))

within(com.baizhiedu.proxy..*)

4.@annotation

作用:为具有特殊注解com.baizhiedu.Log的方法加入额外功能

<aop:pointcut id="" expression="@annotation(com.baizhiedu.Log)"/>

在这里插入图片描述
5.切入点函数的逻辑运算

  • and与操作

    案例:login方法名并且参数 2个字符串

    1. execution(* login(String,String))

    2. execution(* login(…)) and args(String,String)

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

  • or操作

    案例:register方法 或者 login方法都可以作为切入点

    execution(* login(…)) or execution(* register(…))

猜你喜欢

转载自blog.csdn.net/tttxxl/article/details/115373678