简短的前言
AOP是面向切面编程思想,用于日志处理,事务处理,权限处理,缓存处理等等(是基于OCP[开闭原则]在不改变原有代码的情况下,对现有功能进行功能扩展)。
注:如果没有AOP,又要基于OCP原则实现功能扩展有两种方式
1.通过继承方式,原有功能不变,在子类增加特性功能扩展。
2.通过组合方式,对业务层实现类的接口增加特性功能扩展。
AOP底层两种原理
1.被代理对象如果实现接口,底层默认采用JDK动态代理机制为目标对象创建代理对象(目标类和代理类会实现共同接口)。
2.被代理对象如果没有实现接口,则底层默认采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)。
说明:
Spring boot2.x 中AOP现在默认使用的CGLIB代理,假如需要使用JDK动态代理可以在配置文件(applicatiion.properties)中进行如下配置:
spring.aop.proxy-target-class=false
1.添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.创建日志切面处理类
/**
* @Aspect 注解修饰的类通常认为一个切面对象类型
* 切面对象是对扩展业务的封装,它通常会在内部声明
* 如下几个部分.
* 1)实现扩展业务的方法(一般会称为通知-advice)
* 2)切入扩展业务的点(一般会称为切入点-PointCut)
*/
//@Order(1)
@Aspect
@Service
public class SysLogAspect {//日志切面
/**
* @Around注解修饰方法为一个环绕通知,其目的
* 是在目标业务方法执行之前和之后都可以进行
* 扩展业务的处理
* 其中:
* 1)bean(sysUserServiceImpl) 为切入点表达式,
* 表示sysUserServiceImpl对象中所有业务方法执行
* 时都会执行@Around注解修饰的方法
* @param jp 连接点(封装了要执行的目标方法信息)
* @return
* @throws Throwable
*/
//@Around("bean(sysUserServiceImpl)")
//@Around("bean(*ServiceImpl)")
//@annotation()为细粒度的切入点表达式定义方式
@Around("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public Object aroundMethod(ProceedingJoinPoint jp)
throws Throwable{
System.out.println("LogAspect:开始记录日志");
//1.目标业务执行之前的记录
long t1=System.currentTimeMillis();
//2.执行目标业务(底层通过反射执行目标方法)
Object result=jp.proceed();
//3.目标业务执行之后的记录
long t2=System.currentTimeMillis();
System.out.println("目标业务执行时长:"+(t2-t1));
saveObject(jp,(t2-t1));
//4.返回目标业务的执行结果
return result;
}
@Autowired
private SysLogDao sysLogDao;
private void saveObject(ProceedingJoinPoint jp,long time)throws Exception {
//1.获取要保存的日志信息
//1.1获取登陆用户(没问题)
SysUser user=(SysUser)SecurityUtils.getSubject().getPrincipal();
//1.2获取方法签名(此对象封装了我们要执行的目标方法信息)
Signature s=jp.getSignature();
System.out.println(s.getClass().getName());//MethodSignature
MethodSignature ms=(MethodSignature)s;
//1.2.1获取目标对象(要执行的业务层对象)
Class<?> targetClass=jp.getTarget().getClass();
//1.2.2基于目标业务对象获取要执行的目标方法
//?思考(为什么要获取此方法呢)
Method targetMethod=targetClass.getDeclaredMethod(
ms.getName(),
ms.getParameterTypes());
//1.2.3获取方法上定义的注解内容(定义的操作名)
RequiredLog requiredLog=
targetMethod.getDeclaredAnnotation(RequiredLog.class);
String operation=requiredLog.value();
//1.2.4获取目标对象方法的全称(类全名+方法名)
String targetClassName=targetClass.getName();
String targetMethodName=targetClassName+"."+targetMethod.getName();
//1.3获取方法执行时的实际参数
String params=Arrays.toString(jp.getArgs());
//2.封装日志信息
SysLog log=new SysLog();
log.setUsername(user.getUsername());
log.setIp(IPUtils.getIpAddr());
log.setOperation(operation);
log.setMethod(targetMethodName);
log.setParams(params);
log.setTime(time);
log.setCreatedTime(new Date());
//3.将日志信息写入到数据库
sysLogDao.insertObject(log);
}
}
代码内注解的总结:
@Aspect 注解用于标识此类为一个AOP横切面对象
@Pointcut 注解用于定义本类中的切入点,本案例中切入点表达式用的是bean表达式,这个表达式以bean开头,bean括号中的内容为一个spring管理的某个bean对象的id。
@Around用于定义一个环绕通知(满足切入点表达式的核心业务方法执行之前和之后执行的一个操作)
术语增强:
切面:用于封装扩展业务的一个类的对象。
通知:切面扩展业务中的一个操作(方法)。
切面表达式
指示符 作用
bean 应用于类级别,实现粗粒度的控制:(用于匹配指定bean id的的方法执行)
bean(“userServiceImpl”)) | 指定userServiceImpl类中所有方法 |
---|---|
bean("*ServiceImpl") | 指定所有的后缀为serviceImpl的类的所有方法 |
说明:bean表达式内部的对象是由spring容器管理的一个bean对象,表达式内部的内部的名字应该时spring容器中某个bean的key.
within 粗粒度的控制:用于匹配指定包名下类型内的方法执行
within(“aop.service.UserServiceImpl”) | 指定aop.service目录下的UserServiceImpl类中的方法 |
---|---|
within(“aop.service.*”) | 指定aop.service目录下的类中的方法(只包括当前目录下的类) |
within(“aop.service…*”) | 指定aop.service目录包含所有子目录中的类 |
execution 细粒度的控制:用于进行细粒度方法匹配执行具体业务
|
execution(void aop.service.UserServiceImpl.addUser()) | 只匹配方法 |
---|---|
execution(void aop.service.PersonServiceImpl.addUser(String)) | 方法参数必须为字符串 |
|execution(* aop.service….(…)) |万能配置
@annotation 细粒度的控制:用于匹配指定注解修饰的方法执行 RequestLog为自定义的注解
|
@annotation(com.jt.common.anno.RequestLog) | 指定一个需要实现增强功能的方法 |
---|
AOP中的五种通知类型
1)前置通知 (@Before) 方法执行之前执行
2)返回通知 (@AfterReturning) 方法return之后执行
3)异常通知 (@AfterThrowing) 方法出现异常之后执行
4)后置通知 (@After) : 又称之为最终通知(finally)
5)环绕通知 (@Around) :重点掌握
其结构如下:
Try{
@Before
核心业务
@AfterReturning
}catch(Exception e){
….
@AfterThrowing
}finally{
….
@After
}
如上四个一起使用时可以直接使用@Around通知替换
五种通知类型案例分析
@Service
@Aspect
public class SysTimeAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doTime(){}
@Before("doTime()")
public void doBefore(JoinPoint jp){
System.out.println("time doBefore()");
}
@After("doTime()")
public void doAfter(){
System.out.println("time doAfter()");
}
/**核心业务正常结束时执行
* 说明:假如有after,先执行after,再执行returning*/
@AfterReturning("doTime()")
public void doAfterReturning(){
System.out.println("time doAfterReturning");
}
/**核心业务出现异常时执行
说明:假如有after,先执行after,再执行Throwing*/
@AfterThrowing("doTime()")
public void doAfterThrowing(){
System.out.println("time doAfterThrowing");
}
@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp)
throws Throwable{
System.out.println("doAround.before");
Object obj=jp.proceed();
System.out.println("doAround.after");
return obj;
}
}
其中:环绕通知的优先级最高.
切面执行顺序配置
注解方式顺序配置需要借助@Order注解
@Order(1)
@Aspect
@Component
public class TxManager {
…
}
注解方式顺序配置
@Order(2)
@Aspect
@Component
public class SysLogAspect {
…
}