一个注解干翻所有Controller!

今天咱就来搞个大事情:用一个自定义注解,把这些破事儿全搞定!以后写 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标签,这个管家就会自动帮你把这些活儿全干了。

具体怎么实现呢?咱们分三步走:

  1. 定义注解:就是创建@AutoController这个 “标签”,规定它能贴在哪些地方(比如类上、方法上),能保存哪些配置(比如是否跳过参数校验)。
  2. 写 “管家逻辑”:用 AOP(面向切面编程)或者 Spring 的HandlerMethodArgumentResolver、ResponseBodyAdvice这些扩展点,实现 “参数校验、异常捕获、结果封装” 的具体逻辑。
  3. 让 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 行代码,只有核心业务逻辑!

而且不管是参数校验、登录校验,还是异常处理、结果封装,全都是自动的:

  1. 如果你传的参数不符合规则(比如手机号格式错),会自动返回{"code":400,"message":"phone:手机号格式错误","data":null};
  2. 如果你没传 token 就访问需要登录的接口,会自动返回{"code":401,"message":"登录已过期,请重新登录","data":null};
  3. 如果你调用删除接口,会自动返回{"code":200,"message":"删除用户成功","data":null};
  4. 如果 Service 里抛了new BusinessException("用户已存在"),会自动返回{"code":400,"message":"用户已存在","data":null};
  5. 如果出现未知异常(比如数据库连接失败),会自动返回{"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大模型商业化落地方案

    因篇幅有限,仅展示部分资料,需要点击文章最下方名片即可前往获

    作为普通人,入局大模型时代需要持续学习和实践,不断提高自己的技能和认知水平,同时也需要有责任感和伦理意识,为人工智能的健康发展贡献力量

    评论
    成就一亿技术人!
    拼手气红包6.0元
    还能输入1000个字符
     
    红包 添加红包
    表情包 插入表情
     条评论被折叠 查看
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值