1.Spring Aop是什么?
AOP(Aspect Oriented Programming面向切面编程),是一种设计思想,它是面向编程(oop)的一种完善,通过编译期或运行期动态代理的方式为目标对象进行业务功能的增强,也就是为目标对象进行功能的扩展,如果使用传统的方式进行功能扩展的话有两种方式:
方案一:基于继承方式实现其功能,设计如下:**
假如有一个公告(通知)业务接口及实现:
如何在不改变源码的前提下对对象进行功能扩展
pubic interface NoticeService{
int deleteById(Integer…ids);
}
public class NoticeServiceImpl implements NoticeService{
public int deleteById(Integer…ids){
System.out.println(Arrays.toString(ids));
return 0 ;
} }
需求:基于OCP(开闭原则-对扩展开放,对修改关闭)实际方式对NoticeServiceImpl类的功能进行扩展,例如在deleteById方法的前后输出以下系统时间
public int deleteById(Integer…ids){
System.out.println("Start:"+System.currentTimeMillis());
Int rows=super.deleteById(ids);
System.out.println("After:"+System.currentTimeMillis());
return rows;
} }
关键设计如下:
public class CglibLogNoticeService extends NoticeServiceImpl{
public int deleteById(Integer…ids){
System.out.println("Start:"+System.currentTimeMillis());
Int rows=super.deleteById(ids);
System.out.println("After:"+System.currentTimeMillis());
return rows;
} }
测试类如下:
public class NoticeServiceTests{
public static void main(String[] args){
NoticeService ns=new CglibLogNoticeService();
ns.deleteById(10,20,30);
} }
结论:基于继承方式实现功能扩展,代码简单,容易理解,但是不够灵活(java中只能单一继承,继承一个类就不能继承另一个类),耦合性太高。
方案二:基于组合方式实现功能扩展,代码如下:
public class JdkLogNoticeService implements NoticeService{
private NoticeService noticeService;//has a
public JdkLogNoticeService(NoticeService noticeService){
this.noticeService=noticeService;
}
public int deleteById(Integer…ids){
System.out.println("Start:"+System.currentTimeMillis());
int rows=this.noticeService.deleteById(ids);
System.out.println("After:"+System.currentTimeMillis());
return rows;
} }
测试类如下:
public class NoticeServiceTests{
public static void main(String[] args){
NoticeService ns=
new JdkLogNoticeService(new NoticeServiceImpl());
ns.deleteById(10,20);
} }
结论:基于组合方式实现功能扩展,代码比较灵活,耦合低,稳定性强,但理解相对比较困

实现原理:

