最初的代码
@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);
//验证token,处理业务逻辑
return "success";
}
}
在方法中通过HttpServletRequest获取头信息
中的token,然后进行验证,之后再做业务逻辑处理。乍一看没什么问题,但是写多了就觉得这么写很麻烦,每个接口都要多这么一个不必要的参数,能不能处理一下呢?
方法带参数的切面代码
这时候想起来了,以前学Spring的时候不就说过吗,处理这种和业务无关的大量重复劳动,放在切面里不就好了吗
。但回头一想,Service里的方法也不是每个都需要验证token,干脆写个注解,用切面来处理加了注解的方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NeedToken {
}
切面要做的很简单,通过SpringMvc提供的RequestContextHolder获取当前的HttpServletRequest请求,然后再取出header中的token。
这时候第一个问题来了,我在切面获取到了token以后,怎么传递给Service中调用的方法呢?回想到在切面中可以获取方法的参数,然后动态修改参数的值就可以了。修改Service方法的参数,去掉烦人的HttpServletRequest,添加一个String类型的参数,用于接收token。
@Service
public class MyService {
@NeedToken
public String test(String token){
System.out.println(token);
//验证token,处理业务逻辑
return "success";
}
}
切面实现如下:
@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;
}
}
切面中做了下面几件事:
-
定义切点,在加了
@NeedToken
注解的方法织入逻辑 -
通过
RequestContextHolder
获取HttpServletRequest
,获取header中的token -
通过
MethodSignature
获取方法的参数列表,修改参数列表中的token的值
-
使用新的参数列表调用原方法,这时候就把token传递给了方法
@RestController
@RequestMapping("api")
public class TestController {
@Autowired
MyService myService;
@GetMapping("test")
public String test(){
String result = myService.test(null);
return result;
}
}
说能解决问题,但是要多写一个null的参数,就让人就很难受了
方法不带参数的切面代码
声明全局变量
如果不通过传递参数的方式,有什么办法能把token传递给方法呢
?这里灵机一动,可以通过切面获取方法属于的对象
啊,有了对象就好办了,直接通过反射给某个属性注入值。再次修改Service,声明一个全局变量,用于反射注入token使用
@Service
public class MyService{
private String TOKEN;
@NeedToken
public String test() {
System.out.println(TOKEN);
//验证token,处理业务逻辑
return TOKEN;
}
}
修改切面实现方法:
@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;
}
}
注意这里不再去修改方法传入的参数,而是通过获取类的Field ,然后向当前对象的token对应的Field注入实际值来实现的。
继承父类的方法
写到这自我感觉良好了一会,但是写了几个类发现了我每个Service类都得多声明一个String类型的token全局变量啊,不光是麻烦,万一哪个类忘了写不就直接gg了,有没有什么更简便、安全的方法呢?
先定义一个父类,至于为什么使用父类而不是接口,原因就是接口中声明的变量是默认被
final
修饰的,所以是不能被改变的。
public class BaseService {
public String TOKEN = null;
}
修改Service代码,继承
BaseService
类,删掉自己的TOKEN变量:
@Service
public class MyService extends BaseService {
@NeedToken
public String test() {
System.out.println(TOKEN);
//验证token,处理业务逻辑
return TOKEN;
}
}
反射的时候不能通过当前对象拿到父类中定义的变量,那我们就简单修改一下切面的代码:
@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;
}
}
修改为
通过当前对象获取父类,然后获取父类中的变量,再通过反射注入token值
继承父类的方法优化,防止拿错token
测试了几遍token获取都没啥问题,简直美滋滋。但是隔了一天突然发现不对啊,众所周知Spring的默认情况下Bean都是单例模式
,并且全局变量的值任何一个线程都可能去改变。那么就存在情况,可能一个线程会拿到另一个线程修改后的token
重新定义父类,使用ThreadLocal保存token:
public class BaseService2 {
public static ThreadLocal<String> TOKEN=
ThreadLocal.withInitial(() -> null);
}
修改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;
}
}
修改切面:
@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;
}
}
//防止内存泄露!!!
@After("tokenPointCut()")
public void doAfter(ProceedingJoinPoint point) {
TOKEN.remove ();
}