利用SpringAOP + 注解解析方法参数,并解决方法参数不能为Null的问题

前言

我们知道,面向切面编程是一个非常成熟的代码解决方案。我们可以通过不改变代码结构的情况下增强特定代码段的功能,比如最经典的加注解完成方法运行时间计算。切面和切点就成为了代码增强的要点。而Java中主要使用强大的反射机制完成这一解析。

前段时间有一个需要用到Dubbo的明文参数传递Token鉴权,而一个应用里面有很多前端控制器接口都需要转写成dubbo的接口去给其他应用调用,一个个写鉴权逻辑又导致代码冗余过高,用静态方法解析似乎也不够简洁。于是我想到了注解和切面的鉴权方式。

本文只讨论获取方法指定参数的技巧,不讨论鉴权逻辑。

起步

我们需要一个SpringBoot工程,版本2.x,并需要引入如下依赖

<!-- aop -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.7</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

至此,第一阶段的准备工作就做好了

注意

SpringAOP不是所有场景都能用的,只有在被切方法是代理调用的时候,切面才能生效,也就是说,如果被切方法是被直接调用的,那么这个方法是不能被切的。

说人话就是,只有注入到Spring容器里面的对象,通过Spring代理的方式调用,这个方法才能被切面捕获并调用切面方法。

举两个例子,如下方式是不能被切的:

  1. funcA中直接调用funcB,相当于直接通过this.funcB去调用了B方法,那么方法B上的注解,是不能被AOP扫描到并切面的。
public void funcA() {
    funcB();
}

@AnnoTest
public void funcB() {
    
}
  1. 自己new出来的对象方法是不能被切的。

所以要想在同一个类里面互相调用还能被切,那么可以将类本身注入到Spring容器,再进行调用,这样就可以被切到对应的方法了

public class Demo {
    @Autowired
    Demo demo;
    
    public void funcA() {
        demo.funcB();
    }

    @AnnoTest
    public void funcB() {

    }
}

类与方法定义

讲完了注意事项,我们来看如何获取到方法参数,首先我们定义一个注解,采用注解的形式对该方法进行切面获取

@Target(ElementType.METHOD)  //表示在方法上可用
@Retention(RetentionPolicy.RUNTIME)  //运行时生效
@Documented
public @interface TokenCheck {

}

我们定义一个需要被切的方法

public class AopPointCutDemo() {
    @TokenCheck
    public String aopDemo(String parm1, String param2, String tokenId) {
    	//自己的方法逻辑,已省略,不重要。
	}
}

现在在这个类里面定义了一个方法,这个方法里面有三个参数,其中第三个tokenId是我们需要获取的鉴权对象。并在方法上加上该注解

定义切面

我们创建一个类:TokenCheckHandler,并完成如下代码

@Slf4j  //日志工具,可引入lombok依赖使用
@Aspect //切面
@Component //注入Spring容器
public class TokenCheckHandler {
    
    //定义切点,切点为带TokenCheck注解的方法
    @Pointcut("@annotation(TokenCheck)")
    private void pointCut() {

    }
    
    @Around("pointCut()")
    public Object annotationAround(ProceedingJoinPoint pjp) throws Throwable {
        //切点获取逻辑
    }
    
    private void isTokenValid(String tokenId) {
        //具体的tokenId鉴权逻辑
    }
}

至此,一个切面的大概代码就完成了,接下来就是重头戏,获取token完成解析。

切点逻辑

  1. 首先,我们可以通过切点方法annotationAround中的ProceedingJoinPoint pjp参数,获取到切点的类名和方法名
//声明tokenId
String tokenId = null;
//获取切面的类名和方法名
String classType = pjp.getTarget().getClass().getName();
String methodName = pjp.getSignature().getName();
  1. 通过切点获取到参数值,并完成参数值的类名获取,由于可能出现基本数据类型的情况,所以我们还需要对基本数据类型的类名转化为包装类型,先定义一个转化的规则Map
private static HashMap<String, Class> map = new HashMap<String, Class>() {
    {
        put("java.lang.Integer", Integer.class);
        put("java.lang.Double", Double.class);
        put("java.lang.Float", Float.class);
        put("java.lang.Long", Long.class);
        put("java.lang.Short", Short.class);
        put("java.lang.Boolean", Boolean.class);
        put("java.lang.Char", Character.class);
    }
};

然后写for循环获取到每一个参数的类型

//获取参数值
Object[] args = pjp.getArgs();
//解析参数的类名
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
    if (!args[k].getClass().isPrimitive()) {
        // 获取的是封装类型而不是基础类型
        String result = args[k].getClass().getName();
        Class s = map.get(result);
        classes[k] = s == null ? args[k].getClass() : s;
    }
}
  1. 获取到当前的Method对象。获取到对应的方法需要采用方法签名的方式,只有方法签名才能唯一地定位方法:方法签名=类型+方法名+方法参数类型
Method method = Class.forName(classType).getMethod(methodName, classes);
  1. 获取方法参数。

    接下来这里需要引入一个知识点,是关于Spring核心的一个方法参数名的解析功能,众所周知,Java在运行时是不记录方法参数名的,例如你定义的时候是String id,到执行的时候就变成了Sting param1…等,所以Spring在这里使用了一个ASM工具,可以在运行时获取字节码的方法参数变量,从而得到参数名去解析对应的MVC参数,这个东西咱了解就好,有兴趣的可以去翻看Spring的这段源码。

    定义一个获取参数名称的工具ParameterNameDiscoverer

