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);
} }
结论:基于组合方式实现功能扩展,代码比较灵活,耦合低,稳定性强,但理解相对比较困
难。 总之,无论是继承, 还是组合都是基于 OCP 方式实现了对象功能扩展 , 都有相应的优缺点 , 并 且我们都要自己去写这些子类或兄弟类,对于这样的模板代码我们能否进行简化呢 ?aop就实现了对象和两种方式的优化。 实际项目中我们通常将面向对象理解 为一个静态过程 ( 例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性 ) ,面 向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)。如图 -1 所示 实现原理: AOP 可以在系统启动时为目标类型创建子类或兄弟类型对象 , 这样的对象我们通常会称 之为动态代理对象 . 如图所示 :其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种:
- 第一种方式 : 借助 JDK 官方 API 为目标对象类型创建其兄弟类型对象 , 但是目标对象类 型需要实现相应接口。
-
第二种方式
:
借助
CGLIB
库为目标对象类型创建其子类类型对象
,
但是目标对象类型不
能使用
final
修饰
.
注意:spring中默认使用jdk代理,如果目标对象没有实现接口会换成CGLIB代理方式;spring boot默认使用CGLIB方式代理 。
相关术语分析
- 切面 (aspect): 横切面对象 , 一般为一个具体类对象。、
- 切入点 (pointcut): 定义了切入扩展业务逻辑的位置 ( 哪些方法运行时切入扩展业务 ), 一 般会通过表达式进行相关定义 , 一个切面中可以定义多个切入点。
- 通知 (Advice): 内部封装扩展业务逻辑的具体方法对象 , 一个切面中可以有多个通知 ( 在 切面的某个特定位置上执行的动作 ( 扩展功能 ) 。
-
连接点
(joinpoint):
程序执行过程中,封装了某个正在执行的目标方法信息的对象
,
可以
通过此对象获取具体的目标方法信息
,
甚至去调用目标方法。连接点与切入点定义如图
所示:
说明:我们可以简单的将机场的一个安检口理解为连接点,多个安检口为切入点,安全 检查过程看成是 通知。总之,概念很晦涩难懂,多做例子,做完就会清晰。先可以按白话去理解。
org.springframework.boot spring-boot-starter-aop
说明:基于此依赖 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(com.cy.pj.common.annotation.RequiredLog)")
public void doLog(){}//此方法只负责承载切入点的定义
@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
注解描述通告业务
(@Service)
相关方法,此时这个方法为
日志切入点方法,例如:
@RequiredLog(operation="公告查询") @Override public ListfindNotices(SysNotice notice) { //log.debug("start: {}",System.currentTimeMillis()); List 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(com.cy.pj.common.annotation.RequiredLog)")
public void doLog(){}//此方法只负责承载切入点的定义
@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 注解进行描述,数字越小优先级越高,默认优先级比较 低。例如:@Order(1) @Aspect @Component public class SysLogAspect { … }
定义缓存切面并指定优先级:
@Order(2) @Aspect @Component public class SysCacheAspect { ... }说明:当多个切面作用于同一个目标对象方法时,这些切面会构建成一个切面链,类似过滤 器链、拦截器链,其执行分析如图所示:
注意:spring中提供的缓存在于第三方分页框架进行整合使用时,@Cacheable(cacheNames = "lk"),该注解如果放在service层查询结果时,第一次正常,第二次会发现没有数据,原因应该是拦截器的执行顺序问题...
分页底层会在sql访问数据前将原来sql语句进行拦截,而缓存是在第一次访问数据库后将返回的结果放到缓存区中一份,下次如果访问的是同一个方法,就从缓存区中取。如果将缓存置于service层的话,用户发送请求调用方法会先执行分页,所以这个时候数据没有被放到缓存中去,缓存中是空的,这就会导致页面第二次查询无结果。



