收藏这三篇笔记,完整回顾Spring常见问题及使用方式速查:
- Spring 学习笔记①:IoC容器、Bean与注入
- Spring 学习笔记②:动态代理及面向切面编程(即本篇)
- Spring 学习笔记③:JDBC与事务管理
// 关注点代码举例
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); ////..【关注点代码】
}
}
术语 | 解释 |
---|---|
Joinpoint(连接点) | 指那些被拦截到的点,在 Spring 中,可以被动态代理拦截目标类的方法。 |
Pointcut(切入点) | 指要对哪些 Joinpoint 进行拦截,即被拦截的连接点(方法)。 |
Advice(通知) | 指拦截到 Joinpoint 之后要做的事情,即对切入点增强的内容。 |
Target(目标) | 指代理的目标对象。 |
Weaving(植入) | 指把增强代码应用到目标上,生成代理对象的过程。 |
Proxy(代理) | 指生成的代理对象。 |
Aspect(切面) | 切入点和通知的结合。 |
为其他对象提供一个代理以控制对某个对象的访问,代理类不现实具体服务,而是利用委托类来完成服务,并将执行结果封装处理。在Spring中被用来做无侵入的代码增强。
和装饰器模式有什么不同?答:不会产生太多的装饰类。
显然,一个代理类只能代理一个目标对象,会造成目标类的泛滥。这也是“静态”的意思。
业务逻辑的接口:
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图:
代理类对象:
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();
java.lang.reflect.Proxy
是基于反射的动态代理,是属于JDK的原生实现。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;
}
}
利用 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对象的方法作为参数传入。相较于基于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>cglibgroupId>
<artifactId>cglibartifactId>
<version>3.3.0version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aopartifactId>
<version>5.2.6.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.6.RELEASEversion>
dependency>
名称 | 说明 |
---|---|
org.springframework.aop.MethodBeforeAdvice(前置通知) | 在方法之前自动执行的通知称为前置通知,可以应用于权限管理等功能。 |
org.springframework.aop.AfterReturningAdvice(后置通知) | 在方法之后自动执行的通知称为后置通知,可以应用于关闭流、上传文件、删除临时文件等功能。 |
org.aopalliance.intercept.MethodInterceptor(环绕通知) | 在方法前后自动执行的通知称为环绕通知,可以应用于日志、事务管理等功能。 |
org.springframework.aop.ThrowsAdvice(异常通知) | 在方法抛出异常时自动执行的通知称为异常通知,可以应用于处理异常记录日志等功能。 |
org.springframework.aop.IntroductionInterceptor(引介通知) | 在目标类中添加一些新的方法和属性,可以应用于修改旧版本程序(增强类)。 |
现在,假设要增强 UserDao
,切入点是 save()
方法,要在之前加入自己面向 User
的增强方法,如校验等切面业务。核心要点有:
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"/>
<property name="interceptorNames" value="userDaoAspect"/>
<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.");
}
}
工程结构:
需要引入命名空间:
<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"/>
<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>
<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");
附
标签格式概览:
<aop:config>
<aop:pointcut expression="execution ( * target.* (..))" id="pointcut-id-x" />
<aop:aspect ref="myAspect">
<aop:before method="myBefore" pointeut-ref="pointcut-id-x" />
<aop:after-returning method="myAfterReturning" pointcut-ref="pointcut-id-x" returning="returnVal" />
<aop:around method="myAround" pointcut-ref="pointcut-id-x" />
<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("最终通知");
}
}
名称 | 说明 |
---|---|
@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"/>
构建切面类:
@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");
}
}
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");
}
}
假设有一个新的业务需要被 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");
}
}
由于被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();
}
}
切入点表达式为:
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。除了使用 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
:若当前类有注解,则该类对父类继承、重载及自有的方法被拦截;对子类不起效。