AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率
通俗点讲就是提供一个为一个业务实现提供切面注入的机制,通过这种方式,在业务运行中将定义好的切面通过切入点绑定到业务中,以实现将一些特殊的逻辑绑定到此业务中。
举个栗子,项目中有记录操作日志的需求、或者流程变更是记录变更履历,无非就是插表操作,很简单的一个save操作,都是一些记录日志或者其他辅助性的代码。一遍又一遍的重写和调用。不仅浪费了时间,又将项目变得更加的冗余,实在得不偿失。
所以就需要面向切面aop就出场了
要理解SpringBoot整合aop的实现,就必须先对面向切面实现的一些aop的名称有所了解
切面(Aspect)
:一个关注点的模块化。以注解@Aspect的形式放在类上方,声明一个切面。
连接点(Joinpoint)
:在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候都可以是连接点。
通知(Advice)
:通知增强,需要完成的工作叫做通知,就是你写的业务逻辑中需要比如事务、日志等先定义好,然后需要的地方再去用。
主要包括5个注解:Before,After,AfterReturning,AfterThrowing,Around。
@Before
:在切点方法之前执行。
@After
:在切点方法之后执行
@AfterReturning
:切点方法返回后执行
@AfterThrowing
:切点方法抛异常执行
@Around
:属于环绕增强,能控制切点执行前,执行后,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解
切点(Pointcut)
:其实就是筛选出的连接点,匹配连接点的断言,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点做为切点。如果说通知定义了切面的动作或者执行时机的话,切点则定义了执行的地点。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。
引入(Introduction)
:在不改变一个现有类代码的情况下,为该类添加属性和方法,可以在无需修改现有类的前提下,让它们具有新的行为和状态。其实就是把切面(也就是新方法属性:通知定义的)用到目标类中去。
目标对象(Target Object)
:被一个或者多个切面所通知的对象。也被称做被通知(adviced)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
AOP代理(AOP Proxy)
:AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
织入(Weaving)
:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
其中重要的名词有:切面(Aspect)
,切入点(Pointcut)
以处理业务逻辑日志为例,新增日志处理的面向切面处理
「添加依赖」
org.springframework.boot
spring-boot-starter-aop
「定义日志注解」
/**
* @Description TODO
* 微信公众号:云说Java、Java栈记
* @Date 2021/02/22 22:09
* @Created by 墨云
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
}
「定义日志实体类」
/**
* @Description TODO
* 微信公众号:云说Java、Java栈记
* @Date 2021/2/28 23:07
* @Created by moyun
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("com_sys_log")
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String className;
private String methodName;
private String params;
private Long excTime;
private String remark;
private String createDate;
}
「定义日志切面」
/**
* @Description TODO
* 微信公众号:云说Java、Java栈记
* @Date 2021/2/28 23:19
* @Created by moyun
*/
@Aspect //使用@Aspect
@Component
public class SysLogAsp {
@Autowired
private SysLogService sysLogService;
/**
*定义切点, 这里我们使用注解的形式
* 当然,我们也可以通过切点表达式直接指定需要拦截的package,需要拦截的class 以及 method
* 切点表达式: execution(...)
*/
@Pointcut("@annotation(com.kjyfx.annotation.SysLog)")
public void logPointCut() {}
/**
* 添加环绕通知@Around
* @param point
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = point.proceed();
long time = System.currentTimeMillis() - beginTime;
try {
//实现保存日志逻辑
saveLog(point, time);
} catch (Exception e) {
}
return result;
}
/**
* 保存日志
* @param joinPoint
* @param time
*/
private void saveLog(ProceedingJoinPoint joinPoint, long time) {
// 获取方法的关键信息,类,包
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
SysLogEntity sysLogEntity = new SysLogEntity();
sysLogEntity.setExcTime(time);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
sysLogEntity.setCreateDate(dateFormat.format(new Date()));
SysLog sysLog = method.getAnnotation(SysLog.class);
if(sysLog != null) {
//注解上的描述
sysLogEntity.setRemark(sysLog.value());
}
//请求的 类名、方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
sysLogEntity.setClassName(className);
sysLogEntity.setMethodName(methodName);
//请求的参数
Object[] args = joinPoint.getArgs();
try {
List list = new ArrayList();
for (Object o : args) {
list.add(o.toString());
}
sysLogEntity.setParams(list.toString());
} catch (Exception e){
}
sysLogService.save(sysLogEntity);
}
}
控制类添方法上添加@SysLog
注解
/**
* @Description TODO
* 微信公众号:云说Java、Java栈记
* @Date 2021/2/28 23:07
* @Created by moyun
*/
@RestController
@RequestMapping("/sysUser")
public class SysUserController {
@Autowired
private SysUserService userService;
@RequestMapping("/getUserById/{id}")
@SysLog("根据Id获取用户")
public BaseResponse getUserById(@PathVariable Long id){
SysUser user = userService.getById(id);
return BaseResponse.renderSuccess("成功!",user);
}
}
此时查看数据库系统日志表,则会看到对应的日志信息
定义切入点的时候需要一个包含名字和任意参数的签名,还有一个切入点表达式,如execution(public * com.example.aop...(..))
切入点表达式的格式:execution([可见性]返回类型[声明类型].方法名(参数)[异常])
其中[]内的是可选的,其它的还支持通配符的使用:
*:匹配所有字符
..:一般用于匹配多个包,多个参数
+:表示类及其子类
运算符有:&&,||,!
「切入点表达式关键词用例」execution
:用于匹配子表达式。//匹配com.cjm.model包及其子包中所有类中的所有方法,返回类型任意,方法参数任意 @Pointcut(“execution(* com.cjm.model...(..))”) public void before(){}
within
:用于匹配连接点所在的Java类或者包。//匹配Person类中的所有方法 @Pointcut(“within(com.cjm.model.Person)”) public void before(){} //匹配com.cjm包及其子包中所有类中的所有方法 @Pointcut(“within(com.cjm..*)”) public void before(){}
this
:用于向通知方法中传入代理对象的引用。@Before(“before() && this(proxy)”) public void beforeAdvide(JoinPoint point, Object proxy){ //处理逻辑 }
target
:用于向通知方法中传入目标对象的引用。@Before(“before() && target(target) public void beforeAdvide(JoinPoint point, Object proxy){ //处理逻辑 }
args
:用于将参数传入到通知方法中。@Before(“before() && args(age,username)”) public void beforeAdvide(JoinPoint point, int age, String username){ //处理逻辑 }
@within
:用于匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配。@Pointcut(“@within(com.cjm.annotation.AdviceAnnotation)”) - 所有被@AdviceAnnotation标注的类都将匹配 public void before(){}
@target
:和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。@Pointcut(“@target(com.cjm.annotation.AdviceAnnotation)”) public void before(){}
@args
:传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注。@Before(“@args(com.cjm.annotation.AdviceAnnotation)”) public void beforeAdvide(JoinPoint point){ //处理逻辑 }
@annotation
:匹配连接点被它参数指定的Annotation注解的方法。也就是说,所有被指定注解标注的方法都将匹配。@Pointcut(“@annotation(com.cjm.annotation.AdviceAnnotation)”) public void before(){}
bean
:通过受管Bean的名字来限定连接点所在的Bean。该关键词是Spring2.5新增的。@Pointcut(“bean(person)”) public void before(){}