如何通过自定义注解实现AOP切点定义

时间:2022-09-28 22:35:46

面向切面编程(Aspect Oriented Programming, AOP)是面向对象编程(Object Oriented Programming,OOP)的强大补充,通过横切面注入的方式引入其他额外功能,比如日志记录,事务处理等,用户无需修改源代码就可以"优雅"的实现额外功能的补充,对于Programmer来说,AOP是个非常强大的工具。

如何通过自定义注解实现AOP切点定义

AOP中的切面处理逻辑会被应用到我们所定义的切点(Point Cut)上,切面逻辑定义可以使用 around, before,after等Aspect注解实现,切点可以使用Aspect注解中的参数指定或者通过xml配置文件声明。在编写代码的过程中,切点控制往往不够灵活,需要我们在xml或者Aspect注解参数中指定方法的path,当切点较多,需要颗粒度更加细致的切点控制时,通常我们需要添加大量的切点定义代码,这样比较麻烦。通常呢我们我们可以通过结合自定义注解来解决这个问题,实现更加灵活的切点控制。

自定义注解实现AOP切点定义的背后原理说起来其实很简单,通过扫描项目所有的类,然后过滤出标注点的位置,将切面自定义逻辑应用到标注点上,就实现了我们的业务需求。但是,技术上如何去实现呢?本文就这一问题,结合Java Spring AOP框架给出解答。

目标

我们的自定义注解需要具备以下功能:

  1. 类中方法添加注解,则这个方法成为切点
  2. 类添加注解,则这个类中所有的方法成为切点
  3. 抽象类方法添加注解,则抽闲类中的这个方法成为切点
  4. 抽象类添加注解,则抽象类中的所有方法成为切点
  5. 接口添加注解,则接口中定义的所有方法成为切点
  6. 接口中方法添加注解,则接口中的这个方法成为切点

JDK提供的关键类和方法

Class

Java最基本的元素称为"类",类中可以包涵方法和属性的定义。Class对象提供了很多有用的方法,可以帮助我们切点位置定位,比如:

  1. public native Class<? super T> getSuperclass();获取当前类的父类
  2. public Class<?>[] getInterfaces();获取当前类实现的接口
  3. public Method[] getDeclaredMethods() throws SecurityException 获取当前类中声明的方法
  4. public < A extends Annotation > A getAnnotation(Class < A > annotationClass)获取当前类指定标签的对象,若为空,表明当前类没有标签annotationClass。

Method

我们还使用到Method类的一些方法:

  1. public String getName(); 获取方法的名字
  2. public < T extends Annotation > T getAnnotation(Class annotationClass);取当前类指定标签的对象,若为空,表明当前类没有标签annotationClass

ProceedingJoinPoint

ProceedingJoinPoint接口提供了很多实用的函数,便于用户获取应用切面点函数具体的信息。下面四个接口是我们用的比较多的:

  1. Object proceed() throws Throwable; 调用要拦截的方法
  2. Object proceed(Object[] var1) throws Throwable;调用要拦截的方法,可以自定义传入参数
  3. Object[] getArgs();获取拦截方法的传入参数
  4. Signature getSignature();获取拦截方法的方法名

实现

XML配置

配置xml文件,使能AOP和Spring Bean自动装配

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 使能AOP-->
<aop:aspectj-autoproxy/>
<!-- 自动装载bean使能-->
<context:component-scan base-package="com.mj.spring.aop"/>
<context:annotation-config/> </beans>

定义自定义标签

我们的自定义标签可以作用于类,方法上,运行时工作。这儿需要说一下两个标签@Target和Retention,@Target用来设置标签的作用范围:

  1. @Target(ElementType. FIELD)表示标签只能用来修饰字段、枚举的常量
  2. @Target(ElementType.METHOD)表示标签只能用来修饰方法
  3. @Target(ElementType.TYPE) 标签可用来修饰接口、类、枚举、注解
  4. ...

@Retention用来修饰注解的生存范围

  1. @Retention(RetentionPolicy.SOURCE) 注解仅存在于源码中,在class字节码文件中不包含

  2. @Retention(RetentionPolicy.CLASS) 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得,

  3. @Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,在运行时可以通过反射获取到

     @Retention(RetentionPolicy.RUNTIME)
    public @interface AOPLog4jAnnotation { }

注意到我们没有添加Target标签,不指定标签的作用范围,那么标签适用于所有范围。

定义切面类

完整的切面类代码如下所示,类中实现了切面逻辑的定义和切点判断的逻辑代码。

