在Spring的体系中有很多种实现自定义注解的方式,在这里介绍一下如何通过AOP的方式来实现用户自定义的注解。

在这里实现一个对方法中的指定参数进行是否为null的判断的注解,以此为例展示自定义注解的方便之处。

配置环境

首先搭建Spring环境,在pom.xml中引入相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>tech.codemine</groupId>
    <artifactId>annotation-in-action</artifactId>
    <version>1.0-SNAPSHOT</version>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
    </dependencies>
</project>

接着在resources文件夹中建立一个applicationContext.xml的文件,内容如下

<?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:context="http://www.springframework.org/schema/context"
        xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:task="http://www.springframework.org/schema/task"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-3.0.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
           http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd"
        default-lazy-init="false">

    <context:component-scan base-package="tech.codemine"/>
    <aop:aspectj-autoproxy/>
    <aop:config proxy-target-class="true"> </aop:config>
</beans>

然后建立四个使用到的注解,分别是

用于检验所有参数都不为null的@CheckAllNotNull

package tech.codemine.nullcheck.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface CheckAllNotNull {

}

用于检验指定参数不为null的@CheckNotNull

package tech.codemine.nullcheck.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface CheckNotNull {

}

以及用来为部分参数注解是否需要检验的@NotNull,与@CheckNotNull进行配合

package tech.codemine.nullcheck.annotation;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Inherited
public @interface NotNull {

}

接着建立一个NullCheck.java文件用于处理注解

package tech.codemine.nullcheck.aspect;

import javafx.util.Pair;
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.springframework.stereotype.Component;
import tech.codemine.nullcheck.annotation.NotNull;
import tech.codemine.nullcheck.exception.ParameterIsNullException;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 @Auther: wangzhen25
 @Date: 2018/11/20 16:51
 @Description:
 */
@Aspect
@Component
public class NullCheckAspect {
    
    // 在这里定义切点,切点为我们之前设置的注解@CheckAllNotNull
    @Pointcut("@annotation(tech.codemine.nullcheck.annotation.CheckAllNotNull)")
    public void checkAllNotNullPointcut() {
    }

    // 在这里定义切点,切点为我们之前设置的注解@CheckNotNull
    @Pointcut("@annotation(tech.codemine.nullcheck.annotation.CheckNotNull)")
    public void checkNotNullPointcut() {
    }

    // 在这里定义@CheckAllNotNull的增强方法
    @Around("checkAllNotNullPointcut()")
    public Object methodsAnnotatedWithCheckAllNotNull(ProceedingJoinPoint joinPoint) throws Throwable {
        // 首先获取方法的签名,joinPoint中有相应的签名信息
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        // 通过方法的签名可以获取方法本身
        Method method = signature.getMethod();
        // 通过joinPoint获取方法的实际参数的值
        Object[] args = joinPoint.getArgs();
        // 对参数的值进行遍历判断,如果为null则抛出异常
        for (int i = 0; i < args.length; i++) {
            Object arg = args[i];
            if(arg == null)
                throw new ParameterIsNullException(method, i + 1);
        }
        // 最后如果校验通过则调用proceed方法进行方法的实际执行
        return joinPoint.proceed();
    }

    // 在这里定义@CheckNotNull的增强方法
    @Around("checkNotNullPointcut()")
    public Object methodsAnnotatedWithCheckNotNull(ProceedingJoinPoint joinPoint) throws Throwable {
        // 首先获取方法的签名,joinPoint中有相应的签名信息
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        // 通过方法的签名可以获取方法本身
        Method method = signature.getMethod();
        // 获取方法的参数列表,注意此方法需JDK8+
        Parameter[] parameters = method.getParameters();
        // 获取参数列表的实际的值
        Object[] args = joinPoint.getArgs();
        // 建立参数列表和实际的值的对应关系
        Pair<Parameter, Object>[] parameterObjectPairs = new Pair[parameters.length];
        for(int i = 0; i < parameters.length; i++) {
            parameterObjectPairs[i] = new Pair<>(parameters[i], args[i]);
        }
        // 对参数进行遍历
        for(int i = 0; i < parameterObjectPairs.length; i++) {
            Pair<Parameter, Object> pair = parameterObjectPairs[i];
            Parameter parameter = pair.getKey();
            Object arg = pair.getValue();
            // 获取参数的注解列表
            NotNull[] notNulls = parameter.getAnnotationsByType(NotNull.class);
            // 如果发现没有@NotNull注解则校验下一个参数
            if(notNulls.length == 0) {
                continue;
            }
            // 如果发现参数有@NotNull注解并且实际值为null则抛出异常
            if(arg == null) {
                throw new ParameterIsNullException(method, i + 1);
            }
        }
        // 校验通过后继续往下执行实际的方法
        return joinPoint.proceed();
    }
}

