【README】
本文总结自《spring揭秘》,作者王福强,非常棒的一本书,墙裂推荐;
本文总结了aop的3个经典应用场景(当然,aop的应用范围不止这3点):
- 基于aop的全局异常处理;
- 基于aop的权限检查;
- 基于aop的缓存;
【1】基于aop实现全局异常处理
1)背景:业务逻辑在运行过程中会抛出运行时异常,如数据库连接不可用,http超时,空指针等;不同业务逻辑都需要定义异常处理逻辑,即便异常处理逻辑相同,即异常处理逻辑散落在各个业务逻辑中(散弹式代码);
- 解决方法: 定义异常处理逻辑切面,在一个地方统一拦截系统异常,实现异常处理代码内聚。也可以拦截后在通知或横切逻辑内部做相关处理,如发送kafka日志,发送即时运维消息等;
【AopAppFaultBarrierMain】异常处理(故障屏障)切面测试main
public class AopAppFaultBarrierMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext container =
new ClassPathXmlApplicationContext("chapter11/beans11aopappfaultbarrier.xml");
// 获取目标对象的代理对象
Object proxy = container.getBean("target");
try {
((BusiFileReader) proxy).readFileName("abcd");
} catch (Exception e) {
System.out.println(e.getMessage());
}
// 获取目标对象的代理对象
System.out.println("==== 我是分割线 ====");
Object userDaoProxy = container.getBean("userDAO");
try {
((UserDAO) userDaoProxy).qryUserNameById("");
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
【打印日志】
[@AfterThrowing]: faultBarrierHandleException
被切面拦截处理后的异常,原生异常消息=[文件不存在]
==== 我是分割线 ====
[@AfterThrowing]: faultBarrierHandleException
被切面拦截处理后的异常,原生异常消息=[用户id非法]
【beans11aopappfaultbarrier.xml】
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean class="com.tom.springnote.chapter11.faultbarrier.FaultBarrierAspect" />
<bean id="target" class="com.tom.springnote.chapter11.target.BusiFileReader" />
<bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>
【FaultBarrierAspect】异常处理切面 (环绕通知)
@Aspect
public class FaultBarrierAspect {
@AfterThrowing(pointcut = "execution(* *(..))", throwing = "e")
public void faultBarrierHandleException(Exception e) {
System.out.println("[@AfterThrowing]: faultBarrierHandleException");
throw new RuntimeException("被切面拦截处理后的异常,原生异常消息=[" + e.getMessage() + "]", e);
}
}
【BusiFileReader】目标类1
public class BusiFileReader {
public String readFileName(String path) {
File file = new File(path);
if (file.exists()) {
return file.getName();
} else {
throw new RuntimeException("文件不存在");
}
}
}
【UserDAO】目标类2
public class UserDAO {
public String qryUserNameById(String userId) {
if (!StringUtils.hasText(userId)) {
throw new RuntimeException("用户id非法");
}
System.out.println("UserDAO#qryUserNameById() 方法被调用");
return "张三" + userId;
}
}
【补充:java异常】
补充1:java把Error子类或RuntimeException子类的所有异常称为非受查异常; 其他异常称为受查异常;( 也可以这样理解: 受查异常的根因通常是外部环境引起的,如文件不存在;而非受查异常根因是内部程序问题导致的,即程序一定有问题)
补充2:受查指的是受编译器检查;即编译器在编译期间会检查所有受查异常是否都有异常处理器;如果没有,则程序编译不通过;
补充3:java异常层次结构:
补充4:常见异常举例:
- 非受查异常举例:
- Error异常:
- OutOfMemoryError
- StackOverflowError
- NoClassDefFoundError
- RuntimeException异常:
- NullPointerException
- ArrayIndexOutOfBoundException
- IllegalArgumentException
- Error异常:
- 受查异常举例:
- IOException
- SQLException
- ClassNotFoundException
【2】基于aop实现权限检查
1)背景:权限检查,包括但不限于安全检查,是一个web应用必须的功能; 如网关检查当前操作用户是否有操作权限,token解析是否成功等; 显然, 所有业务功能(对的就是所有业务功能)都需要权限检查,即权限检查是公共功能。 一般的,公共功能通过aop来实现,可以实现代码高内聚,且便于后续运维(如果某些方法不检查,仅需要修改pointcut表达式即可,而不用修改过多代码) ;
2)业务场景: 检查方法参数是否合法;不合法,抛出异常;
【AopAppSecurityCheckMain】
public class AopAppSecurityCheckMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext container =
new ClassPathXmlApplicationContext("chapter11/beans11aopappchecksecurity.xml");
// 获取目标对象的代理对象
Object userDaoProxy = container.getBean("userDAO");
try {
((UserDAO) userDaoProxy).qryUserNameById("");
} catch (Exception e) {
System.out.println("[main] " + e.getMessage());
}
}
}
【打印日志 】
[@Around] checkSecurity() 被调用
[main] 安全检查不通过: userId为空
【beans11aopappchecksecurity.xml】
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean class="com.tom.springnote.chapter11.security.SecurityCheckAspect" />
<bean class="com.tom.springnote.chapter11.faultbarrier.FaultBarrierAspect" />
<bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>
【SecurityCheckAspect】
@Aspect
public class SecurityCheckAspect {
@Around("execution(* *(..)) && args(userId)")
public Object checkSecurity(ProceedingJoinPoint joinPoint, String userId) throws Throwable {
System.out.println("[@Around] checkSecurity() 被调用");
if (!StringUtils.hasText(userId)) {
throw new RuntimeException("安全检查不通过: userId为空"); // 方法调用阻断,校验不通过,不继续调用目标对象方法
}
return joinPoint.proceed(); // 调用目标对象方法
}
}
【注意】
- 有个问题:代理类(即切面类SecurityCheckAspect)抛出的异常,不会被切面类 FaultBarrierAspect 异常处理类捕获; 即便 FaultBarrierAspect 是全局异常处理切面,因为其pointcut = "execution(* *(…)) ,匹配所有方法;
- 但需要注意的是,这里的所有方法指的是目标对象方法,不包括代理对象方法; 所以当代理对象方法 checkSecurity (如环绕通知方法)抛出异常,是不会被异常处理切面 FaultBarrierAspect 匹配到的 ;
【3】基于aop实现缓存
1)aop另一个主要应用场景: 为系统透明添加缓存,缓存在很大程度上可以提升系统性能,缓存是系统需求,而不是业务功能需求;
【AopCacheAspectMain】aop缓存main
public class AopCacheAspectMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext container =
new ClassPathXmlApplicationContext("chapter11/beans11aopappcache.xml");
// 获取代理对象
Object proxy = container.getBean("userDAO");
((UserDAO) proxy).qryUserNameById("01");
((UserDAO) proxy).qryUserNameById("01");
((UserDAO) proxy).qryUserNameById("02");
((UserDAO) proxy).qryUserNameById("01");
}
}
【打印日志】 ( 虽然qryUserNameById方法调用了4次,但通过日志可以看到,实际上目标对象只调用了2次,因为有缓存 )
[@Around] userInfoCache()被调用
UserDAO#qryUserNameById() 方法被调用 // 实际第1次调用
[@Around] userInfoCache()被调用
[@Around] userInfoCache()被调用
UserDAO#qryUserNameById() 方法被调用 // 实际第2次调用
[@Around] userInfoCache()被调用
【beans11aopappcache.xml】
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean class="com.tom.springnote.chapter11.cache.CacheAspect" />
<bean id="userDAO" class="com.tom.springnote.chapter11.target.UserDAO" />
</beans>
【CacheAspect】缓存切面
@Aspect
public class CacheAspect {
private static final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
@Around("args(usreId)")
public Object userInfoCache(ProceedingJoinPoint joinPoint, String userId) throws Throwable {
System.out.println("[@Around] userInfoCache()被调用");
Object value = cache.get(userId);
if (Objects.isNull(value)) {
cache.putIfAbsent(userId, joinPoint.proceed());
}
return value;
}
}
【4】aop无法拦截目标对象内部方法调用的问题
【4.1】问题概述
1)代理对象与目标对象:
- 表面看,织入器把横切逻辑或通知织入到与pointcut匹配的目标对象(或目标方法);
- 但实际上,横切逻辑或通知是织入到代理对象;代理对象与目标对象有着相同的方法(JDK动态代理实现aop,则代理对象与目标对象实现相同接口;CGLIB动态代理,则代理对象继承目标类 ); 调用代理对象的方法,代理对象根据通知类型执行横切逻辑(前置或后置),接着执行目标对象方法;即通过代理对象调用方法才会被拦截,而通过目标对象本身调用方法不会被拦截 ;
2)通过代理对象调用方法才会被拦截,而通过目标对象本身调用方法不会被拦截;
- 如 通过ProxyObject 调用方法(要被pointcut表达式匹配)会被拦截;
- 而通过 targetObject.method()1 调用 target自身方法method2(),不会被拦截;(即便method2被pointcut表达式匹配)
3)aop不拦截目标对象内部方法之间调用
【NestedCallMain】目标对象内部方法调用测试main
public class NestedCallMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext container = new ClassPathXmlApplicationContext("chapter12/beans12nestedcall.xml");
// 获取代理对象
Object proxy = container.getBean("nestedCallTarget");
((NestedCallTarget) proxy).method1();
}
}
【打印日志】
stopWatch.start()
NestedCallTarget#method1() 方法被调用
NestedCallTarget#method2() 方法被调用 // 显然,目标对象nestedCallTarget内部方法method1() 调用内部方法method2(),没有触发aop拦截
stopWatch.stop()
方法执行耗时1.028E-4
显然,目标对象nestedCallTarget内部方法method1() 调用内部方法method2(),没有触发aop拦截 ;
【beans12nestedcall.xml】
<?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:context="http://www.springframework.org/schema/context"
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/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 使用 <aop:aspectj-autoproxy> 元素,用于替换 AnnotationAwareAspectJAutoProxyCreator 自动代理织入器 -->
<aop:aspectj-autoproxy proxy-target-class="true" />
<bean class="com.tom.springnote.chapter12.target.NestedCallAspect" />
<bean id="nestedCallTarget" class="com.tom.springnote.chapter12.target.NestedCallTarget" />
</beans>
【NestedCallAspect】切面
@Aspect
public class NestedCallAspect {
@Around("execution(* method1(..)) || execution(* method2(..))")
public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
try {
System.out.println("stopWatch.start()");
stopWatch.start();
return joinPoint.proceed();
} finally {
System.out.println("stopWatch.stop()");
stopWatch.stop();
System.out.printf("方法执行耗时%s\n", stopWatch.getTotalTime(TimeUnit.SECONDS));
}
}
}
【NestedCallTarget】目标对象 ( 内部方法method1() 调用内部方法method2() )
public class NestedCallTarget {
public void method1() {
System.out.println("NestedCallTarget#method1() 方法被调用");
method2();
}
public void method2() {
System.out.println("NestedCallTarget#method2() 方法被调用");
}
}
【4.2】解决方法
1)目标对象内部方法method1() 调用 method2()时, 不调用目标对象的method2()方法,而调用代理对象的 method2()方法 ;
class Target {
method1() {
proxy.method2();
}
}
2)目标对象如何获取代理对象proxy ; 通过调用 AopContext.currentProxy() 来实现, 且同时设置织入器的 exposeProxy属性为true ;
【NestedCallTargetWithHoldProxyMain】持有代理对象的目标对象内部方法调用main
public class NestedCallTargetWithHoldProxyMain {
public static void main(String[] args) {
NestedCallTargetWithHoldProxy target = new NestedCallTargetWithHoldProxy();
AspectJProxyFactory weaver = new AspectJProxyFactory(target);
weaver.setExposeProxy(true); // 设置为true ,AopContext.currentProxy() 才生效
weaver.addAspect(NestedCallAspect.class);
// 获取代理对象
Object proxy = weaver.getProxy();
((NestedCallTargetWithHoldProxy) proxy).method1();
}
}
【打印日志】
stopWatch.start()
NestedCallTarget#method1() 方法被调用
stopWatch.start()
NestedCallTarget#method2() 方法被调用
stopWatch.stop()
方法执行耗时6.5E-5
stopWatch.stop()
方法执行耗时0.0029408
【NestedCallTargetWithHoldProxy】持有代理对象的内嵌调用目标类
public class NestedCallTargetWithHoldProxy {
public void method1() {
System.out.println("NestedCallTarget#method1() 方法被调用");
NestedCallTargetWithHoldProxy proxy = (NestedCallTargetWithHoldProxy) AopContext.currentProxy();
proxy.method2();
}
public void method2() {
System.out.println("NestedCallTarget#method2() 方法被调用");
}
}
【补充】当然,我们可以注入 NestedCallTargetWithHoldProxy代理对象(依赖)到 NestedCallTargetWithHoldProxy目标对象中;