【Spring】(1)浅谈AOP
(1)认识OOP(面向对象,竖向抽取)
OOP=面向对象编程
在OOP中,我们将两个类中相同的部分抽取出来,形成父类,这两个类再继承这个父类。从而实现了消除冗余代码。
(2)认识AOP(面向切面,横向抽取)
AOP是啥?
AOP(Aspect Orient Programming)也就是面向切面编程,AOP是OOP的补充,OOP是将重复的属性或方法抽取出来,AOP是将方法内的重复东西抽取出来。
所以AOP有两个任务:将冗余代码抽取出来,将抽取出来的代码嵌入到原代码中,且不影响功能。
AOP原理(动态代理)
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
AOP有什么用?
AOP可以分离系统的业务逻辑和系统服务(日志,安全等),原理是使用了代理模式
在日常的软件开发中,拿日志来说,一个系统软件的开发都是必须进行日志记录的,不然万一系统出现什么bug,你都不知道是哪里出了问题。举个小栗子,当你开发一个登陆功能,你可能需要在用户登陆前后进行权限校验并将校验信息(用户名,密码,请求登陆时间,ip地址等)记录在日志文件中,当用户登录进来之后,当他访问某个其他功能时,也需要进行合法性校验。想想看,当系统非常地庞大,系统中专门进行权限验证的代码是非常多的,而且非常地散乱,我们就想能不能将这些权限校验、日志记录等非业务逻辑功能的部分独立拆分开,并且在系统运行时需要的地方(连接点)进行动态插入运行,不需要的时候就不理,因此AOP是能够解决这种状况的思想
总结一下AOP的定义
aop是将那些与业务无关,却为业务模块所共同调用的逻辑或责任进行封装,从而减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作和可维护性。我们常用于实现事务、日志等功能中。
Spring中对AOP的支持
首先AOP思想的实现一般都是基于代理模式,在JAVA中一般采用JDK动态代理模式,但是我们都知道,JDK动态代理模式只能代理接口,如果要代理类那么就不行了。因此,Spring AOP 会这样子来进行切换,因为Spring AOP 同时支持 CGLIB、ASPECTJ、JDK动态代理,当你的真实对象有实现接口时,Spring AOP会默认采用JDK动态代理,否则采用cglib代理。
- 如果目标对象的实现类实现了接口,Spring AOP 将会采用 JDK 动态代理来生成 AOP 代理类;
- 如果目标对象的实现类没有实现接口,Spring AOP 将会采用 CGLIB 来生成 AOP 代理类——不过这个选择过程对开发者完全透明、开发者也无需关心。
AOP常见术语
- 【连接点JoinPoint】:指的是那些被拦截的点(业务层Service所有的方法都是连接点)。在spring中的连接点指的就是业务层service中的方法,因为spring只支持方法类型的连接点。这些方法(连接点)用来连接我们的业务和要增强的方法,例如给业务层里的所有方法加上“事务控制”
- 【切点Pointcut】:指的是我们要对哪些Jointpoint进行拦截的定义。所有被增强的方法都是切入点,但是没有被增强的方法不是切入点
- 【增强Advice】:指的是拦截到JoinPoint之后要做的事情就是通知,通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知
- 【目标对象Target】:代理的目标对象
- 【引介Introduction】:(了解即可)引介是一种特殊的通知在不修改类代码的前提下,Introduction可以在运行器为类动态地添加一些方法或域Filed
- 【织入Weaving】:是指把增强应用到目标对象来创建新的代理对象的过程,spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入
- 【代理对象Proxy】:一个类被AOP织入增强后,就产生一个结果代理类
- 【切面Aspect】:是切入点和通知(引介)的结合
注意:所有的切入点都是连接点,但有点连接点不是切入点(没有增强的方法是连接点,但不是切入点)
(3)AOP的案例代码(注解方式)
通知(Advice)用到的注解:
- Before 在方法被调用之前调用
- After 在方法完成后调用通知,无论方法是否执行成功
- After-returning 在方法成功执行之后调用通知
- After-throwing 在方法抛出异常后调用通知
- Around 通知了好、包含了被通知的方法,在被通知的方法调用之前后调用之后执行自定义的行为
给转账添加日志
(3.1)业务层Service的接口
public interface IAccountService {
void saveAccount();
void updateAccount(int i);
void deleteAccount();
}
(3.2)业务层Service实现类
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
// 2-在业务层实现类的方法中,使用持久层的对象调用持久层接口中的方法,并且返回值给业务层
@Override
public void saveAccount() {
System.out.println("执行了保存...");
}
@Override
public void updateAccount(int i) {
System.out.println("执行了更新..."+i);
}
@Override
public void deleteAccount() {
int i=1/0;
System.out.println("执行了删除...");
}
}
(3.3)记录日志的工具类
(3.3.1)
@Component("logger")//把Logger类的对象放进IOC容器
@Aspect//表示当前类是一个切面类
public class Logger {
//被拦截的方法是连接点(拦截了Service实现类里的所有方法)
//被拦截并被增强的方法是切入点
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void ptl(){}
//在ptl执行之前通知
@Before("ptl()")
public void beforePrintLog() {
System.out.println("要开始执行了,前置通知:Logger类中的beforePrintLog方法开始记录日志了...");
}
//ptl执行之后通知
@AfterReturning("ptl()")
public void afterReturningPrintLog() {
System.out.println("方法返回结果了,后置通知:Logger类中的afterReturningPrintLog方法开始记录日志了...");
}
//ptl抛异常后通知
@AfterThrowing("ptl()")
public void afterThrowingPrintLog() {
System.out.println("方法抛出异常了,异常通知:Logger类中的afterThrowingPrintLog方法开始记录日志了...");
}
//ptl执行结束后通知
@After("ptl()")
public void afterPrintLog() {
System.out.println("方法执行结束了,最终通知:Logger类中的afterPrintLog方法开始记录日志了...");
}
}
测试结果如图:
(3.3.2)环绕通知
spring框架为我们提供了一个接口–ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法,是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
@Component("logger")//把Logger类的对象放进IOC容器
@Aspect//表示当前类是一个切面类
public class Logger {
//被拦截的方法是连接点(拦截了Service实现类里的所有方法)
//被拦截并被增强的方法是切入点
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void ptl(){}
/**
* 环绕通知
* 问题:当我们配置了环绕配置之后,切入点方法没有执行,只执行了环绕通知的方法
* 分析:通过对此动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码中没有
* 解决:spring框架为我们提供了一个接口--ProceedingJoinPoint。
* 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法
*
* spring中的环绕通知
* 是spring框架为我们提供的一种可以在代码中手动控制增强方法何时执行的方式
*
* @MethodName: arroundPrintLog
* @Author: AllenSun
* @Date: 2019/8/30 0:39
*/
@Around("ptl()")
public Object arroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
//得到方法执行所需的参数,准备开始执行了
Object[] objects = pjp.getArgs();
System.out.println("开始执行方法了,前置通知(在proceed之前)---环绕通知:Logger类中的arroundPrintLog方法开始记录日志了...");
//明确调用业务层方法(切入点方法)
pjp.proceed(objects);
System.out.println("方法执行结束了,后置通知(在proceed之后)---环绕通知:Logger类中的arroundPrintLog方法开始记录日志了...");
return rtValue;
} catch (Throwable throwable) {
System.out.println("方法抛出异常了,异常通知(在catch之中)---环绕通知:Logger类中的arroundPrintLog方法开始记录日志了...");
throw new RuntimeException(throwable);
} finally {
System.out.println("方法返回结果了,最终通知(在finally之中)---环绕通知:Logger类中的arroundPrintLog方法开始记录日志了...");
}
}
}
测试结果如图:
(3.4)测试类Junit
@RunWith(SpringJUnit4ClassRunner.class)//使用Junit提供的注解把原来的main方法替换了,换成spring提供的RunWith
@ContextConfiguration("classpath:applicationContext.xml")//找到IOC容器的位置,获取容器
public class AccountServicesTest {
@Autowired
private IAccountService as;
@Test
public void testAroundPrintLog(){
//3-执行方法
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}
(4)实际使用案例
(1)场景描述:xxljob在跑调度任务的时候,是不能从会话中获取当前用户信息的,而业务逻辑中需要对用户进行权限校验。所以我们需要给会话里添加了一个默认的用户信息。
(2)实现方案:最简单的方案就是直接在调度任务的方法里直接写代码来实现。但是如果以后还要添加很多其他的调度任务怎么办,每加一个都要写一遍这个逻辑,其实这个逻辑并不是主要的业务代码,这种实现方式代码侵入强,不合适。另一种方案就是使用切面对xxljob的方法实现切面拦截,通过代理对拦截的方法都进行包装加工,统一实现添加用户信息的效果。
这样的好处就是代码没有侵入,且新增调度任务的时候自动实现这个功能。
(3)实现代码
@Component
@Aspect
@Slf4j
public class XxljobAspect {
@Value("${scheduler.user_id}")
private Long userId;
@Value("${scheduler.tenant_id}")
private Long tenantId;
@Value("${scheduler.user_name}")
private String userName;
@Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
public void aopPointcut() {
}
@Around("aopPointcut()")
public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
ContextMap userContextMap = ContextMap.getContext(true);
if (userContextMap.getUserSession(false) == null) {
UserInfo userInfo = new UserInfo();
userInfo.setId(userId);
userInfo.setUserName(userName);
userInfo.setTenantId(tenantId);
YtUserSession ytUserSession = new YtUserSession(null,userInfo,null);
userContextMap.setUserSession(ytUserSession);
ContextMap.setContext(userContextMap);
}
Object result;
try {
LogMDCUtil.setTraceId();
result = joinPoint.proceed();
} finally {
LogMDCUtil.clear();
}
//返回结果
return result;
}
}
(5)总结一下
本来Service类中的方法有三个,分别是转账,保存,删除。但是使用AOP进行操作后,这三个方法实现了方法增强,多了输出日志的功能,这就是把通用的方法进行了横向抽取,然后再织入切入点。