Elegant use of token s

original code

@RestController
@RequestMapping("api")
public class TestController {
    @Autowired
    MyService myService;

    @GetMapping("test")
    public String test(HttpServletRequest request){
        String result = myService.test(request);
        return result;
    }
}
@Service
public class MyService {
    public String test(HttpServletRequest request){
        String token = request.getHeader("token");
        System.out.println(token);
        //Verify token, handle business logic
        return "success";
    }
}

In the method, the token in the header information is obtained through HttpServletRequest, and then verified, and then the business logic is processed. At first glance, there is no problem, but if I write too much, I feel that it is very troublesome to write this way. Each interface has such an unnecessary parameter. Can you deal with it?

Aspect code with method parameters

At this time, I remembered, didn't I say it when I was learning Spring before, wouldn't it be good to deal with such a large amount of repetitive work that has nothing to do with business, and put it in the section. But looking back, not every method in the Service needs to verify the token. Simply write an annotation and use the aspect to process the annotated method

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedToken {
}

The aspect to do is very simple, get the current HttpServletRequest request through the RequestContextHolder provided by SpringMvc, and then take out the token in the header.

At this time, the first question comes. After I get the token in the aspect, how do I pass it to the method called in the Service? Recall that the parameters of the method can be obtained in the aspect, and then the value of the parameter can be dynamically modified. Modify the parameters of the Service method, remove the annoying HttpServletRequest, and add a parameter of type String to receive the token.

@Service
public class MyService {
    @NeedToken
    public String test(String token){
        System.out.println(token);
        //Verify token, handle business logic
        return "success";
    }
}

The aspect implementation is as follows:

@Aspect
@Component
public class TokenAspect {
    @Pointcut("@annotation(com.cn.hydra.aspectdemo.annotation.NeedToken)")
    public void tokenPointCut() {
    }

    @Around("tokenPointCut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        Object[] args = point.getArgs();
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        String[] paramName = methodSignature.getParameterNames();
        List<String> paramNameList = Arrays.asList(paramName);
        if (paramNameList.contains("token")){
            int pos = paramNameList.indexOf("token");
            args[pos]=token;
        }

        Object object = point.proceed(args);
        return object;
    }
}

The following things are done in the section:

  • Define the cut point and weave logic into the method annotated with @NeedToken

  • Get HttpServletRequest through RequestContextHolder and get the token in the header

  • Obtain the parameter list of the method through MethodSignature, and modify the token value in the parameter list

  • Call the original method with the new parameter list, and pass the token to the method at this time

@RestController
@RequestMapping("api")
public class TestController {
    @Autowired
    MyService myService;
    
    @GetMapping("test")
    public String test(){
        String result = myService.test(null);
        return result;
    }
}

It is said that it can solve the problem, but it is very uncomfortable to write an extra null parameter

Aspect code with method without parameters

Declare global variables

If not by passing parameters, is there any way to pass the token to the method? There is an idea here, you can get the object that the method belongs to through the aspect, and it is easy to handle with the object, and directly inject a value into a certain attribute through reflection. Modify Service again, declare a global variable for reflection injection token use

@Service
public class MyService{
    private String TOKEN;
    
    @NeedToken
    public String test() {
        System.out.println(TOKEN);
        //Verify token, handle business logic
        return  TOKEN;
    }
}

Modify the aspect implementation method:

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        Field tokenField = point.getTarget().getClass().getDeclaredField("TOKEN");
        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),token);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throw e;
    }
}

Note that it is no longer necessary to modify the parameters passed in by the method, but by obtaining the Field of the class, and then injecting actual values ​​into the Field corresponding to the token of the current object.

Inherit the method of the parent class

I feel good about writing this for a while, but after writing a few classes, I found that each of my Service classes has to declare a token global variable of type String. Not only is it troublesome, but if any class forgets to write it, just directly gg, is there any easier and safer way?

First define a parent class. As for why the parent class is used instead of the interface, the reason is that the variables declared in the interface are modified by default by default, so they cannot be changed.

public class BaseService {
    public String TOKEN = null;
}

Modify the Service code, inherit the BaseService class, and delete your own TOKEN variable:

@Service
public class MyService extends BaseService {
    @NeedToken
    public String test() {
        System.out.println(TOKEN);
        //Verify token, handle business logic
        return  TOKEN;
    }
}

When reflecting, the variables defined in the parent class cannot be obtained through the current object, so we simply modify the code of the aspect:

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {
        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        //Field tokenField = point.getTarget().getClass().getDeclaredField("TOKEN");
        Class<?> baseClazz = point.getTarget().getClass().getSuperclass();
        Field tokenField = baseClazz.getDeclaredField("TOKEN");
        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),token);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throw e;
    }
    
}

Modify to obtain the parent class through the current object, then obtain the variables in the parent class, and then inject the token value through reflection

Inherit the method optimization of the parent class to prevent taking the wrong token

After testing several times to get the token, there is no problem, it is really flattering. But after a day, I suddenly found something wrong. As we all know, Spring’s beans are singletons by default, and the value of global variables can be changed by any thread. Then there is a situation, maybe one thread will get the token modified by another thread

Redefine the parent class and use ThreadLocal to save the token:

public class BaseService2 {
    public static ThreadLocal<String> TOKEN= 
            ThreadLocal.withInitial(() -> null);
}

Modify Service:

@Service
public class MyService2 extends BaseService2 {
    @NeedToken
    public boolean testToken(String name) {
        String token=TOKEN.get();
        boolean check = name.equals(token);
        System.out.println(name+"  "+token +"  "+check);
        return  check;
    }
}

Modify the aspect:

@Around("tokenPointCut()")
public Object doAround(ProceedingJoinPoint point) throws Throwable {
    try {

        ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");

        Class<?> baseClazz = point.getTarget().getClass().getSuperclass();
        Field tokenField = baseClazz.getDeclaredField("TOKEN");
        ThreadLocal<String> local = (ThreadLocal<String>) tokenField.get(point.getTarget());
        local.set(token);

        tokenField.setAccessible(true);
        tokenField.set(point.getTarget(),local);

        Object object = point.proceed();
        return object;
    } catch (Throwable e) {
        e.printStackTrace();
        throw e;
    }
}

//Prevent memory leaks! ! !
@After("tokenPointCut()")
  public void doAfter(ProceedingJoinPoint point) {
      TOKEN.remove ();

  }

Tags: Java jvm Spring

Posted by BigMonkey on Sun, 20 Nov 2022 14:07:13 +1030