最近以前的同事遇到这样一个问题:
需求:对于系统提供的接口有些需要登陆验证,有些则不需要验证,通过代码实现此功能。
说明:系统使用的是springboot框架,采用Java+kotlin混合编码。
定义自定义登录注解LoginCheck,只能添加到方法上。
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
使用spring aop对添加了LoginCheck注解的方法做切面,采用around方式对指定方法做登录验证。
@Aspect
@Component
@Slf4j
public class LoginAop {
@Pointcut("@annotation(LoginCheck)")
public void pointcut(){}
@Around("pointcut()")
public Object aop(ProceedingJoinPoint point){
try {
// 登录校验逻辑(省略)
return point.proceed();
} catch (Throwable throwable) {
log.error("error:",throwable);
}
return null;
}
}
@RestController
@RequestMapping("/test")
class TestController (
val goodsService: GoodsService
){
@GetMapping("demo01")
@LoginCheck
fun demo01(){
goodsService.getOne();
}
}
可以看到原来kotlin的类默认是final类型的,而Spring对于类做Aop,采用的方案是使用Cglib动态生成代理对象即源类的子类对象实现的,我们知道final关键字修饰的类是不能被之类继承的,这就导致了上面的报错。
修改Kotlin类为open类型的即可,其中kotlin类修饰符如下:
修饰符 | 相应类的成员 | 注解 |
---|---|---|
final |
不能被覆写 | 在kotlin中默认所有的方法和类都是final 属性 |
open |
可以被覆写 | 需要被明确指出 |
abstract |
必须要覆写 | 不能被实例化,默认具有open 属性。 |
override |
覆写超类的方法 | 如果没有被指定为final ,则默认具有open 属性 |
启动程序一切正常,当我们调用接口时,程序NPE
可以看到是goodsService注入失败,我们修改goodsService的注入方式为注解注入@Autowired,程序启动成功,接口调用成功。
通过方案一可以看到kotlin下使用spring 的AOP功能要特别小心,虽然我们再最后解决了所有的问题,但这这种做法打破了kotlin的默认规则很不友好,比如新上手项目的同学就不会想到在类上添加open修饰符,使用bean的注入也势必想象到用构造函数的方式。
方案二采用新的方式,不用修改kotlin任何默认规则即可实现类似aop功能。具体操作如下:
其中主要逻辑为:
@Slf4j
@SpringBootApplication
public class App {
public static ConfigurableApplicationContext application = null;
public static final Set urlSet = new HashSet<>();
public static void main(String[] args) {
log.info("Application:recharge-center 启动开始");
application = SpringApplication.run(App.class, args);
Environment env = application.getEnvironment();
final String contextPath = env.getProperty("server.context-path", "/");
RequestMappingHandlerMapping handlerMapping = application.getBean(RequestMappingHandlerMapping.class);
Map handlerMethods = handlerMapping.getHandlerMethods();
for (Map.Entry entry : handlerMethods.entrySet()) {
final HandlerMethod handlerMethod = entry.getValue();
final Method method = handlerMethod.getMethod();
LoginCheck annotation = method.getAnnotation(LoginCheck.class);
if(annotation == null){
continue;
}
String code = annotation.code();
String[] fullPaths = new String[]{""};
final RequestMapping classReq = handlerMethod.getBeanType().getAnnotation(RequestMapping.class);
if(classReq != null){
fullPaths = classReq.value();
}
Class[] clazzs = new Class[]{RequestMapping.class,PostMapping.class,GetMapping.class};
for (Class clazz : clazzs) {
final Annotation methodAnnotation = method.getAnnotation(clazz);
if(methodAnnotation != null){
try {
final Method valueMethod = clazz.getDeclaredMethod("value");
for (String s : (String[]) valueMethod.invoke(methodAnnotation)) {
for (String fullPath : fullPaths) {
urlSet.add((contextPath + "/" + fullPath + "/" +s).replaceAll("/+","/"));
}
}
} catch (Exception e) {
log.error("初始化APP异常:",e);
}
break;
}
}
}
}
}
过滤所有请求通过判断请求地址是否在第3步urlSet集合中来决定是否需要登录验证
@Bean
public FilterRegistrationBean registerDefaultFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new Filter() {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI().replaceAll("/+","/");
if(requestURI.contains("?")){
requestURI = requestURI.substring(0,requestURI.indexOf("?"));
}
final String url = matchUrl(requestURI);
if(StringUtils.isNotBlank(url)){
//登录验证具体逻辑(省略)
filterChain.doFilter(requestWrapper, servletResponse);
}else{
filterChain.doFilter(requestWrapper, servletResponse);
}
}
private String matchUrl(String uri){
if(App.urlSet.containsKey(uri)){
return uri;
}else{
for (String str : App.urlSet) {
final String[] sysStrs = str.split("/");
final String[] webStrs = uri.split("/");
if(sysStrs.length != webStrs.length){
return null;
}
boolean match = true;
for (int i = 0; i < sysStrs.length; i++) {
match &= sysStrs[i].matches("^\\{.+\\}$") | StringUtils.equals(sysStrs[i],webStrs[i]);
}
if(match){
return uri;
}
}
}
return null;
}
});
registration.addUrlPatterns("/*");
registration.setName("defaultFilter");
registration.setOrder(10); //值越小,Filter越靠前。
return registration;
}