编译时为方法添加注解:如何通过@EnableCRUD自动生成Spring相关注解
我正在为一些实体实现CRUD操作,想做一个自定义注解@EnableCRUD,让它能自动给目标类和方法添加对应的Spring MVC注解。举个例子:
原代码:
@EnableCRUD(base="/tests", get="testGet") class CrudTestResource{ public List<String> testGet(String id){ return new ArrayList(); } }
期望编译后(或运行时生效)的代码等价于:
@RestController @RequestMapping("/tests") class CrudTestResource{ @GetMapping("/{id}") public List<String> testGet(@PathVariable String id){ return new ArrayList(); } }
请问这个需求该怎么实现?
嗨,这个需求其实有两种主流实现思路,一种是编译时注解处理器(APT),另一种是Spring运行时的字节码增强/BeanPostProcessor,我给你详细拆解下:
方法一:编译时注解处理器(APT)
这种方式会在编译阶段直接修改类的字节码或者生成代理类,优点是运行时没有额外开销,缺点是需要编写注解处理器代码,相对繁琐一点。
步骤1:定义自定义注解@EnableCRUD
先创建你的注解类,指定作用在类上,并且保留到编译阶段:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) // 保留到编译阶段,供处理器处理 public @interface EnableCRUD { String base(); String get(); // 指定要添加@GetMapping的方法名 }
步骤2:编写注解处理器
继承AbstractProcessor,处理@EnableCRUD注解,然后通过字节码操作库(比如ASM或Javapoet)修改目标类:
- 给类添加
@RestController和@RequestMapping(base)注解; - 根据
get()属性找到对应的方法,给方法添加@GetMapping("/{id}"),同时给方法的第一个参数添加@PathVariable注解。
举个简化的思路:用ASM扫描带有@EnableCRUD的类,直接修改类的字节码,注入需要的注解信息。你可以参考ASM的官方文档来实现具体的注解注入逻辑。
步骤3:注册注解处理器
在项目的src/main/resources/META-INF/services/javax.annotation.processing.Processor文件中,添加你的注解处理器全类名,这样Java编译器在编译时会自动调用它。
方法二:Spring运行时增强(BeanPostProcessor + 字节码操作)
如果不想在编译阶段处理,也可以利用Spring的扩展机制,在Bean初始化时动态修改注解信息,这种方式更灵活,适合Spring项目场景。
步骤1:定义@EnableCRUD注解(保留到运行时)
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) // 保留到运行时,供Spring处理 public @interface EnableCRUD { String base(); String get(); }
步骤2:实现BeanPostProcessor
创建一个BeanPostProcessor的实现类,在Bean初始化前检查类是否带有@EnableCRUD注解,如果有,就通过字节码操作(比如ASM或CGLIB)修改类和方法的注解:
@Component public class CrudAnnotationEnhancer implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { Class<?> beanClass = bean.getClass(); EnableCRUD crudAnnotation = beanClass.getAnnotation(EnableCRUD.class); if (crudAnnotation != null) { // 1. 给类添加@RestController和@RequestMapping注解 enhanceClassAnnotations(beanClass, crudAnnotation.base()); // 2. 找到指定方法,添加@GetMapping和参数注解 try { Method targetMethod = beanClass.getMethod(crudAnnotation.get(), String.class); enhanceMethodAnnotations(targetMethod); enhanceParameterAnnotations(targetMethod); } catch (NoSuchMethodException e) { throw new RuntimeException("指定的方法不存在:" + crudAnnotation.get(), e); } } return bean; } // 工具方法:给类添加Spring MVC注解(需用ASM实现字节码修改) private void enhanceClassAnnotations(Class<?> clazz, String basePath) { // 省略ASM具体实现:给类添加@RestController和@RequestMapping(basePath) } // 工具方法:给方法添加@GetMapping注解 private void enhanceMethodAnnotations(Method method) { // 省略ASM具体实现:给方法添加@GetMapping("/{id}") } // 工具方法:给方法参数添加@PathVariable注解 private void enhanceParameterAnnotations(Method method) { // 省略ASM具体实现:给方法第一个参数添加@PathVariable } }
注意:Java反射本身不允许修改已加载类的注解,所以必须用ASM这类字节码操作库来修改类的字节码,才能让Spring识别到新增的注解。
额外小建议
如果你只是想简化Spring MVC的CRUD代码,其实Spring官方有现成的解决方案:Spring Data REST,它可以自动为Spring Data Repositories生成REST接口,完全不需要手动写控制器方法,可能更贴合你的需求。不过如果一定要自己实现自定义注解,上面两种方式都能满足你的要求。
内容的提问来源于stack exchange,提问作者navid_gh

