系列文章:
Java | Spring框架学习笔记–(1)工厂
Java | Spring框架学习笔记–(2)AOP
Java | Spring框架学习笔记–(3)持久层整合
Java | Spring框架学习笔记–(4)MVC框架整合
Java | Spring框架学习笔记–(5)注解编程
Spring笔记(2) AOP
静态代理设计模式
概念:通过代理类,为原始类增加额外功能
好处:利于原始类的维护
名词解释:
1. ⽬标类 原始类
指的是 业务类 (核⼼功能 --> 业务运算 DAO调⽤)
2. ⽬标⽅法,原始⽅法
⽬标类(原始类)中的⽅法 就是⽬标⽅法(原始⽅法)
3. 额外功能 (附加功能)
⽇志,事务,性能
代理类的核心要素:
- 原始类
- 额外功能
- 和原始类实现同一接口
静态代理:为每⼀个原始类,⼿⼯编写⼀个代理类
代码
-
接口:
public interface UserService { void register(); void login(); }
-
原始类
public class UserServiceImpl implements UserService{ @Override public void register() { System.out.println("UserServiceImpl.register"); } @Override public void login() { System.out.println("UserServiceImpl.login"); } }
-
代理类(和原始类实现同一个接口)
public class UserServiceProxy implements UserService{ UserService userService = new UserServiceImpl(); @Override public void register() { System.out.println("代理"); userService.register(); } @Override public void login() { System.out.println("代理"); userService.login(); } }
-
测试
public class TestProxy {
@Test
public void test(){
UserService userService = new UserServiceProxy();
userService.login();
userService.register();
}
}
-
输出
代理 UserServiceImpl.login 代理 UserServiceImpl.register Process finished with exit code 0
(可以看到,既实现了额外功能,也实现了原始功能)
存在的问题
- 静态类文件数量过多,不利于项目管理
有一个UserServiceImpl就有一个UserServiceProxy - 额外功能维护性差
Spring的动态代理
环境搭建
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
开发步骤
-
创建原始对象(目标对象)
public class UserServiceImpl implements UserService{ @Override public void register() { System.out.println("UserServiceImpl.register"); } @Override public void login() { System.out.println("UserServiceImpl.login"); } }
<bean id="userService" class="com.prince.proxy.UserServiceImpl" />
-
创建一个类来实现
MethodBeforeAdvice
接口,类里面的方法里实现额外功能public class Before implements MethodBeforeAdvice { /** * 需要把运⾏在原始⽅法执⾏之前运⾏的额外功能,书写在before⽅法中 * @param method * @param objects * @param o * @throws Throwable */ @Override public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println("Before.before"); } }
<bean id="before" class="com.prince.proxy.Before" />
-
定义切入点,并组装
<aop:config> <!-- 这里的id随便起, expression:execution()括号里面是切入点表达式 --> <aop:pointcut id="pc" expression="execution(* *(..))"/> <!-- advice-ref:指的是那个代理对象的id pointcut-ref:对应<aop:pointcut>的id 当<aop:pointcut>里配置的expression满足条件的时候,执行before方法。 --> <aop:advisor advice-ref="before" pointcut-ref="pc"/> </aop:config>
-
测试
通过getBean()
,按照原来的方式传入id值,获取的就是代理对象。@Test public void test(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.register(); userService.login(); }
细节分析
-
Spring创建的动态代理类在哪里?
Spring框架在运⾏时,通过动态字节码技术,在JVM创建的,运⾏在JVM内部,等程序结束后,会和JVM⼀起消失 什么叫动态字节码技术:通过第三个动态字节码框架(ASM Javassist Cglib),在JVM中创建对应类的字节码,进⽽创建对象,当虚拟机结束,动态字节码跟着消失。 结论:动态代理不需要定义类⽂件,都是JVM运⾏过程中动态创建的,所以不会造成静态代理,类⽂件数量过多,影响项⽬管理的问题。
-
动态代理编程简化代理的开发
在额外功能不改变的前提下,创建其他⽬标类(原始类)的代理对象时,只需要指定原始(⽬标)对象即可。
(定义切入点的时候并不指定哪个类,只要切入点表达式和方法匹配就可以了) -
维护性大大增强
MethodBeforeAdvice
public class Before implements MethodBeforeAdvice {
/**
* 需要把运⾏在原始⽅法执⾏之前运⾏的额外功能,书写在before⽅法中
* @param method 指的是额外功能增加给的那个原始方法
* @param objects 方法的参数
* @param o 原始对象(比如UserService)
* @throws Throwable
*/
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("Before.before");
}
}
MethodInterceptor
MethodBeforeAdvice
有一个局限性:只能运行在方法执行之前
MethodInterceptor
是一个更高级的接口,可以将额外功能加在方法执行之前,也可以加在方法执行之后,也可以同时加在两个地方。
注意导入包的时候导入的是org.aopalliance.intercept.MethodInterceptor
public class Around implements MethodInterceptor {
/**
* MethodInterceptor方法拦截器,将所需要实现的额外功能加在invoke方法里
* @param methodInvocation 相当于`MethodBeforeAdvice`的Method参数,这个更高级一点,在方法里面我们要手动调用methodInvocation.proceed()来执行原始方法
* @return 返回值,直接返回原始方法的返回值就行了。
* @throws Throwable
*/
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("-----------方法执行之前的额外功能写在这-----------");
Object retVal = null;
try {
retVal = methodInvocation.proceed();//执行方法
} catch (Throwable throwable) {
System.out.println("-----------方法执行且抛出异常后的额外功能写在这-----------");
} finally {
System.out.println("-----------方法执行finally后的额外功能写在这-----------");
}
System.out.println("-----------方法执行之后的额外功能写在这-----------");
return retVal;
}
}
配置也是和之前一样的那4个步骤
<bean id="userService" class="com.prince.proxy.UserServiceImpl" />
<!-- <bean id="before" class="com.prince.proxy.Before" />-->
<bean id="around" class="com.prince.proxy.Around"/>
<aop:config>
<!--
这里的id随便起,
expression:execution()括号里面是切入点表达式
-->
<aop:pointcut id="pc" expression="execution(* *(..))"/>
<!--
advice-ref:指的是那个代理对象的id
pointcut-ref:对应<aop:pointcut>的id
当<aop:pointcut>里配置的expression满足条件的时候,执行before方法。
-->
<!-- <aop:advisor advice-ref="before" pointcut-ref="pc"/>-->
<aop:advisor advice-ref="around" pointcut-ref="pc"/>
</aop:config>
输出(可以看到2个方法都有被代理)
-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.register
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------
-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.login
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------
切入点表达式
定义切入点的时候,表达式* *(..)
就是切入点表达式。
* *(..)
表示匹配所有的方法。
<aop:pointcut id="pc" expression="execution(* *(..))"/>
第一个* -- 修饰符,返回值
第二个* -- 方法名
() -- 参数表
.. -- 对参数没有要求
-
方法切入点表达式
* *(..) --> 所有方法 * ---> 修饰符 返回值 * ---> 方法名 ()---> 参数表 ..---> 对于参数没有要求 (参数有没有,参数有几个都行,参数是什么类型的都行)
-
定义login方法作为切入点
* login(..) # 定义register作为切入点 * register(..)
-
定义login方法且login方法有两个字符串类型的参数 作为切入点
* login(String,String) #注意:非java.lang包中的类型,必须要写全限定名 * register(com.baizhiedu.proxy.User) # ..可以和具体的参数类型连用 * login(String,..) --> login(String),login(String,String),login(String,com.baizhiedu.proxy.User)
-
精准方法切入点限定
修饰符 返回值 包.类.方法(参数) * com.baizhiedu.proxy.UserServiceImpl.login(..) * com.baizhiedu.proxy.UserServiceImpl.login(String,String)
-
-
类切入点
指定特定类作为切入点(额外功能加入的位置),自然这个类中的所有方法,都会加上对应的额外功能
-
语法1
#类中的所有方法加入了额外功能 * com.baizhiedu.proxy.UserServiceImpl.*(..)
-
语法2
#忽略包 1. 类只存在一级包 com.UserServiceImpl * *.UserServiceImpl.*(..) 2. 类存在多级包 com.baizhiedu.proxy.UserServiceImpl * *..UserServiceImpl.*(..)
-
-
包切入点表达式 实战
指定包作为额外功能加入的位置,自然包中的所有类及其方法都会加入额外的功能
-
语法1
#切入点包中的所有类,必须在proxy中,不能在proxy包的子包中 * com.baizhiedu.proxy.*.*(..)
-
语法2
#切入点当前包及其子包都生效 * com.baizhiedu.proxy..*.*(..)
-
切入点函数
作用:用于执行切入点表达式
- execution
最为重要、功能最全。
如:execution(* com.baizhiedu.proxy..*.*(..))
弊端:书写麻烦 - args
主要用于方法参数的匹配
execution(* *(String,String))
和args(String,String)
是一样的。 - within
主要用于进行类、包切入点表达式的匹配
execution(* *..UserServuceImpl.*(..))
可以替换成within(*..UserServiceImpl)
(匹配UserServuceImpl类的所有方法) @annotation
作用:为加入指定注解的方法加入额外功能
@annotation
作用:为加入指定注解的方法加入额外功能
注解如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
UserServiceImpl中给login方法加入Log注解
@Log
@Override
public void login() {
System.out.println("UserServiceImpl.login");
}
修改切入点表达式(annotation里面写的是注解的全类名)
<aop:pointcut id="pc" expression="@annotation(com.prince.proxy.Log)"/>
测试,可以发现只有login方法有被代理
UserServiceImpl.register
-----------方法执行之前的额外功能写在这-----------
UserServiceImpl.login
-----------方法执行finally后的额外功能写在这-----------
-----------方法执行之后的额外功能写在这-----------
切入点函数的逻辑运算
逻辑运算:指的是 整合多个切⼊点函数⼀起配合⼯作,进而完成更为复杂的需求
-
and 与操作
用法:将2个切入点函数用and
运算符连接在一起案例:login方法,并且参数为2个String类型的字符串,可以这样写
execution(* login(String,String)) execution(* login(..)) and args(String,String)
-
or 或操作
案例:login和register方法都能被代理
execution(* login(..)) or execution(* register(..))
AOP编程
AOP(Aspect Oriented Programing) 面向切面编程
切面 = 切入点 + 额外功能
面向切面编程 = Spring的动态代理开发
AOP的本质就是Spring的动态代理开发,通过代理类为原始类增加额外功能。
好处:利于原始类的维护。
AOP编程底层实现原理
核心问题
- AOP如何创建代理类(动态字节码技术)
- Spring工厂如何加工创建代理对象
– 通过原始对象的ID值,获得的是代理对象
动态代理类的创建
JDK的动态代理
使用的是JDK里内置的Proxy类
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
方法参数:
-
ClassLoader loader 类加载器
类加载器的作用:- 通过类加载器,把对应的字节码文件加载到JVM
- 通过类加载器创建类的Class对象,进而创建这个类的对象
如何获得类加载器:每一个类的.class文件,都会自动分配与之对应的ClassLoader
为什么要传入ClassLoader:Proxy通过newProxyInstance创建动态代理的过程中,需要ClassLoader创建类的Class对象,可是因为动态代理类没有对应的.class文件,JVM也就不会为其分配ClassLoader,但是又需要只能当做参数传进去。 -
Class<?>[] interfaces 原始对象实现的所有接口
-
InvocationHandler h
new InvocationHandler() { /** * 作用:用于实现额外功能,额外功能可以放的位置和Spring动态代理的MethodInterceptor一样(运行原始方法前、后、前后、抛出异常时) * @param proxy 忽略掉,代表的是代理对象(newProxyInstance方法的返回值,也作为方法的参数放在这里) * @param method 额外功能,所增加给的那个原始方法 * @param args 原始方法的参数 * @return 返回值 * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return null; } }
编码实现:
public class JDK_Proxy {
public static void main(String[] args) {
UserService us = new UserServiceImpl();
UserService proxyInstance = (UserService) Proxy.newProxyInstance(us.getClass().getClassLoader(), us.getClass().getInterfaces(), new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("---------------log-----------------");
Object obj = method.invoke(us, args); //原始方法执行
return obj;
}
}); //创建代理对象
proxyInstance.login();
proxyInstance.register(); //通过代理对象来执行方法
}
}
细节:
-
newProxyInstance的第一个参数,可以是UserService的类加载器,也可以是JDK_Proxy 的类加载器。
-
JDK8.0之前,内部类访问外部的对象应该加入final
final UserService us = new UserServiceImpl();
CGlib的动态代理
原理:
CGlib和JDK的实现方式的区别是:JDK的动态代理是原始对象和代理对象都实现相同的接口,而CGlib的动态代理是父子继承的关系,原始类作为⽗类,代理类作为⼦类,这样既可以保证2者⽅法⼀致,同时在代理类中提供新的实现(额外功能+原始⽅法) 。
编码:
UserService.java(不实现任何接口)
public class UserService {
public void login(){
System.out.println("UserService.login");
}
public void register(){
System.out.println("UserService.register");
}
}
TestCglib.java
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class TestCglib {
public static void main(String[] args) {
// 1. 创建原始对象
UserService us = new UserService();
// 2. 通过CGlib来创建代理对象(Spring已集成,不需要另外导包)
Enhancer enhancer = new Enhancer();
// 2.1 和Proxy一样,需要设置3样东西--类加载器,实现的接口(CGlib的是继承的父类),回调函数
enhancer.setClassLoader(TestCglib.class.getClassLoader()); //类加载器
enhancer.setSuperclass(UserService.class); //父类(原始对象的类)
//回调函数 -- 注意和之前Spring动态代理的那个MethodInterceptor所在的包不一样,这个的是org.springframework.cglib.proxy.MethodInterceptor
MethodInterceptor mi = new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("-----------TestCglib.intercept----------");
Object obj = method.invoke(us,args);
return obj;
}
};
// 设置回调函数
enhancer.setCallback(mi);
// 获取代理对象
UserService userService = (UserService) enhancer.create();
userService.login();
userService.register();
}
}
输出:
-----------TestCglib.intercept----------
UserService.login
-----------TestCglib.intercept----------
UserService.register
Process finished with exit code 0
Spring对动态代理的加工
思路分析:(用BeanPostProcessor就可了)
编码:
-
通过BeanPostProcessor对原始对象进行加工
public class ProxyBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { //通过动态代理对对象进行加工 return Proxy.newProxyInstance(ProxyBeanPostProcessor.class.getClassLoader(), bean.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("================ProxyBeanPostProcessor.invoke====================="); return method.invoke(bean,args); } }); } }
-
把这个ProxyBeanPostProcessor加入到Spring的容器中
<bean id="userService" class="com.prince.proxy.UserServiceImpl" /> <bean id="beanPostProcessor" class="com.prince.factory.ProxyBeanPostProcessor" />
-
根据id获取UserServiceImpl时,实际上获取到的就是代理对象
@Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext1.xml"); UserService userService = applicationContext.getBean("userService", UserService.class); userService.register(); userService.login(); }
基于注解的AOP编程
开发步骤:
- 原始对象
- 额外功能
- 切入点
- 组装切面
@Aspect注解
定义额外功能的类,需要加入@Aspect
注解来代表这是一个切面类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect //加入Aspect注解,代表这是一个切面类
public class MyAround {
/*
和MethodInterceptor相比,这个不用实现指定的接口
可以自己随便定义一个方法,方法名可以随便起,只要加上@Around注解
注解里面是切入点表达式
方法参数ProceedingJoinPoint和MethodInterceptor接口中invoke方法的MethodInvocation是一样的
*/
@Around("execution(* *(..))") //加上Around注解,代表里面的是额外功能
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================log=====================");
Object ret = joinPoint.proceed();
return ret;
}
}
xml配置:(注意要加入<aop:aspectj-autoproxy />
的标签来开启注解配置)
<bean id="userService" class="com.prince.aspectj.UserServiceImpl" />
<bean class="com.prince.aspectj.MyAround" id="around"></bean>
<!--开启注解配置-->
<aop:aspectj-autoproxy />
在测试类中,通过userService这个id值就可以获取到对应的代理对象。
切入点复用@Pointcut注解
如果切面类中里面有2个切入点方法,2个切入点都是同一个表达式,那么我们就可以把2个切入点表达式抽取出来。
@Aspect
public class MyAround1 {
@Around("execution(* login(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================log=====================");
Object ret = joinPoint.proceed();
return ret;
}
@Around("execution(* login(..))")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================hhh=====================");
Object ret = joinPoint.proceed();
return ret;
}
}
使用@Pointcut
注解就可以将切入点表达式抽取出来
步骤:
- 定义一个方法,方法名随便起,但是该方法必须是public修饰,返回值是void
- 给这个方法加上
@Pointcut
注解,参数写上切入点表达式 @Around("方法名()")
就可以使用到那个切入点表达式
@Aspect
public class MyAround1 {
@Pointcut("execution(* login(..))")
public void myPointcut(){}
@Around("myPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================log=====================");
Object ret = joinPoint.proceed();
return ret;
}
@Around("myPointcut()")
public Object around1(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================hhh=====================");
Object ret = joinPoint.proceed();
return ret;
}
}
Spring对JDK和CGlib动态代理的切换
运行测试类,打开断点,我们发现Spring底层默认用的是JDK的动态代理,而不是CGlib
如果想切换到CGlib,可以使用以下方法:
-
基于注解:给开启注解配置的标签
<aop:aspectj-autoproxy />
加上一个属性
值默认为false,如果为true则代表采用CGlib的方式<aop:aspectj-autoproxy proxy-target-class="true" />
-
传统的AOP开发,给
<aop:config>
标签加上一样的属性<aop:config proxy-target-class="true">
AOP开发中的一个坑
坑:在同一个业务类中,进行业务方法间的调用时,只有最外层的方法,才是加入了额外功能的方法,而内部的方法还是原始对象的方法。
验证:
从@Around
注解的属性来看,我们的切入点表达式是匹配所有方法的
@Aspect
public class MyAround {
@Around("execution(* *(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable{
System.out.println("====================log=====================");
Object ret = joinPoint.proceed();
return ret;
}
}
如果我们在UserServiceImpl中的register方法里面调用login方法
public class UserServiceImpl implements UserService {
@Override
public void register() {
System.out.println("UserServiceImpl.register");
this.login();
}
@Log
@Override
public void login() {
System.out.println("UserServiceImpl.login");
}
}
在测试类中只调用register方法
@Test
public void test(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.register();
}
输出,可以发现,只有register方法加入了额外功能,而login方法没有加入额外功能
====================log=====================
UserServiceImpl.register
UserServiceImpl.login
分析login方法没有加入额外功能的原因:在UserServiceImpl中的register方法里面调用login方法时候,this.login()
的那个this还是原始对象,而不是代理对象
解决方法:在Spring的工厂中重新获取代理对象,然后调用
public class UserServiceImpl implements UserService {
@Override
public void register() {
System.out.println("UserServiceImpl.register");
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext2.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}
@Log
@Override
public void login() {
System.out.println("UserServiceImpl.login");
}
}
但是!ApplicationContext是一个重量级资源,不可以多创建,只创建一个就好。
我们可以拿测试类中的那个ApplicationContext过来用就可以了
ApplicationContextAware接口的作用:当在Spring工厂中创建这个类的对象时,会自动把这个ApplicationContext通过setApplicationContext
方法传进来。
public class UserServiceImpl implements UserService, ApplicationContextAware {
ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void register() {
System.out.println("UserServiceImpl.register");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.login();
}
@Log
@Override
public void login() {
System.out.println("UserServiceImpl.login");
}
}
运行测试类,输出,可以发现两个方法都加上了额外功能
====================log=====================
UserServiceImpl.register
====================log=====================
UserServiceImpl.login
Process finished with exit code 0