//获取参数名称的工具
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
//通过上述步骤获取的方法对象得到方法的参数名称
String[] parameterNames = pnd.getParameterNames(method);
for (int i = 0; i < Objects.requireNonNull(parameterNames).length; i++) {
    //比对方法名称,找到是tokenId并且是字符串类型则赋值给tokenId,跳出循环
    if ("tokenId".equals(parameterNames[i]) && "java.lang.String".equals(classes[i].getName())) {
        tokenId = (String) args[i];
        break;
    }
}
  1. tokenId进行鉴权,鉴权完成后放行方法继续运行被切方法逻辑(省略)
isTokenValid(tokenId);
log.info("tokenId已解析,完成鉴权!");
return pjp.proceed(); 

发现问题

按照正常逻辑来说,以上代码是没有什么问题的,能够正常在spring环境或者dubbo环境中使用。但是如果此时调用方传入的参数中有一个为null,那么就会报空指针异常

通过定位,找到问题出在切点逻辑的第2点,for循环获取参数类型上,如果传入的参数为空,那么args[k].getClass().isPrimitive()这个方法就不能执行,导致出现空指针异常。

那么有聪明的小伙伴就想到了,那我在这个逻辑之上再加一个判空不就行了?有道理,将逻辑改成这样

Object[] args = pjp.getArgs();
Class<?>[] classes = new Class[args.length];
for (int k = 0; k < args.length; k++) {
    //避免空指针
    if (args[k] == null) {
       	//遇到空的赋值为Object类型
        classes[k] = Object.class;
        //进入下一次循环
        continue;
    }
    if (!args[k].getClass().isPrimitive()) {
        // 获取的是封装类型而不是基础类型
        String result = args[k].getClass().getName();
        Class s = map.get(result);
        classes[k] = s == null ? args[k].getClass() : s;
    }
}

但是很快你就会发现这段代码是不报错了,但是下面这行代码却报了NoSuchMethodException的异常

Method method = Class.forName(classType).getMethod(methodName, classes);

还记得上面说过的方法签名获取方法对象吗?我们给未知的null参数的Class数组赋值了Object.class那么自然也就找不到对应的方法了,看样子这个方法是不能传null了,但是传null的场景还是很多的,不能因噎废食。普通反射的方式看来是行不通了。

但是,灵性的但是,我们的切点,不是能获取方法名吗?那凭啥不能获取到方法对象呢?

但很可惜,ProceedingJoinPoint里面的getSignature()方法获取到的Signature接口对象,并没有getMethod()方法。

于是我开始找源码,往这个接口的实现类里面去寻找,发现实现它的下一级接口MethodSignature拥有getMethod()这个方法,于是,我开始Debug,发现其实这个接口所承载的对象,就是MethodSignature的实现类MethodSignatureImpl

于是我使用了强转的方式,将参数类型由MethodSignature接收,从而成功获取到当前切点的方法

//获取切面的类名和方法名
//String classType = pjp.getTarget().getClass().getName();
//String methodName = pjp.getSignature().getName();
//获取被切方法
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method invokeMethod = signature.getMethod();
//中间省略开始...
//中间省略结束...
ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
//Method method = Class.forName(classType).getMethod(methodName, classes);
// 参数名
String[] parameterNames = pnd.getParameterNames(invokeMethod);

这样就能够正常获取到被切方法,而且方法的参数名也能被获取出来了,原来的那种方式,其实有点舍近求远。明明被切的切点就可以拿到方法对象,还使用方法签名来获取它,多此一举。

其实翻看很多博客和教程,大部分都是按照我上面的方法进行参数的处理,没有考虑参数可以为null的情况,导致空指针异常!

这样,我们就能成功处理接收方法的参数为null的情况了。

至此,我们就实现了一个能够获取注解方法参数的功能。

  • 23
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot实现AOP面向切面编程的具体方法如下: 1. 首先,你需要在项目的pom.xml文件中添加spring-boot-starter-aop依赖。可以参考以下代码: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 2. 然后,你需要编写一个用于拦截的bean。这个bean将包含你想要在目标方法执行前后执行的逻辑。你可以使用注解或者编程方式来定义切面。例如,你可以使用@Aspect注解来定义一个切面,然后在切面的方法上使用@Before、@After等注解来定义具体的拦截行为。 3. 接下来,你需要将切面应用到目标对象上,创建代理对象。这个过程称为织入(Weaving)。在Spring Boot中,你可以使用@EnableAspectJAutoProxy注解来启用自动代理,它会根据切面定义自动创建代理对象。 总而言之,Spring Boot实现AOP面向切面编程的具体方法包括:添加依赖、编写用于拦截的bean,以及启用自动代理。这样就能实现在目标方法执行前后执行特定逻辑的效果了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [SpringBoot整合aop面向切面编程过程解析](https://download.csdn.net/download/weixin_38689551/12743012)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [SpringBoot实现AOP面向切面编程](https://blog.csdn.net/weixin_52536274/article/details/130375560)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值