以及一个专属的ParameterIsNullException

package tech.codemine.nullcheck.exception;

import java.lang.reflect.Method;

/**
 @Auther: wangzhen25
 @Date: 2018/11/20 18:20
 @Description:
 */
public class ParameterIsNullException extends Exception {

    public ParameterIsNullException(String message) {
        super(message);
    }

    public ParameterIsNullException(Method method, int paramentIndex) {
        super("[in method: " + method.getName() + "] the " + paramentIndex + "th parameter " + "can not be null");
    }
}

项目的完整结构如下

springmvc 自定义handlermapping handlerAdaptor handler_spring

功能测试

@CheckAllNotNull

package tech.codemine.nullcheck;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
import tech.codemine.nullcheck.annotation.CheckAllNotNull;
import tech.codemine.nullcheck.annotation.CheckNotNull;
import tech.codemine.nullcheck.annotation.NotNull;

@Component
public class Main {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Main m = (Main) applicationContext.getBean("main");
        m.test("hello", null);
    }

    @CheckAllNotNull
    public void test(String var1, String var2) {
        System.out.println(var1 + var2);
    }
}

控制台输出

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
	at tech.codemine.nullcheck.Main$$EnhancerBySpringCGLIB$$ed31225e.test(<generated>)
	at tech.codemine.nullcheck.Main.main(Main.java:17)
Caused by: tech.codemine.nullcheck.exception.ParameterIsNullException: [in method: test] the 2th parameter can not be null
	at tech.codemine.nullcheck.aspect.NullCheckAspect.methodsAnnotatedWithCheckAllNotNull(NullCheckAspect.java:41)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
	... 2 more

Process finished with exit code 1

@CheckNotNull

测试一

首先测试对第一个参数设为null但不使用@NotNull

package tech.codemine.nullcheck;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
import tech.codemine.nullcheck.annotation.CheckAllNotNull;
import tech.codemine.nullcheck.annotation.CheckNotNull;
import tech.codemine.nullcheck.annotation.NotNull;

@Component
public class Main {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Main m = (Main) applicationContext.getBean("main");
        m.test(null, null);
    }

    @CheckNotNull
    public void test(String var1, String var2) {
        System.out.println(var1 + var2);
    }
}

控制台输出

nullnull

Process finished with exit code 0

测试二

现在使用@NotNull对参数进行约束

package tech.codemine.nullcheck;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.stereotype.Component;
import tech.codemine.nullcheck.annotation.CheckAllNotNull;
import tech.codemine.nullcheck.annotation.CheckNotNull;
import tech.codemine.nullcheck.annotation.NotNull;

@Component
public class Main {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
                new ClassPathXmlApplicationContext("applicationContext.xml");
        Main m = (Main) applicationContext.getBean("main");
        m.test(null, null);
    }

    @CheckNotNull
    public void test(@NotNull String var1, String var2) {
        System.out.println(var1 + var2);
    }
}

控制台输出

Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
	at tech.codemine.nullcheck.Main$$EnhancerBySpringCGLIB$$ed31225e.test(<generated>)
	at tech.codemine.nullcheck.Main.main(Main.java:17)
Caused by: tech.codemine.nullcheck.exception.ParameterIsNullException: [in method: test] the 1th parameter can not be null
	at tech.codemine.nullcheck.aspect.NullCheckAspect.methodsAnnotatedWithCheckNotNull(NullCheckAspect.java:65)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
	at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
	at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
	... 2 more

Process finished with exit code 1

测试结束