目录
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个字符串
-
execution(* login(String,String))
-
execution(* login(…)) and args(String,String)
注意:与操作不能用于同种类型的切入点函数
-
-
or操作
案例:register方法 或者 login方法都可以作为切入点
execution(* login(…)) or execution(* register(…))