@Component
@Aspect
public class APIProxy{ private final static Log LOGGER = LogFactory.getLog(APIProxy.class); //切面应用范围是在com.mj.spring.aop包下面所有函数
@Around("execution(* com.mj.spring.aop..*.*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String signatureName = joinPoint.getSignature().getName();
Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
if (isTagged(invokeClass, signatureName)) {
LOGGER.info(signatureName + " is tagged");
joinPoint.proceed();
return;
}
joinPoint.proceed(); } //扫描父类是否被打上标签,或者父类中的这个方法是否被打伤标签
private boolean isTagged(Class invokeClass, String signatureName) {
if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
return true;
}
if (!invokeClass.equals(Object.class)) {
return isTaggedInClassOf(invokeClass, signatureName) ? true :
isTagged(invokeClass.getSuperclass(), signatureName);
}
return false;
} //扫描当前类的接口
private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
Class[] interfaces = invokeClass.getInterfaces();
for (Class cas : interfaces) {
return isTaggedInClassOf(cas, signatureName) ? true :
isTaggedInInterfaceOf(cas, signatureName);
}
return false;
} //方法名为signatureName的方法tagged有两种情况:方法本身被taged或者方法所在的类被taged
private boolean isTaggedInClassOf(Class cas, String signatureName) {
return Lists.newArrayList(cas.getDeclaredMethods())
.stream().anyMatch(method ->
isMethodWithName(method, signatureName) && isMethodTagged(method)
|| isMethodWithName(method, signatureName) && isClassTagged(cas));
} private boolean isClassTagged(Class invokeClass) {
return invokeClass.getAnnotation(AOPLog4jAnnotation.class) != null;
} private boolean isMethodTagged(Method method) {
return method.getAnnotation(AOPLog4jAnnotation.class) != null;
} private boolean isMethodWithName(Method method, String name) {
return method.getName().equals(name);
}
}

下面代码实现了一个around切面advice定义,切面逻辑的应用范围是com.mj.spring.aop包下的所有的方法,判断当前执行方法是否被打上标签,如果打上标签,那么执行我们额外添加的业务逻辑代码,这里为简单起见在方法运行前打了一个log,然后执行方法,返回。否则直接调用方法,不做任何额外处理。

//切面应用范围是在com.mj.spring.aop包下面所有函数
@Around("execution(* com.mj.spring.aop..*.*(..))")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
String signatureName = joinPoint.getSignature().getName();
Class<? extends Object> invokeClass = joinPoint.getTarget().getClass();
if (isTagged(invokeClass, signatureName)) {
LOGGER.info(signatureName + " is tagged");
joinPoint.proceed();
return;
}
joinPoint.proceed(); }

方法被打上自定义标签有以下几种可能:

  1. 该方法方法体被打上标签
  2. 该方法所在类被打上标签
  3. 该方法所在的API接口函数对应被打上标签
  4. 该方法所在的API接口被打上标签
  5. 该方法所在的抽象类被打上标签
  6. 该方法所在的抽象类函数定义被打上标签

对于接口函数来说,接口之间可以多重嵌套,搜寻接口中指定函数的标签,需要采用递归的方式向上寻找,对于父类继承也同样如此。下面的代码实现涵盖了上面6种能性的所有判读。

    //扫描父类是否被打上标签,或者父类中的这个方法是否被打伤标签
private boolean isTagged(Class invokeClass, String signatureName)
{
if (isTaggedInInterfaceOf(invokeClass, signatureName)) {
return true;
}
if (!invokeClass.equals(Object.class)) {
return isTaggedInClassOf(invokeClass, signatureName) ? true :
isTagged(invokeClass.getSuperclass(), signatureName);
}
return false;
}

函数开始:

  1. 判断当前名为signatureName的方法是否在invokeClass类所实现的API接口中被Tag。(实现3和4的判断)
  2. 判断当前类是否为Object.class,若不是则执行第三步,否则执行第四步
  3. 判断当前名为signatureName的方法是否在类invokeClass中被tag(实现1和2的判断)
  4. 上面三项没有为真,则调用当前类的父类继续递归(实现5和6的判断)

判断当前名为signatureName的方法是否在invokeClass类所实现的API接口中被Tag的代码如下所示,首先获取当前类所有接口,分别对每个接口类进行方法检查,若检查成功,则返回true,否则继续向上递归。

    //扫描当前类的接口
private boolean isTaggedInInterfaceOf(Class invokeClass, String signatureName) {
Class[] interfaces = invokeClass.getInterfaces();
for (Class cas : interfaces) {
return isTaggedInClassOf(cas, signatureName) ? true :
isTaggedInInterfaceOf(cas, signatureName);
}
return false;
}

