之前写过一篇Spring面向切面编程的具体操作:三种方式配置通知,当然也只是停留在操作层面,今天回头看这个知识点的时候,发现自己的理解更加深刻,故在此做一点小小的总结。
AOP面向切面编程是spring的核心之一,它的一些术语还是比较抽象的,至少初始的时候我是这么觉得的,但慢慢接触了一些设计思想,如代理模式创建实现相同接口的代理对象,以增强指定方法的思想之后,就渐渐理解其中的精妙,当然,理解还是不能完全理解的,只能说慢慢探索,日益精进。
一、简单案例的理解
面向切面编程的思想被广泛应用一定有他的道理,一定是因为它的出现解决了某些繁杂的类似于搬砖似的工作。
我们以一个简单案例作为切入,请暂时不要在意其中逻辑,暂时以打印日志信息作为事务控制:
首先,我们定义一个账户接口AccountService
,里面包含一些基本的增删改方法,并创建一个实现类AccountServiceImpl
实现之,暂且以打印信息模拟数据库操作。
@Service("accountService")
public class AccountServiceImpl implements AccountService {
public void saveAccount() {
System.out.println("==> 正常业务:AccountServiceImpl的saveAccount方法正常执行");
}
public void updateAccount(int i) {
System.out.println("==> 正常业务:AccountServiceImpl的updateAccount方法正常执行");
}
public int deleteAccount() {
System.out.println("==> 正常业务:AccountServiceImpl的deleteAccount方法正常执行");
return 10;
}
}
需求:在每个方法执行前后都打印日志信息,如果发生异常,打印异常信息。
呃,需求还是很好实现的,随便一想就有俩可以实现这个简单的需求:
- 直接在方法里面打印信息嘛,所有方法都写上一遍,不怕累,但日志代码大量侵入正常业务功能模块,存在大量耦合,显然不可取。
- 使用动态代理技术,基于JDK的动态代理技术,创建出与被代理对象实现相同接口的代理对象,在反射调用方法前后对方法进行增强,比如打印必要的日志信息。
于是我们果断采用动态代理的技术,对需求进行实现,并进行了测试:
public class aopTest {
public static void main(String[] args) {
//获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//获取对象
final AccountService as = ac.getBean(AccountService.class);
AccountService asProxy = (AccountService)Proxy.newProxyInstance(as.getClass().getClassLoader(), as.getClass().getInterfaces(), new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value = null;
//获取方法名
String name = method.getName();
try {
System.out.println(name+"方法 ==>即将执行...");
value = method.invoke(as, args);
System.out.println(name+"方法 ==>环绕返回通知... 返回结果 ==>"+value);
} catch (Throwable e) {
System.out.println(name+"方法 ==>环绕异常通知... 异常信息 ==>"+e);
} finally {
System.out.println(name+"方法 ==>最终执行完毕...");
}
return value;
}
});
//执行方法
asProxy.deleteAccount();
System.out.println("================");
asProxy.saveAccount();
}
}
可以发现动态代理可以实现我们的需求,但JDK的动态代理只能基于接口进行,如果要基于实现类,可以利用第三方库cglib实现,在此就不赘述了。
ok,说到这,我们成功地使日志代码动态地在目标业务方法的前后执行,我们的业务代码仅仅只需要关注业务自身逻辑,而日志信息,事务控制等代码转移至切面中即可,其中的合理性也是显而易见的。
二、SpringAOP的简单构建
spring框架对AOP的支持构建在动态代理的基础之上,当然也只是支持仅限于方法的拦截。那么,如何来构建呢,关于构建,我在上一篇基于操作的文章中已经写明,这边就选择其中一种,基于xml+注解的方式。
一、首先引入必要的jar包座标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
二、定义切面类@Aspect注解标注,并让spring管理,定义通知和切点:
ps:后置通知和返回通知中文翻译上可能会有偏差,以英文语义为准。
/**
* @author Summerday
*
* 记录日志工具类(切面类)
*/
@Component
@Aspect
public class Logger {
//提取可重用切入点表达式
@Pointcut("execution(* com.smday.service.impl.*.*(..))")
private void pt1(){}
/**
* 用于打印日志:计划让其在切入点方法执行之前执行(切入点方法就是业务层方法)
* 可以通过JoinPoint获取目标方法的详细信息
*/
@Before("pt1()")
public void printBeforeLog(JoinPoint joinPoint){
//目标方法运行时的参数
Object[] args = joinPoint.getArgs();
//获取方法签名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>前置通知...");
}
@After("pt1()")
public void printAfterLog(JoinPoint joinPoint){
//获取方法签名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>后置通知...");
}
//可以指定返回值
@AfterReturning(value = "pt1()",returning = "result")
public void printAfterReturningLog(JoinPoint joinPoint,Object result){
//获取方法签名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>返回通知... 返回结果 ==>"+result);
}
//可以指定异常
@AfterThrowing(value = "pt1()",throwing = "e")
public void printAfterThrowingLog(JoinPoint joinPoint,Exception e){
//获取方法签名
String name = joinPoint.getSignature().getName();
System.out.println(name+"方法 ==>异常通知... 异常信息 ==>"+e.getCause());
}
//@Around("pt1()")
//只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint
public Object printAroundLog(ProceedingJoinPoint pjp){
//获取参数
Object[] args = pjp.getArgs();
//获取方法名
String name = pjp.getSignature().getName();
Object proceed = null;
try {
System.out.println(name+"方法 ==>环绕前置通知...");
//利用反射推进目标方法即可,即method.invoke(obj,args)
proceed = pjp.proceed(args);
System.out.println(name+"方法 ==>环绕返回通知... 返回结果 ==>"+proceed);
} catch (Throwable throwable) {
System.out.println(name+"方法 ==>环绕异常通知... 异常信息 ==>"+throwable);
} finally {
System.out.println(name+"方法 ==>环绕后置通知...");
}
//反射调用后的返回值一定返回出去
return proceed;
}
}
三、基于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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
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 http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.smday"/>
<!-- 启用AspectJ自动代理-->
<aop:aspectj-autoproxy/>
</beans>
四、测试通知
public class aopTest {
public static void main(String[] args) {
//获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//获取对象
AccountService as = ac.getBean(AccountService.class);
//执行方法
as.deleteAccount();
System.out.println("================");
as.saveAccount();
}
}
我们可以发现执行的顺序:依次为前置通知、方法正常执行、后置通知、返回通知。
三、AOP术语学习
学习aop,免不了学习各种新鲜的术语,结合我们之前的小案例,应该会容易理解的多。
-
切面(Aspect):也就是我们定义的专注于提供辅助功能的模块,比如安全管理,日志信息等。
-
连接点(JoinPoint):切面代码可以通过连接点切入到正常业务之中,图中每个方法的每个点都是连接点。
-
切入点(PointCut):一个切面不需要通知所有的连接点,而在连接点的基础之上增加切入的规则,选择需要增强的点,最终真正通知的点就是切入点。
-
通知方法(Advice):就是切面需要执行的工作,主要有五种通知:
- 前置通知Before:目标方法调用之前执行的通知。
- 后置通知After:目标方法完成之后,无论如何都会执行的通知。
- 返回通知AfterReturning:目标方法成功之后调用的通知。
- 异常通知AfterThrowing:目标方法抛出异常之后调用的通知。
- 环绕通知Around:可以看作前面四种通知的综合。
-
织入(Weaving):将切面应用到目标对象并创建代理对象的过程,SpringAOP选择再目标对象的运行期动态创建代理对象。
四、切入点表达式
上面提到:连接点增加切入规则就相当于定义了切入点,当然切入点表达式分为两种:within和execution,这里主要学习execution表达式。
-
写法:execution(访问修饰符 返回值 包名.包名……类名.方法名(参数列表))
-
例:
execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())
-
访问修饰符可以省略,返回值可以使用通配符*匹配。
-
包名也可以使用
*
匹配,数量代表包的层级,当前包可以使用..
标识,例如* *..AccountServiceImpl.saveAccount()
-
类名和方法名也都可以使用
*
匹配:* *..*.*()
-
参数列表使用
..
可以标识有无参数均可,且参数可为任意类型。
全通配写法:* ….*(…)
通常情况下,切入点应当设置再业务层实现类下的所有方法:* com.smday.service.impl.*.*(..)
。
五、SpringAOP总结
-
获取对象时,生成目标对象的代理对象。
-
根据切入点规则,匹配用户连接点,得到切入点。
-
当切入点被调用时,通过代理对象拦截。
-
由切面类中的指定的通知执行来进行增强。
Spring自动为目标对象生成代理对象,默认情况下,如果目标对象实现过接口,则采用java的动态代理机制,如果目标对象没有实现过接口,则采用cglib动态代理。
六、简单小实例
一、异常信息写入文件
@Component
@Aspect
public class ExceptionAspect {
private FileWriter writer = null;
{
try{
writer = new FileWriter("err.log");
}catch (IOException e){
e.printStackTrace();
}
}
@AfterThrowing(value = "execution(* com.smday.service.impl.*.*(..))",throwing = "t")
public void afterThrowing(JoinPoint joinPoint,Throwable t)throws Exception{
//获取类型信息
Class<?> aClass = joinPoint.getTarget().getClass();
//获取方法名
String name = joinPoint.getSignature().getName();
//获取异常信息
String msg = t.getMessage();
String err = "["+aClass+"] == ["+name+"] == ["+msg+"]";
writer.write(err);
writer.flush();
}
}
二、权限简单管理
- 自定义注解
/**
* 自定义权限注解
* @author Summerday
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authority {
String value();
}
- 定义切面
@Component
@Aspect
public class AuthorityAspect {
@Around("execution(* com.smday.service.impl.*.*(..))&&@annotation(authority)")
public Object around(ProceedingJoinPoint pjp, Authority authority) throws Throwable {
//获取方法注解定义权限
String value = authority.value();
//方法名
String name = pjp.getSignature().getName();
//权限列表
List<String> authorityList = AopTest.getAuthorityList();
System.out.println("当前用户拥有的权限列表为:"+ authorityList);
Object proceed = null;
if(authorityList.contains(value)){
System.out.println("==> ["+name+"]方法已拥有权限...");
proceed = pjp.proceed();
}else {
System.out.println("==> ["+name+"]方法并没有权限...");
}
return proceed;
}
}
- 测试
public class AopTest {
private static final ThreadLocal<List<String>> AuthorityList = new ThreadLocal<List<String>>();
static {
List<String> list = new ArrayList<String>();
list.add("delete");
AuthorityList.set(list);
}
public static List<String> getAuthorityList() {
return AuthorityList.get();
}
public static void main(String[] args) {
//获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//获取对象
final AccountService as = ac.getBean(AccountService.class);
as.deleteAccount();
System.out.println("=====================================");
as.saveAccount();
}
}