代理引入、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动态代理
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 动态代理开发步骤(只测试两个接口)
创建原始对象并在bean中注册
额外功能:MethodBeforeAdvice接口:额外的功能书写在接口的实现中
实现接口,并将类注册在xml文件中
定义切入点:定义额外功能加入的位置
根据需要决定额外功能加入给哪个原始方法
Spring配置文件中配置
注册代理类 配置:将切入点和额外功能整合--->切面
组装(2.3.4步)
调用
获得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方法中,可以运行在原始方法之前、之后、前后
- 怎么保证原始方法的运行呢?
参数:methodInvocation:额外功能所增加给的那个原始方法:底层是对之前method方法的封装
methodInvocation.proceed(): 原始方法运行
返回值:Object:原始方法执行后的返回值
- 什么样的额外功能运行在原始方法的之前和之后呢?
事务:
原始方法抛出异常:
- 什么情况下MethodInterceptor会影响原始方法的返回值?
invoke方法的返回值不返回原始方法的返回值
2.5动态代理细节分析:
Spring创建的动态代理类在哪里?
此类Spring框架运行时,通过动态字节码技术,在JVM创建的运行在JVM内部,等程序结束后会和JVM一起消失。
什么叫动态代理技术?
通过三个动态字节码框架ASM、Javassist、Cglib,在虚拟机中创建对应类的字节码,进而创建对象,当虚拟机结束,动态字节码跟着消失。
有什么好处?
动态代理不需要定义类文件,都是JVM运行过程中动态创建的,所以不会造成静态代理的问题,代理类文件过多。
3.切入点表达式
3.1 切入点表达式剖析
切入点决定额外功能加入的位置(方法)
execution() 切入点函数,内部写切入点表达式
切入点表达式剖析: * *(. .)
括号内连续两个点表示对参数没有要求,一般也不做要求* * (..) 修饰符+返回值 方法名 ()参数表
切入点表达式举例:
例:* 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
execution:主要用于方法名的匹配,最重要的切入点函数,功能最全
优点:可执行三种表达式方法切入点、类切入点、包切入点
缺点:
书写复杂 可用其它切入点函数简化,功能上完全一致
args:主要用于函数参数的匹配 只关注()里面的内容
execution(* *(String,String))
args(String,String)
within: 类 包切入点表达式的匹配
execution(* *..UserServiceImpl.*(..))
within(*..UserServiceImpl)
@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(..))