其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种:
-
第一种方式 : 借助 JDK 官方 API 为目标对象类型创建其兄弟类型对象 , 但是目标对象类型需要实现相应接口。
-
第二种方式 : 借助 CGLIB 库为目标对象类型创建其子类类型对象 , 但是目标对象类型不能使用 final 修饰 .注意:spring中默认使用jdk代理,如果目标对象没有实现接口会换成CGLIB代理方式;spring boot默认使用CGLIB方式代理 。
相关术语分析
-
切面 (aspect): 横切面对象 , 一般为一个具体类对象。、
-
切入点 (pointcut): 定义了切入扩展业务逻辑的位置 ( 哪些方法运行时切入扩展业务 ), 一般会通过表达式进行相关定义 , 一个切面中可以定义多个切入点。
-
通知 (Advice): 内部封装扩展业务逻辑的具体方法对象 , 一个切面中可以有多个通知 ( 在切面的某个特定位置上执行的动作 ( 扩展功能 ) 。
-
连接点 (joinpoint): 程序执行过程中,封装了某个正在执行的目标方法信息的对象 , 可以通过此对象获取具体的目标方法信息 , 甚至去调用目标方法。连接点与切入点定义如图所示:说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全检查过程看成是 通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。
-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
说明:基于此依赖 spring 可以整合 AspectJ 框架快速完成 AOP 的基本实现。AspectJ 是一个面向切面的框架,他定义了 AOP 的一些语法,有一个专门的字节码生成器来生成遵守 java 规范的 class 文件。
业务切面对象设计
第一步:创建注解类型,应用于切入点表达式的定义,关键代码如下:
package com.cy.pj.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequiredLog {
String operation();
}
第二步:创建切面对象,用于做日志业务增强,关键代码如下:
@Aspect
@Component
public class SysLogAspect {
private static final Logger log=
LoggerFactory.getLogger(SysLogAspect.class);
/**
* @Pointcut 注解用于定义切入点
* @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
* 点方法
*/
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void doLog(){}//此方法只负责承载切入点的定义
/**
* @Around 注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
* @param jp 连接点对象,此对象封装了要执行的目标方法信息.
* 可以通过连接点对象调用目标方法
* @return 目标方法的执行结果
* @throws Throwable
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint jp)throws Throwable{
long t1=System.currentTimeMillis();
try {
//执行目标方法(切点方法中的某个方法)
Object result = jp.proceed();
long t2=System.currentTimeMillis();
log.info("opertime:{}",t2-t1);
return result;//目标业务方法的执行结果
}catch(Throwable e){
e.printStackTrace();
long t2=System.currentTimeMillis();
log.info("exception:{}",e.getMessage());
throw e;
}
}
@RequiredLog(operation="公告查询")
@Override
public List<SysNotice> findNotices(SysNotice notice) {
//log.debug("start: {}",System.currentTimeMillis());
List<SysNotice> list=sysNoticeDao.selectNotices(notice);
//log.debug("end: {}",System.currentTimeMillis());
return list; }
第四步:测试通知业务方法,并检测日志输出以及了解其运行原理,如图所示
成功后再来修改切面对象:
@Aspect
@Component
public class SysLogAspect {
private static final Logger log=
LoggerFactory.getLogger(SysLogAspect.class);
/**
* @Pointcut 注解用于定义切入点
* @annotation(注解)为切入点表达式,后续由此注解描述的方法为切入
* 点方法
*/
@Pointcut("@annotation(com.cy.pj.common.annotation.RequiredLog)")
public void doLog(){}//此方法只负责承载切入点的定义
/**
* @Around 注解描述的方法,可以在切入点执行之前和之后进行业务拓展,
* @param jp 连接点对象,此对象封装了要执行的目标方法信息.
* 可以通过连接点对象调用目标方法.
* @return 目标方法的执行结果
* @throws Throwable
*/
@Around("doLog()")
public Object doAround(ProceedingJoinPoint jp)throws Throwable{
long t1=System.currentTimeMillis();
log.info("Start:{}",t1);
try {
//执行目标方法(切点方法中的某个方法)
Object result = jp.proceed();
long t2=System.currentTimeMillis();
log.info("After:{}",t2);
doLogInfo(jp,t2-t1,null);
return result;//目标业务方法的执行结果
}catch(Throwable e){
e.printStackTrace();
long t2=System.currentTimeMillis();
doLogInfo(jp,t2-t1,e);
throw e;
}
}
//记录用户行为日志
13
private void doLogInfo(ProceedingJoinPoint jp,
long time,Throwable e)
throws Exception {
//1.获取用户行为日志
//1.1 获取登录用户名(没做登录时,可以先给个固定值)
String username="cgb";
//1.2 获取 ip 地址
String ip= "202.106.0.20";
//1.3 获取操作名(operation)-@RequiredLog 注解中 value 属性的值
//1.3.1 获取目标对象类型
Class<?> targetCls=jp.getTarget().getClass();
//1.3.2 获取目标方法
MethodSignature ms=
(MethodSignature) jp.getSignature();//方法签名
Method targetMethod=targetCls.getMethod(
ms.getName(),ms.getParameterTypes());
//1.3.3 获取方法上 RequiredLog 注解
RequiredLog annotation =
targetMethod.getAnnotation(RequiredLog.class);
//1.3.4 获取注解中定义操作名
String operation=annotation.operation();
//1.4 获取方法声明(类全名+方法名)
String classMethodName=
targetCls.getName()+"."+targetMethod.getName();
//1.5 获取方法实际参数信息
Object[]args=jp.getArgs();
String params=new ObjectMapper().writeValueAsString(args);
//2.封装用户行为日志
SysLog sysLog=new SysLog();
sysLog.setUsername(username);
sysLog.setIp(ip);
sysLog.setOperation(operation);
sysLog.setMethod(classMethodName);
sysLog.setParams(params);
sysLog.setTime(time);
if(e!=null) {
sysLog.setStatus(0);
sysLog.setError(e.getMessage());
}
//3.打印日志
String userLog=new ObjectMapper()
.writeValueAsString(sysLog);
log.info("user.oper {}",userLog);
} }
Spring AOP 技术进阶
通知类型
- @Around (优先级最高的通知,可以在目标方法执行之前,之后灵活进行业务拓展.)
- @Before (目标方法执行之前调用)
- @AfterReturning (目标方法正常结束时执行)
- @AfterThrowing (目标方法异常结束时执行)
- @After (目标方法结束时执行,正常结束和异常结束它都会执行)
切面执行顺序
@Order(1)@Aspect@Componentpublic class SysLogAspect {…}
定义缓存切面并指定优先级:
@Order(2)@Aspect@Componentpublic class SysCacheAspect {...}

注意:spring中提供的缓存在于第三方分页框架进行整合使用时,@Cacheable(cacheNames = "lk"),该注解如果放在service层查询结果时,第一次正常,第二次会发现没有数据,原因应该是拦截器的执行顺序问题...
分页底层会在sql访问数据前将原来sql语句进行拦截,而缓存是在第一次访问数据库后将返回的结果放到缓存区中一份,下次如果访问的是同一个方法,就从缓存区中取。如果将缓存置于service层的话,用户发送请求调用方法会先执行分页,所以这个时候数据没有被放到缓存中去,缓存中是空的,这就会导致页面第二次查询无结果。