兄弟们,大家写 Controller 的时候,是不是总感觉自己像个 “复制粘贴工程师”?
比如写个用户接口,先校验参数非空,再 try-catch 包一圈,最后还要把返回结果套上Result.success()或者Result.fail();下一个订单接口,嘿,好家伙,又是这套流程 —— 参数校验、异常捕获、结果封装,连注释都长得差不多。更气人的是,万一某天产品说 “返回格式要加个 requestId”,你就得打开十几个 Controller,改完还得挨个测,手都快按抽筋了。
今天咱就来搞个大事情:用一个自定义注解,把这些破事儿全搞定!以后写 Controller,咱只专注业务逻辑,那些重复的 “边角料”,让注解帮咱扛了。全程大白话,不整虚的,保证你看完就能上手,看完就想把公司项目里的 Controller 全重构一遍!
一、先吐个槽:你写的 Controller,可能一半是 “废话”
先给大家看个 “标准” 的 Controller 代码,我赌五毛,你电脑里绝对有差不多的:
你品,你细品:一个接口真正的业务逻辑,可能就userService.addUser(userDTO)这一行,剩下的全是 “重复性劳动”。更要命的是,一旦项目里有几十个 Controller、上百个接口,这些 “废话代码” 会让项目变得巨难维护 —— 比如想加个 “所有接口都要校验 token”,你得一个个加拦截;想改返回格式,你得一个个改Result。这时候肯定有人会说:“我用了 Bean Validation 啊,比如 @NotNull、@Min,能少写点参数校验代码!”
确实,Bean Validation 能解决一部分问题,但也就仅限于参数校验。异常捕获、结果封装、甚至接口权限校验这些事儿,你该写还是得写。而且如果遇到复杂校验(比如 “用户年龄大于 18 才能注册”),光靠注解还不够,你还得写自定义校验器,最后还是逃不掉 “代码冗余” 的坑。
那有没有一种办法,能把这些 “非业务逻辑” 全抽离出去,让 Controller 只专注于 “做什么业务”,而不是 “怎么处理参数 / 异常 / 返回值”?
答案就是:自定义注解 + AOP + Spring 扩展。咱们今天的主角 ——@AutoController注解,就是干这个的!
二、核心思路:让注解成为 Controller 的 “全能管家”
在开始写代码之前,咱先搞明白一个事儿:为什么一个注解能搞定这么多活儿?
其实原理很简单,你可以把@AutoController理解成一个 “全能管家”。以前你得自己干 “开门(参数校验)、看家(异常捕获)、送客人(结果封装)” 这些活儿,现在你给 Controller 贴个@AutoController标签,这个管家就会自动帮你把这些活儿全干了。
具体怎么实现呢?咱们分三步走:
- 定义注解:就是创建@AutoController这个 “标签”,规定它能贴在哪些地方(比如类上、方法上),能保存哪些配置(比如是否跳过参数校验)。
- 写 “管家逻辑”:用 AOP(面向切面编程)或者 Spring 的HandlerMethodArgumentResolver、ResponseBodyAdvice这些扩展点,实现 “参数校验、异常捕获、结果封装” 的具体逻辑。
- 让 Spring 识别:把注解和 “管家逻辑” 注册到 Spring 容器里,让 Spring 知道 “遇到贴了 @AutoController 的 Controller,就用这个管家”。
听起来好像有点复杂?别怕,咱一步步来,每一行代码都给你讲明白,保证你看完就能抄走用。
三、实战:手把手写一个 “干翻 Controller” 的注解
咱们基于 Spring Boot 2.7.x 来写,毕竟现在大部分公司都用这个版本。先准备好依赖,在pom.xml里加这些(如果是 Gradle,对应转一下就行):
依赖搞定,咱们开始写核心代码。
第一步:定义 “全能管家” 注解 ——@AutoController
先写注解本身,代码很简单,重点看注释:
看到没?这个注解带了三个 “配置项”,都是咱们日常开发常用的:是否跳过参数校验、操作描述、是否需要登录。这样一来,注解就不是死的,能根据不同接口的需求灵活调整 —— 比如登录接口,咱们就可以设置needLogin = false,避免自己拦截自己。
第二步:写 “管家逻辑” 之一 —— 参数校验(不用再写 if-else)
以前咱们用 Bean Validation 的@NotNull、@Min这些注解,还得在 Controller 里加@Valid,然后用BindingResult手动处理校验结果,还是有点麻烦。现在咱们用@AutoController,直接把这些活儿全自动化。
先写一个 “参数校验器”,用 Spring 的MethodArgumentResolver扩展点,专门处理贴了@AutoController的接口的参数:
这里有个小细节:咱们用BusinessException这个自定义异常来抛校验错误,后面会统一捕获这个异常,不用在每个接口里写if (errors.hasErrors())了。当然,还得把这个参数解析器注册到 Spring 里,不然 Spring 不认识它。写个配置类:
到这儿,参数校验的逻辑就搞定了。以后咱们在 DTO 里加@NotNull这些注解,再给 Controller 贴个@AutoController,就不用手动处理校验结果了 —— 校验失败会自动抛异常,校验成功直接把参数传给 Controller 方法。
第三步:写 “管家逻辑” 之二 —— 统一异常捕获(不用再写 try-catch)
以前每个接口都要包try-catch,现在咱们用 Spring 的@RestControllerAdvice+@ExceptionHandler,写一个全局异常处理器,统一捕获所有异常,包括咱们刚才抛的BusinessException。
先定义两个自定义异常(业务异常和登录异常),方便区分:
然后写全局异常处理器:
这里有个关键点:咱们返回的是Result对象,这是统一的返回格式。咱们顺便把Result类也写了,以后所有接口都用这个格式返回:
到这儿,异常捕获的逻辑就搞定了。以后 Controller 里不用再写try-catch了 —— 不管是业务异常、登录异常,还是未知异常,都会被这个处理器统一捕获,然后返回统一格式的错误信息。
第四步:写 “管家逻辑” 之三 —— 登录校验(不用再写 token 判断)
很多接口都需要登录才能访问,以前咱们可能会在每个接口里判断token是否有效,或者用 Spring Security。但 Spring Security 对新手不太友好,咱们用@AutoController的needLogin属性,配合 AOP 来做登录校验,更简单直观。
先写一个 AOP 切面,专门处理登录校验:
这里有两个辅助类:UserContextHolder(用 ThreadLocal 存用户信息)和LoginService(模拟登录逻辑),咱们也写一下:
另外,别忘了在请求结束后清除 ThreadLocal 里的用户信息,不然会有内存泄漏风险。写一个拦截器:
然后在WebMvcConfig里注册这个拦截器:
到这儿,登录校验的逻辑也搞定了。以后想让接口需要登录,就不用在方法里写if (token无效) throw 异常了 —— 只要@AutoController的needLogin是 true(默认就是 true),AOP 会自动帮你校验 token。
第五步:写 “管家逻辑” 之四 —— 统一结果封装(不用再写 Result.success)
最后一步,咱们解决 “每个接口都要写Result.success()” 的问题。用 Spring 的ResponseBodyAdvice扩展点,在返回结果给前端之前,自动把 Controller 的返回值封装成Result对象。
写一个统一结果封装器:
这里有个细节:咱们判断了返回值是不是Result类型,如果是就不封装了 —— 避免出现Result<Result<UserVO>>这种嵌套格式。另外,如果 Controller 方法返回 void(比如删除接口),就封装成不带数据的Result.success(message)。
四、见证奇迹:用 @AutoController 重构 Controller
现在,咱们把最开始那个 “臃肿” 的 UserController,用@AutoController重构一下,看看效果:
再看一下 DTO 类(用 Bean Validation 注解做参数校验):
对比一下重构前后的代码:
- 重构前:每个接口平均 15 行代码,其中 12 行是参数校验、try-catch、Result 封装;
- 重构后:每个接口平均 3 行代码,只有核心业务逻辑!
而且不管是参数校验、登录校验,还是异常处理、结果封装,全都是自动的:
- 如果你传的参数不符合规则(比如手机号格式错),会自动返回{“code”:400,”message”:”phone:手机号格式错误”,”data”:null};
- 如果你没传 token 就访问需要登录的接口,会自动返回{“code”:401,”message”:”登录已过期,请重新登录”,”data”:null};
- 如果你调用删除接口,会自动返回{“code”:200,”message”:”删除用户成功”,”data”:null};
- 如果 Service 里抛了new BusinessException(“用户已存在”),会自动返回{“code”:400,”message”:”用户已存在”,”data”:null};
- 如果出现未知异常(比如数据库连接失败),会自动返回{“code”:500,”message”:”系统开了个小差,请稍后再试~”,”data”:null},同时后台打印完整堆栈。
五、进阶:让 @AutoController 更灵活(满足复杂场景)
咱们写的@AutoController已经能满足大部分场景了,但实际开发中可能会有更复杂的需求。比如:
- 某些接口需要特殊的参数校验规则(比如 “用户注册时,手机号必须未被使用”);
- 某些接口需要自定义返回格式(比如给第三方接口返回 XML 格式);
- 某些接口需要多角色校验(比如只有管理员能删除用户)。
别担心,咱们的@AutoController可以轻松扩展,咱们来一个个解决。
1. 扩展:复杂参数校验(自定义校验器)
比如 “用户注册时,手机号必须未被使用”,这种需要查数据库的校验,Bean Validation 的默认注解搞不定,咱们可以写个自定义校验注解,配合@AutoController使用。
先写自定义校验注解@PhoneNotExists:
然后写校验器PhoneNotExistsValidator:
然后在 UserDTO 里用这个注解:
最后在 Controller 的 Service 方法里,指定分组(新增用 AddGroup):
这样一来,当你调用新增用户接口,传手机号 13800138000 时,@AutoController会自动触发这个自定义校验,返回 “手机号已被注册” 的错误信息 —— 不用在 Controller 里写任何额外代码!
2. 扩展:自定义返回格式(比如 XML)
如果某些接口需要返回 XML 格式(比如给第三方系统),咱们可以在@AutoController里加个produce属性,指定返回格式,然后在AutoControllerResponseAdvice里处理。
先修改@AutoController注解,加个produce属性:
然后修改AutoControllerResponseAdvice,根据produce属性设置返回格式:
然后在需要返回 XML 的接口上设置produce:
最后,在pom.xml里加 XML 解析依赖:
这样一来,调用这个接口时,会自动返回 XML 格式的结果,其他接口还是返回 JSON—— 灵活得很!
3. 扩展:多角色校验(比如只有管理员能操作)
比如 “删除用户” 接口,只有管理员能调用,普通用户调用会报错。咱们可以在@AutoController里加个roles属性,指定允许的角色,然后在 AOP 切面里校验。
先修改@AutoController注解,加个roles属性:
然后修改AutoControllerLoginAspect的doLoginCheck方法,加角色校验逻辑:
然后在需要角色校验的接口上设置roles:
这样一来,普通用户调用删除接口时,会自动返回 “没有权限操作,请联系管理员” 的错误信息 —— 不用在 Service 里写if (user.getRole() != admin) throw 异常了!
六、注意事项:别踩这些坑!
咱们的@AutoController虽然好用,但也有一些注意事项,避免你踩坑:
1. 不要重复封装 Result
如果你的 Controller 方法已经返回了Result对象,一定要确保@AutoController的supports方法会跳过它 —— 不然会出现Result<Result<UserVO>>这种嵌套格式。咱们之前在AutoControllerResponseAdvice里已经加了判断:!returnType.getParameterType().isAssignableFrom(Result.class),所以只要你不手动返回Result,就没问题。
2. ThreadLocal 要记得清除
咱们用ThreadLocal存用户信息,一定要在请求结束后清除(比如用拦截器的afterCompletion方法),不然会导致内存泄漏 —— 因为 Tomcat 的线程是复用的,ThreadLocal 里的信息会一直存在,直到线程被销毁。
3. 性能问题:AOP 会不会影响性能?
很多人担心 AOP 会影响性能,其实大可不必。AOP 用的是动态代理,拦截方法的开销非常小,对于大部分业务系统来说,完全可以忽略不计。而且咱们的 AOP 切面只拦截贴了@AutoController的接口,不会对其他接口造成影响。
如果你的系统是超高并发(比如每秒几万请求),可以考虑用 AspectJ 的编译时织入(而不是 Spring AOP 的运行时织入),进一步降低性能开销。
4. 兼容性问题:和其他框架冲突吗?
咱们的@AutoController基于 Spring 的扩展点实现,和 Spring Boot、Spring Cloud 等框架完全兼容。如果你的项目里用了 Spring Security、Shiro 等安全框架,只需要调整一下登录校验的逻辑(比如用 Security 的SecurityContextHolder代替咱们的UserContextHolder),就能完美配合。
七、总结:一个注解,解放你的双手!
看到这儿,你应该明白为什么说 “一个注解干翻所有 Controller” 了吧?
以前写 Controller,你得写参数校验、try-catch、Result 封装、登录校验,这些活儿占了 80% 的代码量,却没什么技术含量;现在你只需要贴个@AutoController,这些活儿全由注解自动搞定,你可以专注于真正有价值的业务逻辑 —— 写代码的效率至少提升 3 倍!
更重要的是,项目的可维护性也大大提升了:
- 要改参数校验规则?改 DTO 的注解就行,不用改 Controller;
- 要改返回格式?改Result类或AutoControllerResponseAdvice就行,不用改几十个接口;
- 要加新的拦截逻辑(比如接口限流)?加个 AOP 切面就行,不用侵入业务代码。
文章来自:51CTO