判断一个名为signatureName的方法在类cas中是否被tag的代码如下所示:

    private boolean isTaggedInClassOf(Class cas, String signatureName) {
return Lists.newArrayList(cas.getDeclaredMethods())
.stream().anyMatch(method ->
isMethodWithName(method, signatureName) && isMethodTagged(method)
|| isMethodWithName(method, signatureName)&& isClassTagged(cas));

代码逻辑实现很简单,判断方法被tag条件为:该方法是在该类中同时(该方法体是被打上标签或者类被打上标签)

Conclusion

本文和大家分享了如何通过自定义注解实现AOP切点定义,希望能够对大家有所帮助。本文完整的源码,单元测试位于:< https://github.com/jma19/spring/tree/master/spring-aop >, 欢迎大家下载,批评指正。

如何通过自定义注解实现AOP切点定义的更多相关文章

  1. SpringBoot自定义注解、AOP打印日志

    前言 在SpringBoot中使用自定义注解.aop切面打印web请求日志.主要是想把controller的每个request请求日志收集起来,调用接口.执行时间.返回值这几个重要的信息存储到数据库里 ...

  2. 如何优雅地在 Spring Boot 中使用自定义注解,AOP 切面统一打印出入参日志 &vert; 修订版

    欢迎关注个人微信公众号: 小哈学Java, 文末分享阿里 P8 资深架构师吐血总结的 <Java 核心知识整理&面试.pdf>资源链接!! 个人网站: https://www.ex ...

  3. Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志

    其实,小哈在之前就出过一篇关于如何使用 AOP 切面统一打印请求日志的文章,那为什么还要再出一篇呢?没东西写了? 哈哈,当然不是!原因是当时的实现方案还是存在缺陷的,原因如下: 不够灵活,由于是以所有 ...

  4. Spring Boot 中使用自定义注解,AOP 切面打印出入参日志及Dubbo链路追踪透传traceId

    一.使用背景 开发排查系统问题用得最多的手段就是查看系统日志,在分布式环境中一般使用 ELK 来统一收集日志,但是在并发大时使用日志定位问题还是比较麻烦,由于大量的其他用户/其他线程的日志也一起输出穿 ...

  5. java&sol;springboot自定义注解实现AOP

    java注解 即是注释了,百度解释:也叫元数据.一种代码级别的说明. 个人理解:就是内容可以被代码理解的注释,一般是一个类. 元数据 也叫元注解,是放在被定义的一个注解类的前面 ,是对注解一种限制. ...

  6. 基于springboot通过自定义注解和AOP实现权限验证

    一.移入依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spri ...

  7. 使用自定义注解和AOP管理shiro权限

    一.场景 在使用shiro框架的时候,遇到了这样的需求:本系统有多个用户,每个用户分配不同角色,每个角色的权限也不一致.比如A用户拥有新闻列表的增删改查权限,而B用户只有查看新闻列表的权限,而没有删除 ...

  8. 手写SpringBoot自动配置及自定义注解搭配Aop,实现升级版&commat;Value&lpar;&rpar;功能

    背景 项目中为了统一管理项目的配置,比如接口地址,操作类别等信息,需要一个统一的配置管理中心,类似nacos. 我根据项目的需求写了一套分布式配置中心,测试无误后,改为单体应用并耦合到项目中.项目中使 ...

  9. spring boot通过自定义注解和AOP拦截指定的请求

    一 准备工作 1.1 添加依赖 通过spring boot创建好工程后,添加如下依赖,不然工程中无法使用切面的注解,就无法对制定的方法进行拦截 <dependency> <group ...

随机推荐

  1. 【转】CSS样式覆盖规则

    大家都知道CSS的全称叫做“层叠样式表”,但估计很多人都不知道“层叠”二字的含义.其实,“层叠”指的就是样式的覆盖,当一个元素被运用上多种样式,并且出现重名的样式属性时,浏览器必须从中选择一个属性值, ...

  2. jquery读取XML 生成页面文件

    $.get("../../js/data.xml", function (xml) { $(xml).find("local").each(function ( ...

  3. java中实现链表(转)

    分析: 上述节点具备如下特征: 1. 每个节点由两部分组成(存储信息的字段,存储指向下一个节点的指针) 2. 节点之间有着严格的先后顺序. 3. 单链表节点是一种非线性的结构,在内存中不连续分配空间. ...

  4. Spring AOP中pointcut expression表达式解析 及匹配多个条件

    Spring中事务控制相关配置: <bean id="txManager" class="org.springframework.jdbc.datasource.D ...

  5. VB数据库经典实例总结(二)

    大家先看一张似图非图的图. 我们先称它为“过程”也许有不对的地方,在我学数据库到这个阶段.到这个刚刚接触.初生牛犊不怕虎的阶段对它的理解是这样的.所有的都是这个过程.只是在这中间掺杂了一些知识点(我们 ...

  6. 我的第一篇Markdown博客

    我的第一篇Markdown博客 这是我第一次用Markdown写博客,发现还是比较好用的,加上Marsedit也支持了Markdown的博客预览,博客园也加了Markdown的格式支持,就更加方便了, ...

  7. 用Ajax去读取服务器端的XML格式的数据

    <html> <head></head> <script type="text/javascript"> /*---定义一个全局变量 ...

  8. OUTPUT 在insnert delete update 的神奇功效

    Inserted   deleted  个人理解 应该是两个 临时表   分别存储 变动后的数据集  和  变动前的数据集 使用例子: 1.对于INSERT,可以引用inserted表以查询新行的属性 ...

  9. OpenCV——改变图像大小

    , , int interpolation=INTER_LINEAR ) dsize与fx和fy必须不能同时为零,也就是说要么dsize不为零而fx与fy同时可以为0,要么dsize为0而fx与fy不 ...

  10. ABP应用层——应用服务(Application services)

    ABP应用层——应用服务(Application services) 点这里进入ABP系列文章总目录 基于DDD的现代ASP.NET开发框架--ABP系列之15.ABP应用层——应用服务(Applic ...