Spring02-面向切面编程(AOP)
作者:互联网
面向切面编程[AOP]
代码冗余与装饰器模式
代码冗余现象
我们的Service层实现类中的每个方法都要加上事务控制,这样使得每个方法的前后都要加上重复的事务控制的代码,如下:
@Override
public void saveAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.save(account); // 唯一的一行业务代码
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
@Override
public void updateAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.update(account); // 唯一的一行业务代码
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
@Override
public void deleteAccount(Integer accountId) {
try {
TransactionManager.beginTransaction();
accountDao.delete(accountId); // 唯一的一行业务代码
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
我们发现出现了两个问题:
- 业务层方法变得臃肿了,里面充斥着很多重复代码.
- 业务层方法和事务控制方法耦合了. 若提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码.
因此我们引入了装饰模式解决代码冗余和耦合现象.
解决代码冗余的思路: 装饰模式和动态代理
动态代理的写法
常用的动态代理分为两种
-
基于接口的动态代理,使用JDK 官方的 Proxy 类,要求被代理者至少实现一个接口.
- 基于接口的动态代理:
- 涉及的类:Proxy
- 提供者:JDK官方
- 如何创建代理对象:
- 使用Proxy类中的newProxyInstance方法
- 创建代理对象的要求:
- 被代理类最少实现一个接口,如果没有则不能使用
- newProxyInstance方法的参数:
- ClassLoader:类加载器
- 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
- Class[]:字节码数组
- 它是用于让代理对象和被代理对象有相同方法。固定写法。
- InvocationHandler:用于提供增强的代码
- 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
- 此接口的实现类都是谁用谁写。
- ClassLoader:类加载器
接口名 新对象名 = (接口名)Proxy.newProxyInstance( 被代理的对象.getClass().getClassLoader(), // 被代理对象的类加载器,固定写法 被代理的对象.getClass().getInterfaces(), // 被代理对象实现的所有接口,固定写法 new InvocationHandler() { // 匿名内部类,通过拦截被代理对象的方法来增强被代理对象 /* 被代理对象的任何方法执行时,都会被此方法拦截到 其参数如下: proxy: 代理对象的引用,不一定每次都用得到 method: 被拦截到的方法对象 args: 被拦截到的方法对象的参数 返回值: 被增强后的返回值 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //提供增强的代码 Object returnValue = null; if("方法名".equals(method.getName())) { // 增强方法的操作 rtValue = method.invoke(被代理的对象, args); // 增强方法的操作 return rtValue; } } });
- 基于接口的动态代理:
-
基于子类的动态代理,使用第三方的 CGLib库,要求被代理类不能是final类.
- 基于子类的动态代理:
- 涉及的类:Enhancer
- 提供者:第三方cglib库
- 如何创建代理对象:
- 使用Enhancer类中的create方法
- 创建代理对象的要求:
- 被代理类不能是final类
- create方法的参数:
- Class:字节码
- 它是用于指定被代理对象的字节码。
- Callback:用于提供增强的代码
- 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
- 此接口的实现类都是谁用谁写。
- 我们一般写的都是该接口的子接口实现类:
MethodInterceptor
- Class:字节码
final Producer producer = new Producer(); // 这个代理对象没有实现接口 Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() { /** * 执行该代理对象的任何方法都会经过该方法 * @param proxy * @param method * @param args * 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的 * @param methodProxy :当前执行方法的代理对象 * @return * @throws Throwable */ @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { //提供增强的代码 Object returnValue = null; //1.获取方法执行的参数 Float money = (Float)args[0]; //2.判断当前方法是不是销售 if("saleProduct".equals(method.getName())) { returnValue = method.invoke(producer, money*0.8f); } return returnValue; } }); cglibProducer.saleProduct(12000f);
- 基于子类的动态代理:
使用动态代理解决代码冗余现象
我们使用动态代理对上述Service进行改造,创建BeanFactory
类作为service层对象工厂,通过其getAccountService
方法得到业务层对象.
// 用于创建Service的代理对象的工厂
public class BeanFactory {
private IAccountService accountService; // 被增强的service对象
private TransactionManager txManager; // 事务控制工具类
// 成员变量的set方法,以便Spring容器注入
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
// 获取增强后的Service对象
public IAccountService getAccountService() {
return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
// 增强方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
});
}
}
在bean.xml
中,添加如下配置
<!--配置beanfactory-->
<bean id="beanFactory" class="cn.maoritian.factory.BeanFactory">
<!-- 注入service -->
<property name="accountService" ref="accountService"></property>
<!-- 注入事务控制工具 -->
<property name="txManager" ref="txManager"></property>
</bean>
这样,我们就可以通过Spring的IOC获取增强后的Service对象.
使用SpringAOP解决代码冗余
AOP相关术语
-
Joinpoint
(连接点
): 被拦截到的方法. -
Pointcut
(切入点
): 我们对其进行增强的方法. -
Advice
(通知
/增强
): 对切入点进行的增强操作包括
前置通知
,后置通知
,异常通知
,最终通知
,环绕通知
-
Weaving
(织入
): 是指把增强应用到目标对象来创建新的代理对象的过程. -
Aspect
(切面
): 是切入点和通知的结合.
使用XML配置AOP
使用XML配置AOP的步骤
在bean.xml
中配置AOP要经过以下几步:
-
在
bean.xml中引入约束
并将通知类注入Spring容器中<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--通知类--> <bean id="logger" class="cn.maoritian.utils.Logger"></bean> </beans>
-
使用
<aop:config>
标签声明AOP配置,所有关于AOP配置的代码都写在<aop:config>
标签内<aop:config> <!-- AOP配置的代码都写在此处 --> </aop:config>
-
使用
<aop:aspect>
标签配置切面,其属性如下id
: 指定切入点表达式的id
ref
: 引用通知类的id
<aop:config> <aop:aspect id="logAdvice" ref="logger"> <!--配置通知的类型要写在此处--> </aop:aspect> </aop:config>
-
使用
<aop:pointcut>
标签配置切入点表达式,指定对哪些方法进行增强,其属性如下id
: 指定切入点表达式的id
expression
: 指定切入点表达式
<aop:config> <aop:aspect id="logAdvice" ref="logger"> <aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut> </aop:aspect> </aop:config>
-
使用
<aop:xxx>
标签配置对应类型的通知方法-
其属性如下:
method
: 指定通知类中的增强方法名.ponitcut-ref
: 指定切入点的表达式的id
poinitcut
: 指定切入点表达式
其中
pointcut-ref
和pointref
属性只能有其中一个 -
具体的通知类型:
<aop:before>
: 配置前置通知,指定的增强方法在切入点方法之前执行.<aop:after-returning>
: 配置后置通知,指定的增强方法在切入点方法正常执行之后执行.<aop:after-throwing>
: 配置异常通知,指定的增强方法在切入点方法产生异常后执行.<aop:after>
: 配置最终通知,无论切入点方法执行时是否发生异常,指定的增强方法都会最后执行.<aop:around>
: 配置环绕通知,可以在代码中手动控制增强代码的执行时机.
<aop:config> <aop:aspect id="logAdvice" ref="logger"> <!--指定切入点表达式--> <aop:pointcut expression="execution(* cn,maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut> <!--配置各种类型的通知--> <aop:before method="printLogBefore" pointcut-ref="pt1"></aop:before> <aop:after-returning method="printLogAfterReturning" pointcut-ref="pt1"></aop:after-returning> <aop:after-throwing method="printLogAfterThrowing" pointcut-ref="pt1"></aop:after-throwing> <aop:after method="printLogAfter" pointcut-ref="pt1"></aop:after> <!--环绕通知一般单独使用--> <!-- <aop:around method="printLogAround" pointcut-ref="pt1"></aop:around> --> </aop:aspect> </aop:config>
-
切入点表达式
-
切入点表达式的写法:
execution([修饰符] 返回值类型 包路径.类名.方法名(参数))
-
切入点表达式的省略写法:
-
全匹配方式:
<aop:pointcut expression="execution(public void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
其中访问修饰符可以省略:
<aop:pointcut expression="execution(void cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
返回值可使用
*
,表示任意返回值:<aop:pointcut expression="execution(* cn.maoritian.service.impl.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
包路径可以使用
*
,表示任意包. 但是*.
的个数要和包的层级数相匹配<aop:pointcut expression="execution(* *.*.*.*.AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
包路径可以使用
*..
,表示当前包,及其子包(因为本例子中将bean.xml
放在根路径下,因此..
可以匹配项目内所有包路径)<aop:pointcut expression="execution(* *..AccountServiceImpl.saveAccount(cn.maoritian.domain.Account))" id="pt1"></aop:pointcut>
-
类名可以使用
*
,表示任意类<aop:pointcut expression="execution(* *..*.saveAccount(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
-
方法名可以使用
*
,表示任意方法<aop:pointcut expression="execution(* *..*.*(com.itheima.domain.Account))" id="pt1"></aop:pointcut>
-
参数列表可以使用
*
,表示参数可以是任意数据类型,但是必须存在参数<aop:pointcut expression="execution(* *..*.*(*))" id="pt1"></aop:pointcut>
-
参数列表可以使用
..
表示有无参数均可,有参数可以是任意类型<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
-
全通配方式,可以匹配匹配任意方法
<aop:pointcut expression="execution(* *..*.*(..))" id="pt1"></aop:pointcut>
-
-
切入点表达式的一般写法
一般我们都是对业务层所有实现类的所有方法进行增强,因此切入点表达式写法通常为
<aop:pointcut expression="execution(* cn.maoritian.service.impl.*.*(..))" id="pt1"></aop:pointcut>
环绕通知
-
前置通知
,后置通知
,异常通知
,最终通知
的执行顺序Spring是基于动态代理对方法进行增强的,
前置通知
,后置通知
,异常通知
,最终通知
在增强方法中的执行时机如下:// 增强方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ Object rtValue = null; try { // 执行前置通知 // 执行原方法 rtValue = method.invoke(accountService, args); // 执行后置通知 return rtValue; } catch (Exception e) { // 执行异常通知 } finally { // 执行最终通知 } }
-
环绕通知
允许我们更自由地控制增强代码执行的时机Spring框架为我们提供一个接口
ProceedingJoinPoint
,它的实例对象可以作为环绕通知方法的参数,通过参数控制被增强方法的执行时机.ProceedingJoinPoint
对象的getArgs()
方法返回被拦截的参数ProceedingJoinPoint
对象的proceed()
方法执行被拦截的方法
// 环绕通知方法,返回Object类型 public Object printLogAround(ProceedingJoinPoint pjp) { Object rtValue = null; try { Object[] args = pjp.getArgs(); printLogBefore(); // 执行前置通知 rtValue = pjp.proceed(args);// 执行被拦截方法 printLogAfterReturn(); // 执行后置通知 }catch(Throwable e) { printLogAfterThrowing(); // 执行异常通知 }finally { printLogAfter(); // 执行最终通知 } return rtValue; }
使用注解配置AOP
半注解配置AOP
Spring注解配置AOP的步骤
半注解配置AOP,需要在bean,xml
中加入下面语句开启对注解AOP的支持
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
Spring用于AOP的注解
用于声明切面的注解
@Aspect
: 声明当前类为通知类,该类定义了一个切面.相当于xml配置中的<aop:aspect>
标签
@Component("logger")
@Aspect
public class Logger {
// ...
}
用于声明通知的注解
@Before
: 声明该方法为前置通知.相当于xml配置中的<aop:before>
标签@AfterReturning
: 声明该方法为后置通知.相当于xml配置中的<aop:after-returning>
标签@AfterThrowing
: 声明该方法为异常通知.相当于xml配置中的<aop:after-throwing>
标签@After
: 声明该方法为最终通知.相当于xml配置中的<aop:after>
标签@Around
: 声明该方法为环绕通知.相当于xml配置中的<aop:around>
标签
属性:
-
value
: 用于指定切入点表达式
或切入点表达式的引用
@Component("logger") @Aspect //表示当前类是一个通知类 public class Logger { // 配置前置通知 @Before("execution(* cn.maoritian.service.impl.*.*(..))") public void printLogBefore(){ System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。"); } // 配置后置通知 @AfterReturning("execution(* cn.maoritian.service.impl.*.*(..))") public void printLogAfterReturning(){ System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。"); } // 配置异常通知 @AfterThrowing("execution(* cn.maoritian.service.impl.*.*(..))") public void printLogAfterThrowing(){ System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。"); } // 配置最终通知 @After("execution(* cn.maoritian.service.impl.*.*(..))") public void printLogAfter(){ System.out.println("最终通知Logger类中的printLogAfter方法开始记录日志了。。。"); } // 配置环绕通知 @Around("execution(* cn.maoritian.service.impl.*.*(..))") public Object aroundPringLog(ProceedingJoinPoint pjp){ Object rtValue = null; try{ Object[] args = pjp.getArgs(); printLogBefore(); // 执行前置通知 rtValue = pjp.proceed(args); // 执行切入点方法 printLogAfterReturning(); // 执行后置通知 return rtValue; }catch (Throwable t){ printLogAfterThrowing(); // 执行异常通知 throw new RuntimeException(t); }finally { printLogAfter(); // 执行最终通知 } } }
用于指定切入点表达式的注解
@Pointcut
: 指定切入点表达式,其属性如下:
value
: 指定表达式的内容
@Pointcut
注解没有id
属性,通过调用被注解的方法获取切入点表达式.
@Component("logger")
@Aspect //表示当前类是一个通知类
public class Logger {
// 配置切入点表达式
@Pointcut("execution(* cn.maoritian.service.impl.*.*(..))")
private void pt1(){}
// 通过调用被注解的方法获取切入点表达式
@Before("pt1()")
public void printLogBefore(){
System.out.println("前置通知Logger类中的printLogBefore方法开始记录日志了。。。");
}
// 通过调用被注解的方法获取切入点表达式
@AfterReturning("pt1()")
public void printLogAfterReturning(){
System.out.println("后置通知Logger类中的printLogAfterReturning方法开始记录日志了。。。");
}
// 通过调用被注解的方法获取切入点表达式
@AfterThrowing("pt1()")
public void printLogAfterThrowing(){
System.out.println("异常通知Logger类中的printLogAfterThrowing方法开始记录日志了。。。");
}
}
纯注解配置AOP
在Spring配置类前添加@EnableAspectJAutoProxy
注解,可以使用纯注解方式配置AOP
@Configuration
@ComponentScan(basePackages="cn.maoritian")
@EnableAspectJAutoProxy // 允许AOP
public class SpringConfiguration {
// 具体配置
//...
}
使用注解配置AOP的bug
在使用注解配置AOP时,会出现一个bug. 四个通知的调用顺序依次是:前置通知
,最终通知
,后置通知
. 这会导致一些资源在执行最终通知
时提前被释放掉了,而执行后置通知
时就会出错.
标签:通知,切入点,Object,Spring02,代理,切面,AOP,方法,public 来源: https://www.cnblogs.com/kyrielin/p/13172075.html