对于一个业务而言,我们如何在不修改源代码的基础上对对象功能进行拓展,例如现有一个公告(通知)业务接口及实现:
public interface NoticeService {
int deleteById(Integer... ids);
}
public class NoticeServiceImpl implements NoticeService {
@Override
public int deleteById(Integer... ids) {
System.out.println(Arrays.toString(ids));
return 0;
}
}
需求:基于OCP(开闭原则-对扩展开放对修改关闭)设计原则对NoticeServiceImpl类的功能进行扩展,例如在deleteById业务方法执行之前和之后输出一下系统时间.
方案1:基于继承方式实现其功能扩展,关键设计如下:
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);
}
}
这种基于继承方式实现功能扩展,代码简单,容易理解,但是不够灵活,耦合性比较强。
方案2:基于组合方式实现其功能扩展,关键代码设计如下:
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);
}
}
基于组合方式实现功能扩展,代码比较灵活,耦合低,稳定性强,但理解相对比较困难。
AOP(Aspect Orient Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(OOP)的一种补充和完善。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面理解为一个动态过程(在对象运行时动态织入一些扩展功能或控制对象执行)。如图所示:图片:
AOP 与 OOP 字面意思相近,但其实两者完全是面向不同领域的设计思想。实际项目中我们通常将面向对象理解为一个静态过程(例如一个系统有多少个模块,一个模块有哪些对象,对象有哪些属性),面向切面的运行期代理方式,理解为一个动态过程,可以在对象运行时动态织入一些扩展功能或控制对象执行。
AOP可以在系统启动时为目标类型创建子类或兄弟类型对象,这样的对象我们通常会称之为动态代理对象.如图所示:图片:
其中,为目标类型(XxxServiceImpl)创建其代理对象方式有两种(先了解):
@Aspect:作用是把当前类标识为一个切面供容器读取
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,
一是表达式,二是方法签名。方法签名必须是 public及void型。
可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,
因此我们可以通过方法签名的方式为 此表达式命名。
因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行
在项目中定义一个日志切面,通过切面中的通知方法为目标业务对象做日志功能增强。
添加AOP依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
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();
}
第二步:创建切面对象,用于做日志业务增强,关键代码如下:
package com.cy.pj.sys.service.aspect;
import com.cy.pj.common.annotation.RequiredLog;
import com.cy.pj.sys.pojo.SysLog;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@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;
}
}
说明:
@annotaion
表达式应用于方法级别,实现细粒度的切入点表达式定义
-@annotation(anno.RequiredLog)
匹配有此注解描述的方法。
其中:RequiredLog
为我们自己定义的注解,当我们使用@RequiredLog
注解修饰业务层方法时,系统底层会在执行此方法时进行日志扩展操作。
第三步:通过注解RequiredLog注解描述日志查询或删除业务相关方法,此时这个方法为日志切入点方法,例如:
@RequiredLog(operation="公告查询")
@Override
public List<SysLog> findLogs(SysLog sysLog) {
List<SysLog> list=syslogDao.selectLogs(sysLog);
return list;
}
Spring框架AOP模块定义通知类型,有如下几种:
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低。