收藏这三篇笔记,完整回顾Spring常见问题及使用方式速查:
0. 基本概念
- 面向切面编程,它将业务逻辑的各个部分进行隔离,使开发人员在编写业务逻辑时可以专心于核心业务,从而提高了开发效率。
- 关注点(切入点)代码,就是指重复执行的代码。
- 业务代码与关注点代码分离:关注点代码写一次即可;开发者只需要关注核心业务,运行时期,执行核心业务代码时候动态植入关注点代码。
// 关注点代码举例
public void add(User user) {
Session session = null;
Transaction trans = null;
try {
session = HibernateSessionFactoryUtils.getSession(); // 【关注点代码】
trans = session.beginTransaction(); // 【关注点代码】
session.save(user); // 核心业务代码:如何保存、用户有效性校验
trans.commit(); //…【关注点代码】
} catch (Exception e) {
e.printStackTrace();
if(trans != null){
trans.rollback(); //..【关注点代码】
}
} finally{
HibernateSessionFactoryUtils.closeSession(session); ..【关注点代码】
}
}
1. AOP概念及术语
术语 | 解释 |
---|---|
Joinpoint(连接点) | 指那些被拦截到的点,在 Spring 中,可以被动态代理拦截目标类的方法。 |
Pointcut(切入点) | 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点(方法)。 |
Advice(通知) | 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。 |
Target(目标) | 指代理的目标对象。 |
Weaving(植入) | 指把增强代码应用到目标上,生成代理对象的过程。 |
Proxy(代理) | 指生成的代理对象。 |
Aspect(切面) | 切入点和通知的结合。 |
2. 动态代理
2.1 代理模式
为其他对象提供一个代理以控制对某个对象的访问,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。在Spring中被用来做无侵入的代码增强。
和装饰器模式有什么不同?答:不会产生太多的装饰类。
2.1.1 静态代理
- 被代理类承担、插入自己的方法。
- 创建一个代理类,持有被代理类(目标对象)的引用,实现接口(但具体业务由创建一个接口,被代理类(目标对象)实现接口。
显然,一个代理类只能代理一个目标对象,会造成目标类的泛滥。这也是“静态”的意思。
业务逻辑的接口:
public interface TargetInterface {
void doSomething();
}
目标对象:
public class TargetImpl implements TargetInterface{
@Override
public void doSomething() {
System.out.println("Hello World!");
}
}
代理类:
public class TargetProxy implements TargetInterface{
private TargetInterface target = new TargetImpl(); // 持有引用
@Override
public void doSomething() {
System.out.println("Before invoke" );
this.target.doSomething();
System.out.println("After invoke");
}
}
UML图:
2.1.2 动态代理
- 目标接口和目标对象和静态代理类一致。
- 运用反射来创建代理类。
代理类对象:
public class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke");
method.invoke(object, args);
System.out.println("After invoke");
return null;
}
}
最后基于反射完成代理过程,详见2.2小节:
InvocationHandler handler = new ProxyHandler(new TargetImpl());
TargetInterface targetProxy = (TargetInterface) Proxy.newProxyInstance(TargetImpl.getClassLoader(), TargetImpl.getInterfaces(), handler);
targetProxy.doSomething();
2.1.3 代理模式的缺点
- 静态代理:由于静态代理需要实现目标对象的相同接口,那么可能会导致代理类会非常非常多,不好维护。
- 动态代理:目标对象一定是要有接口的,没有接口就不能实现动态代理。
2.2 java.lang.reflect.Proxy
java.lang.reflect.Proxy
是基于反射的动态代理,是属于JDK的原生实现。
2.2.1 实现Invoke接口、注入增强代码
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
例如:
public class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke"); // 增强代码1
Object obj = method.invoke(object, args);
System.out.println("After invoke"); // 增强代码2
return obj;
}
}
2.2.2 基于JDK的Proxy获得代理对象
利用 static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler )
构建代理对象。其中各参数如下:
ClassLoader loader
:指定当前target对象使用类加载器,获取加载器的方法是固定的,如TargetInterface.class.getClassLoader()
。Class<?>[] interfaces
:target对象实现的接口的类型,使用泛型方式确认类型,如new Class[] { TargetInterface.class}
。InvocationHandler invocationHandler
:事件处理,执行target对象的方法时,会触发事件处理器的方法,会把当前执行target对象的方法作为参数传入。
2.3 CGLib
相较于基于JDK的动态代理仍有局限性,即其目标对象必须要实现至少一个接口。而借用CGlib则不需要,其凭借一个小而快的字节码处理框架ASM转换字节码并生成新的类。由于其基于内存构建出一个子类来扩展目标对象的功能,也被称为“子类代理”。
需要注意的是,目标类不能为不可继承的
final
类型或目标对象的方法不能为静态类型。
public class TargetProxyFactory {
public static TargetProxy getProxyBean() {
// 1. 准备目标类和自定义的切面类(用于增强目标对象)
final Target goodsDao = new Target();
final Aspect aspect = new Aspect();
// 2. 构建CgLib的核心类`Enhancer`
Enhancer enhancer = new Enhancer();
// 3. 确定需要增强的类
enhancer.setSuperclass(goodsDao.getClass());
// 4. 添加回调函数:实现一个MethodInterceptor接口
enhancer.setCallback(() -> {
// intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
aspect.myBefore(); // 前增强
Object obj = method.invoke(goodsDao, args); // 目标方法执行
aspect.myAfter(); // 后增强
return obj;
}
});
// 5. 创建代理类
TargetProxy targetProxy = (TargetProxy) enhancer.create();
return targetProxy;
}
}
构建CGLib依赖的pom.xml
文件为:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
3. Spring中的AOP
3.1 pom.xml文件
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!-- Spring 2.0 以后,新增了对 AspectJ 方式的支持,新版本的 Spring 框架,建议使用 AspectJ 方式开发 AOP -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
3.2 基于通知类型代理增强的Bean
3.2.1 AOP通知类型
- 通知(Advice)其实就是对目标切入点进行增强的内容。
名称 | 说明 |
---|---|
org.springframework.aop.MethodBeforeAdvice(前置通知) | 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。 |
org.springframework.aop.AfterReturningAdvice(后置通知) | 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。 |
org.aopalliance.intercept.MethodInterceptor(环绕通知) | 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。 |
org.springframework.aop.ThrowsAdvice(异常通知) | 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。 |
org.springframework.aop.IntroductionInterceptor(引介通知) | 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。 |
3.2.2 示例:拦截器与工厂方法
现在,假设要增强 UserDao
,切入点是 save()
方法,要在之前加入自己面向 User
的增强方法,如校验等切面业务。核心要点有:
- 目标对象要实现一个通用接口(除非使用CGlib)。
- 代码增强类(切面类)实现一种通知的接口,在其中做增强。
- 在配置文件中利用
org.springframework.aop.framework.ProxyFactoryBean
创建代理类,需要给出proxyInterfaces
(目标对象的接口)、target
(目标对象的引用)、interceptorNames
(拦截器/切面类的名字)。
@Repository("userDao")
public class UserDao implements UserDaoInterface { // 实现一个通用接口
public void save(User user){
System.out.println("数据库已保存" + user); // 业务代码
}
}
代码切面类:
public class UserDaoAspect implements MethodInterceptor { // 此处以环绕通知为例子
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("Dao enhanced before"); // 增强1(重复的切入点代码)
Object obj = methodInvocation.proceed(); // 这里会由Spring替我们注入target对象
System.out.println("Dao enhanced after"); // 增强2(重复的切入点代码)
return obj;
}
}
创建配置文件:
<beans>
<bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 拦截器/切面类-->
<bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目标类 -->
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<!--代理对象实现的接口、目标对象、拦截者(切面类) -->
<property name="proxyInterfaces" value="MVC.Model.Dao.UserDaoInterface"/>
<property name="target" ref="targetUserDao"/> <!-- 此处是一个引用ref -->
<property name="interceptorNames" value="userDaoAspect"/>
<!-- 如何生成代理,true:使用CGLib; false :使用JDK动态代理(默认) -->
<property name="proxyTargetClass" value="true"/>
</bean>
</beans>
其中, UserService
类需要进行修改:
@Service("userService")
public class UserService {
@Resource(name = "userDaoProxy") // 注入ProxyFactoryBean工厂方法获得的代理类(增强类)
private UserDaoInterface userDao; // 修改为其接口
public void service(User user){
System.out.println("MVC Service sth. with " + user);
this.userDao.save(user);
System.out.println("MVC Service Over.");
}
}
工程结构:
3.3 使用AspectJ开发AOP(推荐)
- AspectJ 是一个基于 Java 语言的 AOP 框架,它扩展了 Java 语言。Spring 2.0 以后,新增了对 AspectJ 方式的支持,新版本的 Spring 框架,建议使用 AspectJ 方式开发 AOP。
- 主要有两种开发方法:“基于XML的声明式开发”和“基于注解的声明式开发”。
3.3.1 示例①:基于XML的声明式开发
需要引入命名空间:
<beans xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
编写切面类:
public class UserDaoAspect { // 以前、后通知为例
public void asBefore() {
System.out.println("Dao enhanced before"); // 一些重复的代码
}
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
被增强的目标类(不再需要接口):
@Repository("userDao")
public class UserDao {
public void save(User user){
System.out.println("数据库已保存" + user); // 业务代码
}
}
编写配置文件:
<beans>
<bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 切面类 -->
<bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目标类(随后都会变成代理类) -->
<!-- 面向切面编程,交由Spring管理-->
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut expression="execution(* MVC.Model.Dao.UserDao.save(..))" id="pointcut-save"/>
<!-- 对切入点配置切面 -->
<aop:aspect ref="userDaoAspect">
<!-- 配置前置增强 -->
<aop:before method="asBefore" pointcut-ref="pointcut-save" />
<aop:after method="asAfter" pointcut-ref="pointcut-save"/>
</aop:aspect>
</aop:config>
<!-- 如何生成代理,true:使用CGLib; false :使用JDK动态代理(默认)|如果目标类没有声明接口,则Spring将自动使用CGLib动态代理 -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
注意此处
proxy-target-class="false"
的话注入会报错...but was actually of type ‘com.sun.proxy.$Proxy7'
。
获取增强类:
UserDao userDaoProxy = (UserDao) applicationContext.getBean("targetUserDao");
3.3.2 示例中的标签及对应的切面类解析
附 <aop>
标签格式概览:
<aop:config>
<!-- 配置切入点,通知最后增强哪些方法 -->
<aop:pointcut expression="execution ( * target.* (..))" id="pointcut-id-x" />
<aop:aspect ref="myAspect">
<!--前置通知,关联通知 Advice和切入点PointCut -->
<aop:before method="myBefore" pointeut-ref="pointcut-id-x" />
<!--后置通知,在方法返回之后执行,就可以获得返回值returning 属性 -->
<aop:after-returning method="myAfterReturning" pointcut-ref="pointcut-id-x" returning="returnVal" />
<!--环绕通知 -->
<aop:around method="myAround" pointcut-ref="pointcut-id-x" />
<!--抛出通知:用于处理程序发生异常,可以接收当前方法产生的异常 -->
<!-- * 注意:如果程序没有异常,则不会执行增强 -->
<!-- * throwing属性:用于设置通知第二个参数的名称,类型Throwable -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="pointcut-id-x" throwing="e" />
<!--最终通知:无论程序发生任何事情,都将执行 -->
<aop:after method="myAfter" pointcut-ref="pointcut-id-x" />
</aop:aspect>
</aop:config>
对应的切面类:
//切面类
public class MyAspect {
// 前置通知
public void myBefore(JoinPoint joinPoint) {
System.out.print("前置通知,目标:" + joinPoint.getTarget() + " 方法名称: " + joinPoint.getSignature().getName());
}
// 后置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("后置通知,方法名称:" + joinPoint.getSignature().getName());
}
// 环绕通知
public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
throws Throwable {
System.out.println("环绕开始"); // 开始
Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法
System.out.println("环绕结束"); // 结束
return obj;
}
// 异常通知
public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
System.out.println("异常通知" + "出错了" + e.getMessage());
}
// 最终通知
public void myAfter() {
System.out.println("最终通知");
}
}
3.3.3 示例②:基于注解的声明式开发
名称 | 说明 |
---|---|
@Aspect | 用于定义一个切面。 |
@Before | 用于定义前置通知,相当于 BeforeAdvice。 |
@AfterReturning | 用于定义后置通知,相当于 AfterReturningAdvice。 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。 |
@AfterThrowing | 用于定义抛出通知,相当于ThrowAdvice。 |
@After | 用于定义最终final通知,不管是否异常,该通知都会执行。 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor。 |
编写配置文件:
<context:component-scan base-package="MVC"/> <!-- 扫描注解 -->
<aop:aspectj-autoproxy proxy-target-class="true"/> <!-- 使用CGlib植入切面代码 -->
构建切面类:
@Aspect
@Component
public class UserDaoAspect {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))") // 配置切入点
private void pointCut(){} // 要求:方法必须是private,没有值,名称自定义,没有参数
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before");
}
@After("pointCut()")
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
3.4 一对多的切面及相关问题
3.4.1 实现多切面的有序运行
- 当有多个切面时,它不会存在任何顺序,这些顺序代码会随机生成,但是有时候我们希望它按照指定的顺序运行。
- 此时,需要借助
org.springframework.core.annotation.Order
注解类或实现org.springframework.core.Ordered
接口。
构建一个新的切面:
@Aspect
@Component
@Order(value = 2) // 会在第二个执行
public class UserDaoAspect2 {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
private void pointCut(){} // 要求:方法必须是private,没有值,名称自定义,没有参数
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before 2");
}
@After("pointCut()")
public void asAfter() {
System.out.println("Dao enhanced after 2");
}
}
3.4.2 在注解中实现多个切入点
假设有一个新的业务需要被 UserDao
切入:
@Repository
public class ShopDao {
public void load(){
System.out.println("载入商品");
}
}
则 UserDao
需要修改为:
@Aspect
@Component
public class UserDaoAspect {
@Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
private void pointCut(){}
@Pointcut("execution(* MVC.Model.Dao.ShopDao.load())")
private void pointCut2(){} // 新的切入点
@Before("pointCut()")
public void asBefore() {
System.out.println("Dao enhanced before");
}
@After("pointCut() || pointCut2()") // 修改表达式语句,植入代码
public void asAfter() {
System.out.println("Dao enhanced after");
}
}
3.4.3 获取代理对象的目标对象
由于被CGLib植入之后,IoC容器中所有的目标对象都会变成代理对象,且Spring没有提供获取原生对象的API。
参考解决方法:CSDN@在spring中获取代理对象代理的目标对象工具类。
import java.lang.reflect.Field;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.framework.AopProxy;
import org.springframework.aop.support.AopUtils;
public class AopTargetUtils {
public static Object getTarget(Object proxy) throws Exception {
return !AopUtils.isAopProxy(proxy) ? proxy :
(AopUtils.isJdkDynamicProxy(proxy) ? getJDKDynamicProxyTargetObject(proxy) : getCGlibProxyTargetObject(proxy))
}
// 获取CGLib 代理的对象
private static Object getCGlibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
}
// 获取JDK代理的对象
private static Object getJDKDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
return ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
}
}
3.5 切入点表达式
切入点表达式为:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
符号讲解:
?
号代表0或1,表明可选参数。*
号代表任意类型取0或多,常用作通配符。
表达式匹配参数讲解:
modifiers-pattern?
:【可选】连接点的类型。ret-type-pattern
:【必填】连接点返回值类型,常用*
做匹配。declaring-type-pattern?
:【可选】连接点的类型(包.类
),如com.example.User
,通常不省略。name-pattern(param-pattern)
:【必填】要匹配的连接点名称,即方法
(如果给出了连接点的类型,要用.
隔开),如save(..)
;括号里面是方法的参数(匹配方法见下)。throws-pattern?
:【可选】连接点抛出的异常类型。
方法参数****的匹配方法:
()
匹配不带参数的方法。(..)
匹配带参数的方法(任意个)。(*, String)
匹配带两个参数的方法且第二个必为String。
3.6 PointCut指示符
除了使用 execution
作为切入点表达式进行配置,还可以使用以下表达式内容(需要保证所有的连接点都在IoC容器内):
within
:匹配所有在指定子包内的类的连接点,如within(com.xyz.service.*)
、within(com.xyz.service..*)
;严格匹配目标对象,不理会继承关系 。this
: 匹配所有代理对象为目标类型中的连接点,如this(com.xyz.service.AccountService)
。target
:匹配所有实现了指定接口的目标对象中的连接点,如target(com.xyz.service.UserDaoInterfece)
。bean
:匹配所有指定命名方式的类的连接点,如bean(userDao)
。args
:匹配任何方法参数是指定的类型的连接点,如args(*, java.lang.String)
、args(java.lang.Long, ..)
。@within
:匹配标注有指定注解的类(不可为接口)的所有连接点(要求注解的Retention级别为CLASS),如@within(com.google.common.annotations.Beta)
;对子类不起效,除非使用@within(xxxx)+
或者子类中继承的方法未进行重载。@target
:匹配标注有指定注解的类(不可为接口)的所有连接点(要求注解的Retention级别为RUNTIME),如@target(org.springframework.stereotype.Repository)
;对子类不起效。@args
:匹配传入的参数类标注有指定注解的所有连接点,如@args(org.springframework.stereotype.Repository)
。@anntation
:匹配所有标注有指定注解的连接点,如@annotation(com.aop.annotation.AdminOnly)
。
除此之外,表达式还可以用 &&
、 ||
、 !
进行合并,详见3.4.2小节。
@within 和 @target的区别:
@within
:若当前类有注解,则该类对父类重载及自有方法被拦截。子类中未对父类的方法进行重载时,亦被拦截。@target
:若当前类有注解,则该类对父类继承、重载及自有的方法被拦截;对子类不起效。