目标,创建一个方法注解,我们在执行方法时,并可以给该方法的参数值动态传递给注解。

由于业务需要,需要在自定义注解中将参数中的值传入到注解的指定属性中,这很容易让我联想到 Spring 的 SpEL.

虽然根据反射也能得到相同结果,但是有更好的工具为什么不用呢?因此根据网上大神的攻略,整合出自定义注解伪动态传递参数的 SpEL 使用.

1、自定义注解

import org.springframework.stereotype.Component;
import java.lang.annotation.*;

/**
 * 自定义注解实现日志保存
 * ip 使用SPEL表达式绑定动态变量参数值的形式传递,要求ip参数存在并赋值
 */
@Documented
@Target(ElementType.METHOD)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface OperaLog {
    String message()default "";
    String ip()default "127.0.0.1";
}

2、定义AOP

使用SPEL表达式绑定动态变量参数值

import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;

@Aspect // 1.日志切面类
@Component
@Slf4j
public class OperaLogAspect {

    // 2. PointCut表示这是一个切点,@annotation表示这个切点切到一个注解上,后面带该注解的全类名
    // logPointCut()代表切点名称
    @Pointcut("@annotation(operaLog)")
    public void logPointCut(OperaLog operaLog){}

    // 3. 环绕通知
    @Around("logPointCut(operaLog)")
    public void logAround(ProceedingJoinPoint joinPoint, OperaLog operaLog) {
        // 继续执行方法
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        saveLog(joinPoint, operaLog);
    }

    /**
     * 保存日志
     * @param joinPoint
     * @param operaLog
     */
    private void saveLog(ProceedingJoinPoint joinPoint, OperaLog operaLog){
        String msg = getMsg(joinPoint, operaLog.message());
        String ip = getIp(joinPoint, operaLog);
        log.info("msg=" + msg);
        log.info("ip =" + ip);
        //TODO 保存日志       
    }

    /**
     * 通过使用SPEL表达式绑定动态变量参数值的形式获取ip
     * @param joinPoint
     * @param operaLog
     * @return
     */
    private String getIp(ProceedingJoinPoint joinPoint, OperaLog operaLog){
        //获取方法签名(通过此签名获取目标方法信息)
        MethodSignature ms = (MethodSignature)joinPoint.getSignature();
        Method method = ms.getMethod();
        Object[] args = joinPoint.getArgs();
        //获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = localVariableTable.getParameterNames(method);
        //使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        //SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        //把方法参数放入SPEL上下文中
        for(int i=0;i<paraNameArr.length;i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        String ip = "";
        String param = operaLog.ip();
        // 使用变量方式传入业务动态数据
        if(param.matches("^#.*.$")) {
            ip = parser.parseExpression(param).getValue(context, String.class);
        }
        return ip;
    }

    /**
     * 操作详情组装
     * @param joinPoint
     * @param msg
     * @return
     */
    private String getMsg(ProceedingJoinPoint joinPoint, String msg){
        //获取类的字节码对象,通过字节码对象获取方法信息
        Class<?> targetCls= joinPoint.getTarget().getClass();
        //获取方法签名(通过此签名获取目标方法信息)
        MethodSignature ms=(MethodSignature)joinPoint.getSignature();
        String className =  ms.getDeclaringType().getSimpleName();
        String targetClsName=targetCls.getName();
        System.out.println("targetClsName:"+targetClsName);
        // 获取方法名称
        String methodName = ms.getName();
        String[] parameterName = ms.getParameterNames();
        Map<String, String> map = new LinkedHashMap<>();
        for(String o : parameterName){
            map.put(o,"");
        }
        StringBuilder sb2 = new StringBuilder();
        Class[] parameterType = ms.getParameterTypes();
        for(Class o : parameterType){
            sb2.append(o.getName() + "; ");
        }
        System.out.println("parameterType[]:"+sb2.toString());
        String declaringTypeName =ms.getDeclaringTypeName();
        System.out.println("declaringTypeName:"+declaringTypeName);
        //获取目标方法上的注解指定的操作名称
        Method targetMethod = null;
        try {
            targetMethod = targetCls.getDeclaredMethod(ms.getName(), ms.getParameterTypes());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        System.out.println("targetMethod:"+targetMethod);
        ms.getParameterNames();
        // 获取入参
        Object[] param = joinPoint.getArgs();
        for(Object o : param){
            for (Map.Entry<String, String> entry : map.entrySet()) {
                if(StringUtils.notText(entry.getValue())){
                    entry.setValue(o.toString());
                    break;
                }
            }
        }
        StringBuffer sb = new StringBuffer(256);
        sb.append("进入:").append(className).append("类[").append(methodName).append("]方法->").append(msg);
        sb.append(" 参数:{");
        for (Map.Entry<String, String> entry : map.entrySet()) {
            sb.append("&").append(entry.getKey()).append("=").append(entry.getValue());
        }
        sb.append("}");
        return sb.toString();
    }

}

3、使用

使用SPEL表达式绑定动态变量参数值 #ip

在需要的方法上加上我们的注解,传递要做的是什么事情,并且运用SPEL表达式把接口参数绑定到注解属性上

//使用SPEL表达式绑定动态变量参数值 #ip
	@OperaLog(message = "删除人员", ip = "#ip")
	public String delete(String ip, String userId,String userName) {
		// TODO 删除人员
	}

4、输出结果

msg=进入:Service类[delete]方法->删除人员 参数:{&ip=192.168.122.186&userId=101&userName=张三}
ip =192.168.122.186

写在最后

天下英雄出我辈,一入江湖岁月催
我是「无间行者」,努力把实践过的解决方案分享给大家
码字不易,给点鼓励吧,让我知道你在看