今天咱就来搞个大事情:用一个自定义注解,把这些破事儿全搞定!以后写 Controller,咱只专注业务逻辑,那些重复的 “边角料”,让注解帮咱扛了。全程大白话,不整虚的,保证你看完就能上手,看完就想把公司项目里的 Controller 全重构一遍!
兄弟们,大家写 Controller 的时候,是不是总感觉自己像个 “复制粘贴工程师”?
比如写个用户接口,先校验参数非空,再 try-catch 包一圈,最后还要把返回结果套上Result.success()或者Result.fail();下一个订单接口,嘿,好家伙,又是这套流程 —— 参数校验、异常捕获、结果封装,连注释都长得差不多。更气人的是,万一某天产品说 “返回格式要加个 requestId”,你就得打开十几个 Controller,改完还得挨个测,手都快按抽筋了。
今天咱就来搞个大事情:用一个自定义注解,把这些破事儿全搞定!以后写 Controller,咱只专注业务逻辑,那些重复的 “边角料”,让注解帮咱扛了。全程大白话,不整虚的,保证你看完就能上手,看完就想把公司项目里的 Controller 全重构一遍!
一、先吐个槽:你写的 Controller,可能一半是 “废话”
先给大家看个 “标准” 的 Controller 代码,我赌五毛,你电脑里绝对有差不多的:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public Result<UserVO> addUser(@RequestBody UserDTO userDTO) {
// 第一步:参数校验(每个接口都要写一遍,烦!)
if (userDTO.getUsername() == null || userDTO.getUsername().trim().isEmpty()) {
return Result.fail("用户名不能为空");
}
if (userDTO.getAge() == null || userDTO.getAge() < 0 || userDTO.getAge() > 150) {
return Result.fail("年龄必须在0-150之间");
}
if (userDTO.getPhone() == null || !userDTO.getPhone().matches("^1[3-9]\\d{9}$")) {
return Result.fail("手机号格式错误");
}
// 第二步:try-catch包起来(怕抛异常影响全局,每个接口都包,累!)
try {
UserVO userVO = userService.addUser(userDTO);
// 第三步:封装返回结果(统一格式,但每个接口都要写,蠢!)
return Result.success(userVO, "新增用户成功");
} catch (BusinessException e) {
// 业务异常要返回具体错误信息
return Result.fail(e.getCode(), e.getMessage());
} catch (Exception e) {
// 未知异常要返回通用错误
log.error("新增用户失败", e);
return Result.fail("系统异常,请联系管理员");
}
}
// 下面还有update、delete、getById...全是重复的校验、try-catch、结果封装
@PutMapping("/update")
public Result<UserVO> updateUser(@RequestBody UserDTO userDTO) {
// 又是参数校验...
// 又是try-catch...
// 又是结果封装...
}
}
你品,你细品:一个接口真正的业务逻辑,可能就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,对应转一下就行):
<!-- 核心web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP依赖,用来做切面拦截 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 参数校验依赖,替代手写if-else -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Lombok,少写getter/setter -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
依赖搞定,咱们开始写核心代码。
第一步:定义 “全能管家” 注解 ——@AutoController
先写注解本身,代码很简单,重点看注释:
import java.lang.annotation.*;
/**
* 一个注解干翻所有Controller的核心注解
* 贴在Controller类或方法上,自动实现:参数校验、异常捕获、结果封装
*/
@Target({ElementType.TYPE, ElementType.METHOD}) // 能贴在类上或方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效(因为要在程序跑的时候拦截)
@Documented // 生成文档时会显示这个注解
public @interface AutoController {
/**
* 是否跳过参数校验(默认不跳过)
* 比如某些查询接口不需要校验参数,就可以设置为true
*/
boolean skipValidate() default false;
/**
* 操作描述(比如“新增用户”“删除订单”,用于日志打印)
*/
String operation() default "";
/**
* 是否需要登录(默认需要)
* 比如登录接口本身不需要登录,就设置为false
*/
boolean needLogin() default true;
}
看到没?这个注解带了三个 “配置项”,都是咱们日常开发常用的:是否跳过参数校验、操作描述、是否需要登录。这样一来,注解就不是死的,能根据不同接口的需求灵活调整 —— 比如登录接口,咱们就可以设置needLogin = false,避免自己拦截自己。
第二步:写 “管家逻辑” 之一 —— 参数校验(不用再写 if-else)
以前咱们用 Bean Validation 的@NotNull、@Min这些注解,还得在 Controller 里加@Valid,然后用BindingResult手动处理校验结果,还是有点麻烦。现在咱们用@AutoController,直接把这些活儿全自动化。
先写一个 “参数校验器”,用 Spring 的MethodArgumentResolver扩展点,专门处理贴了@AutoController的接口的参数:
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import javax.servlet.http.HttpServletRequest;
/**
* 自动参数校验器:处理@AutoController注解的接口参数
*/
@Component
@RequiredArgsConstructor // Lombok的注解,自动生成构造方法注入依赖
public class AutoControllerArgumentResolver implements HandlerMethodArgumentResolver {
// Spring自带的校验器,不用自己写
private final Validator validator;
// 第一步:判断当前参数是否需要用这个解析器处理
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 条件:1. 方法或类上有@AutoController注解;2. 不需要跳过校验;3. 参数是实体类(不是基本类型)
AutoController autoController = getAutoControllerAnnotation(parameter);
return autoController != null
&& !autoController.skipValidate()
&& !parameter.getParameterType().isPrimitive();
}
// 第二步:处理参数(核心逻辑:校验参数,并抛出异常)
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 1. 获取请求参数(比如@RequestBody的UserDTO)
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
Object parameterValue = request.getAttribute(parameter.getParameterName());
if (parameterValue == null) {
// 如果参数为空,直接抛业务异常
throw new BusinessException("参数不能为空");
}
// 2. 用Spring的校验器校验参数(比如@NotNull、@Min这些注解)
Errors errors = new BeanPropertyBindingResult(parameterValue, parameter.getParameterName());
validator.validate(parameterValue, errors);
// 3. 如果有校验错误,拼接错误信息并抛异常
if (errors.hasErrors()) {
StringBuilder errorMsg = new StringBuilder();
errors.getFieldErrors().forEach(fieldError -> {
errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(";");
});
// 抛自定义的业务异常,后面会统一处理
throw new BusinessException(errorMsg.toString().substring(0, errorMsg.length() - 1));
}
// 4. 校验通过,返回参数(给Controller方法用)
return parameterValue;
}
// 工具方法:获取方法或类上的@AutoController注解
private AutoController getAutoControllerAnnotation(MethodParameter parameter) {
// 先看方法上有没有
AutoController methodAnnotation = parameter.getMethodAnnotation(AutoController.class);
if (methodAnnotation != null) {
return methodAnnotation;
}
// 方法上没有,再看类上有没有
return parameter.getContainingClass().getAnnotation(AutoController.class);
}
}
这里有个小细节:咱们用BusinessException这个自定义异常来抛校验错误,后面会统一捕获这个异常,不用在每个接口里写if (errors.hasErrors())了。当然,还得把这个参数解析器注册到 Spring 里,不然 Spring 不认识它。写个配置类:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final AutoControllerArgumentResolver autoControllerArgumentResolver;
// 构造方法注入参数解析器
public WebMvcConfig(AutoControllerArgumentResolver autoControllerArgumentResolver) {
this.autoControllerArgumentResolver = autoControllerArgumentResolver;
}
// 注册参数解析器
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 把咱们的解析器加进去,注意顺序(加在前面,优先执行)
resolvers.add(0, autoControllerArgumentResolver);
}
}
到这儿,参数校验的逻辑就搞定了。以后咱们在 DTO 里加@NotNull这些注解,再给 Controller 贴个@AutoController,就不用手动处理校验结果了 —— 校验失败会自动抛异常,校验成功直接把参数传给 Controller 方法。
第三步:写 “管家逻辑” 之二 —— 统一异常捕获(不用再写 try-catch)
以前每个接口都要包try-catch,现在咱们用 Spring 的@RestControllerAdvice+@ExceptionHandler,写一个全局异常处理器,统一捕获所有异常,包括咱们刚才抛的BusinessException。
先定义两个自定义异常(业务异常和登录异常),方便区分:
// 业务异常:比如参数错误、业务逻辑错误(如“用户已存在”)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BusinessException extends RuntimeException {
private Integer code = 400; // 默认错误码400(.bad request)
private String message; // 错误信息
// 重载构造方法,方便只传错误信息
public BusinessException(String message) {
this.message = message;
}
}
// 登录异常:比如“未登录”“token过期”
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginException extends RuntimeException {
private Integer code = 401; // 默认错误码401(unauthorized)
private String message = "请先登录";
}
然后写全局异常处理器:
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器:统一处理所有异常,配合@AutoController使用
*/
@RestControllerAdvice // 全局生效,只处理@RestController的异常
@Slf4j
public class AutoControllerExceptionHandler {
// 处理业务异常(BusinessException)
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
// 打印错误日志(方便排查问题)
log.warn("业务异常:{}", e.getMessage());
// 返回错误结果(统一格式)
return Result.fail(e.getCode(), e.getMessage());
}
// 处理登录异常(LoginException)
@ExceptionHandler(LoginException.class)
public Result<?> handleLoginException(LoginException e) {
log.warn("登录异常:{}", e.getMessage());
return Result.fail(e.getCode(), e.getMessage());
}
// 处理未知异常(所有没被捕获的异常)
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 返回500状态码
public Result<?> handleUnknownException(Exception e) {
// 未知异常要打印完整堆栈(方便排查)
log.error("系统异常:", e);
// 返回通用错误信息(别把具体异常信息返回给前端,不安全)
return Result.fail(500, "系统开了个小差,请稍后再试~");
}
}
这里有个关键点:咱们返回的是Result对象,这是统一的返回格式。咱们顺便把Result类也写了,以后所有接口都用这个格式返回:
import lombok.Data;
/**
* 统一返回结果类
* 所有接口都返回这个格式,前端好处理
*/
@Data
public class Result<T> {
// 状态码:200成功,400业务错误,401未登录,500系统错误
private Integer code;
// 提示信息
private String message;
// 业务数据(成功时返回)
private T data;
// 成功:带数据
public static <T> Result<T> success(T data, String message) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage(message);
result.setData(data);
return result;
}
// 成功:不带数据(比如删除接口)
public static <T> Result<T> success(String message) {
return success(null, message);
}
// 失败:带错误码和信息
public static <T> Result<T> fail(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(null);
return result;
}
// 失败:默认错误码400
public static <T> Result<T> fail(String message) {
return fail(400, message);
}
}
到这儿,异常捕获的逻辑就搞定了。以后 Controller 里不用再写try-catch了 —— 不管是业务异常、登录异常,还是未知异常,都会被这个处理器统一捕获,然后返回统一格式的错误信息。
第四步:写 “管家逻辑” 之三 —— 登录校验(不用再写 token 判断)
很多接口都需要登录才能访问,以前咱们可能会在每个接口里判断token是否有效,或者用 Spring Security。但 Spring Security 对新手不太友好,咱们用@AutoController的needLogin属性,配合 AOP 来做登录校验,更简单直观。
先写一个 AOP 切面,专门处理登录校验:
复制
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* 登录校验切面:配合@AutoController的needLogin属性使用
*/
@Aspect// 标记这是一个AOP切面
@Component// 注册到Spring容器
@Slf4j
@RequiredArgsConstructor
publicclass AutoControllerLoginAspect {
// 假设这是登录服务,用来校验token是否有效(实际项目里替换成你的登录逻辑)
privatefinal LoginService loginService;
// 切入点:所有贴了@AutoController注解的方法或类
@Pointcut("@annotation(com.xxx.annotation.AutoController) || @within(com.xxx.annotation.AutoController)")
public void autoControllerPointcut() {}
// 前置通知:在方法执行前做登录校验
@Before("autoControllerPointcut() && @annotation(autoController)")
public void doLoginCheck(AutoController autoController) {
// 1. 判断是否需要登录(如果needLogin为false,直接跳过)
if (!autoController.needLogin()) {
log.debug("接口无需登录,跳过校验");
return;
}
// 2. 获取请求头里的token(实际项目里可能是Authorization头)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("token");
// 3. 校验token(调用登录服务,实际项目里替换成你的逻辑)
boolean isTokenValid = loginService.validateToken(token);
if (!isTokenValid) {
// token无效或过期,抛登录异常(会被全局异常处理器捕获)
thrownew LoginException("登录已过期,请重新登录");
}
// 4. token有效,还可以把用户信息存入ThreadLocal(方便后续业务使用)
UserInfo userInfo = loginService.getUserInfoByToken(token);
UserContextHolder.setUserInfo(userInfo);
log.debug("登录校验通过,当前用户:{}", userInfo.getUsername());
}
}
这里有两个辅助类:UserContextHolder(用 ThreadLocal 存用户信息)和LoginService(模拟登录逻辑),咱们也写一下:
// UserContextHolder:用ThreadLocal存用户信息,避免参数传递
publicclass UserContextHolder {
privatestaticfinal ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal<>();
// 设置用户信息
public static void setUserInfo(UserInfo userInfo) {
USER_INFO_THREAD_LOCAL.set(userInfo);
}
// 获取用户信息(比如在Service里用)
public static UserInfo getUserInfo() {
return USER_INFO_THREAD_LOCAL.get();
}
// 清除用户信息(避免内存泄漏)
public static void clear() {
USER_INFO_THREAD_LOCAL.remove();
}
}
// LoginService:模拟登录逻辑(实际项目里替换成你的Redis或数据库逻辑)
@Service
publicclass LoginService {
// 模拟校验token(实际项目里查Redis或数据库)
public boolean validateToken(String token) {
if (token == null || token.trim().isEmpty()) {
returnfalse;
}
// 这里只是模拟,实际要判断token是否存在、是否过期
return token.startsWith("valid_token_");
}
// 模拟根据token获取用户信息
public UserInfo getUserInfoByToken(String token) {
// 实际项目里从token解析用户ID,再查数据库
UserInfo userInfo = new UserInfo();
userInfo.setUserId(1L);
userInfo.setUsername("test_user");
userInfo.setRole("admin");
return userInfo;
}
}
// UserInfo:用户信息类
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclass UserInfo {
private Long userId;
private String username;
private String role;
}
另外,别忘了在请求结束后清除 ThreadLocal 里的用户信息,不然会有内存泄漏风险。写一个拦截器:
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 清除ThreadLocal的拦截器:请求结束后清除用户信息
*/
@Component
public class ClearUserContextInterceptor implements HandlerInterceptor {
// 请求处理完成后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) throws Exception {
UserContextHolder.clear();
}
}
然后在WebMvcConfig里注册这个拦截器:
@Configuration
publicclass WebMvcConfig implements WebMvcConfigurer {
// 省略之前的代码...
privatefinal ClearUserContextInterceptor clearUserContextInterceptor;
// 构造方法注入拦截器
public WebMvcConfig(AutoControllerArgumentResolver autoControllerArgumentResolver,
ClearUserContextInterceptor clearUserContextInterceptor) {
this.autoControllerArgumentResolver = autoControllerArgumentResolver;
this.clearUserContextInterceptor = clearUserContextInterceptor;
}
// 注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(clearUserContextInterceptor)
.addPathPatterns("/**"); // 所有请求都拦截
}
}
到这儿,登录校验的逻辑也搞定了。以后想让接口需要登录,就不用在方法里写if (token无效) throw 异常了 —— 只要@AutoController的needLogin是 true(默认就是 true),AOP 会自动帮你校验 token。
第五步:写 “管家逻辑” 之四 —— 统一结果封装(不用再写 Result.success)
最后一步,咱们解决 “每个接口都要写Result.success()” 的问题。用 Spring 的ResponseBodyAdvice扩展点,在返回结果给前端之前,自动把 Controller 的返回值封装成Result对象。
写一个统一结果封装器:
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* 统一结果封装器:自动把Controller的返回值封装成Result格式
*/
@ControllerAdvice// 全局生效
publicclass AutoControllerResponseAdvice implements ResponseBodyAdvice<Object> {
// 第一步:判断是否需要封装结果(只处理贴了@AutoController的接口)
@Override
publicboolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 方法或类上有@AutoController注解,并且返回值不是Result类型(避免重复封装)
AutoController autoController = getAutoControllerAnnotation(returnType);
return autoController != null
&& !returnType.getParameterType().isAssignableFrom(Result.class);
}
// 第二步:封装结果(把Controller的返回值变成Result.success)
@Override
publicObject beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 获取@AutoController的operation属性(作为成功提示信息)
AutoController autoController = getAutoControllerAnnotation(returnType);
String message = autoController.operation() + "成功";
// 如果返回值是void(比如删除接口),就封装成不带数据的Result
if (body == null || returnType.getParameterType().isAssignableFrom(Void.class)) {
return Result.success(message);
}
// 否则,封装成带数据的Result
return Result.success(body, message);
}
// 工具方法:获取@AutoController注解
private AutoController getAutoControllerAnnotation(MethodParameter returnType) {
AutoController methodAnnotation = returnType.getMethodAnnotation(AutoController.class);
if (methodAnnotation != null) {
return methodAnnotation;
}
return returnType.getContainingClass().getAnnotation(AutoController.class);
}
}
这里有个细节:咱们判断了返回值是不是Result类型,如果是就不封装了 —— 避免出现Result<Result<UserVO>>这种嵌套格式。另外,如果 Controller 方法返回 void(比如删除接口),就封装成不带数据的Result.success(message)。
四、见证奇迹:用 @AutoController 重构 Controller
现在,咱们把最开始那个 “臃肿” 的 UserController,用@AutoController重构一下,看看效果:
@RestController
@RequestMapping("/user")
@AutoController(operation = "用户管理") // 类上贴注解,默认所有方法都生效
public class UserController {
@Autowired
private UserService userService;
// 新增用户:不用写参数校验、try-catch、Result封装
@PostMapping("/add")
@AutoController(operation = "新增用户") // 方法上的注解会覆盖类上的
public UserVO addUser(@RequestBody UserDTO userDTO) {
// 只有核心业务逻辑!
returnuserService.addUser(userDTO);
}
// 更新用户:同样不用写边角料代码
@PutMapping("/update")
@AutoController(operation = "更新用户")
publicUserVOupdateUser(@RequestBody UserDTO userDTO) {
// 核心业务逻辑,没了!
returnuserService.updateUser(userDTO);
}
// 删除用户:返回void,会自动封装成Result.success("删除用户成功")
@DeleteMapping("/delete/{id}")
@AutoController(operation = "删除用户")
publicvoiddeleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
// 查询用户:跳过参数校验(比如id可以为null,查所有用户)
@GetMapping("/get")
@AutoController(operation = "查询用户", skipValidate = true)
publicUserVOgetUser(@RequestParam(required = false) Long id) {
returnuserService.getUserById(id);
}
// 登录接口:不需要登录(不然自己拦截自己)
@PostMapping("/login")
@AutoController(operation = "用户登录", needLogin = false, skipValidate = false)
publicLoginVOlogin(@RequestBody LoginDTO loginDTO) {
returnuserService.login(loginDTO);
}
}
再看一下 DTO 类(用 Bean Validation 注解做参数校验):
// UserDTO:用户新增/更新的DTO
@Data
public class UserDTO {
@NotNull(message = "用户ID不能为空", groups = UpdateGroup.class) // 更新时需要ID
@Null(message = "新增用户不能指定ID", groups = AddGroup.class) // 新增时不能有ID
private Long id;
@NotNull(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20之间")
private String username;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@NotNull(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
private String phone;
}
// LoginDTO:登录DTO
@Data
public class LoginDTO {
@NotNull(message = "用户名不能为空")
private String username;
@NotNull(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
}
// 分组校验用的接口(比如新增和更新的校验规则不同)
public interface AddGroup {}
public interface UpdateGroup {}
对比一下重构前后的代码:
- 重构前:每个接口平均 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:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.FIELD}) // 只能贴在字段上
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNotExistsValidator.class) // 指定校验器
public@interface PhoneNotExists {
// 校验失败的提示信息
String message() default"手机号已被注册";
// 分组(和Bean Validation的分组校验配合)
Class<?>[] groups() default {};
// 负载(很少用)
Class<? extends Payload>[] payload() default {};
}
然后写校验器PhoneNotExistsValidator:
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
/**
* 自定义校验器:校验手机号是否已被注册
*/
@Component
@RequiredArgsConstructor
publicclass PhoneNotExistsValidator implements ConstraintValidator<PhoneNotExists, String> {
privatefinal UserService userService;
// 校验逻辑:调用Service查数据库,判断手机号是否存在
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (phone == null || phone.trim().isEmpty()) {
// 手机号为空的校验,交给@NotNull处理,这里不处理
returntrue;
}
// 查数据库,判断手机号是否已被注册
return !userService.isPhoneExists(phone);
}
}
然后在 UserDTO 里用这个注解:
@Data
public class UserDTO {
// 省略其他字段...
@NotNull(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误")
@PhoneNotExists(message = "手机号已被注册", groups = AddGroup.class) // 新增时校验手机号是否存在
private String phone;
}
最后在 Controller 的 Service 方法里,指定分组(新增用 AddGroup):
@Service
publicclass UserService {
// 新增用户:指定用AddGroup的校验规则
public UserVO addUser(@Validated(AddGroup.class) UserDTO userDTO) {
// 业务逻辑...
}
// 更新用户:指定用UpdateGroup的校验规则
public UserVO updateUser(@Validated(UpdateGroup.class) UserDTO userDTO) {
// 业务逻辑...
}
// 校验手机号是否存在
public boolean isPhoneExists(String phone) {
// 查数据库,返回true表示已存在,false表示不存在
// 模拟逻辑:假设手机号13800138000已被注册
return"13800138000".equals(phone);
}
}
这样一来,当你调用新增用户接口,传手机号 13800138000 时,@AutoController会自动触发这个自定义校验,返回 “手机号已被注册” 的错误信息 —— 不用在 Controller 里写任何额外代码!
2. 扩展:自定义返回格式(比如 XML)
如果某些接口需要返回 XML 格式(比如给第三方系统),咱们可以在@AutoController里加个produce属性,指定返回格式,然后在AutoControllerResponseAdvice里处理。
先修改@AutoController注解,加个produce属性:
public @interface AutoController {
// 省略其他属性...
/**
* 返回格式(默认JSON,支持application/json、application/xml)
*/
String produce() default "application/json";
}
然后修改AutoControllerResponseAdvice,根据produce属性设置返回格式:
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
// 获取@AutoController的produce属性,设置返回格式
AutoController autoController = getAutoControllerAnnotation(returnType);
response.getHeaders().setContentType(MediaType.parseMediaType(autoController.produce()));
// 省略其他封装逻辑...
}
然后在需要返回 XML 的接口上设置produce:
@GetMapping("/get/xml")
@AutoController(operation = "查询用户(XML格式)", produce = "application/xml")
public UserVO getUserXml(@RequestParam Long id) {
return userService.getUserById(id);
}
最后,在pom.xml里加 XML 解析依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
这样一来,调用这个接口时,会自动返回 XML 格式的结果,其他接口还是返回 JSON—— 灵活得很!
3. 扩展:多角色校验(比如只有管理员能操作)
比如 “删除用户” 接口,只有管理员能调用,普通用户调用会报错。咱们可以在@AutoController里加个roles属性,指定允许的角色,然后在 AOP 切面里校验。
先修改@AutoController注解,加个roles属性:
public @interface AutoController {
// 省略其他属性...
/**
* 允许访问的角色(默认所有角色都能访问)
* 比如{"admin", "super_admin"}表示只有这两个角色能访问
*/
String[] roles() default {};
}
然后修改AutoControllerLoginAspect的doLoginCheck方法,加角色校验逻辑:
@Before("autoControllerPointcut() && @annotation(autoController)")
publicvoid doLoginCheck(AutoController autoController) {
// 省略登录校验逻辑...
// 角色校验:如果roles不为空,判断当前用户的角色是否在允许的列表里
String[] allowRoles = autoController.roles();
if (allowRoles.length > 0) {
UserInfo userInfo = UserContextHolder.getUserInfo();
boolean hasPermission = Arrays.asList(allowRoles).contains(userInfo.getRole());
if (!hasPermission) {
thrownew BusinessException("没有权限操作,请联系管理员");
}
log.debug("角色校验通过,当前用户角色:{},允许角色:{}", userInfo.getRole(), Arrays.toString(allowRoles));
}
}
然后在需要角色校验的接口上设置roles:
@DeleteMapping("/delete/{id}")
@AutoController(operation = "删除用户", roles = {"admin"}) // 只有admin角色能删除
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
这样一来,普通用户调用删除接口时,会自动返回 “没有权限操作,请联系管理员” 的错误信息 —— 不用在 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 切面就行,不用侵入业务代码。
AI大模型学习福利
作为一名热心肠的互联网老兵,我决定把宝贵的AI知识分享给大家。 至于能学习到多少就看你的学习毅力和能力了 。我已将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
一、全套AGI大模型学习路线
AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获取
二、640套AI大模型报告合集
这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
三、AI大模型经典PDF籍
随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
四、AI大模型商业化落地方案

因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获
作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量
348

被折叠的 条评论
为什么被折叠?



