偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一個注解干翻所有Controller!

開發(fā) 前端
今天咱就來搞個大事情:用一個自定義注解,把這些破事兒全搞定!以后寫 Controller,咱只專注業(yè)務(wù)邏輯,那些重復(fù)的 “邊角料”,讓注解幫咱扛了。全程大白話,不整虛的,保證你看完就能上手,看完就想把公司項目里的 Controller 全重構(gòu)一遍!

兄弟們,大家寫 Controller 的時候,是不是總感覺自己像個 “復(fù)制粘貼工程師”?

比如寫個用戶接口,先校驗參數(shù)非空,再 try-catch 包一圈,最后還要把返回結(jié)果套上Result.success()或者Result.fail();下一個訂單接口,嘿,好家伙,又是這套流程 —— 參數(shù)校驗、異常捕獲、結(jié)果封裝,連注釋都長得差不多。更氣人的是,萬一某天產(chǎn)品說 “返回格式要加個 requestId”,你就得打開十幾個 Controller,改完還得挨個測,手都快按抽筋了。

今天咱就來搞個大事情:用一個自定義注解,把這些破事兒全搞定!以后寫 Controller,咱只專注業(yè)務(wù)邏輯,那些重復(fù)的 “邊角料”,讓注解幫咱扛了。全程大白話,不整虛的,保證你看完就能上手,看完就想把公司項目里的 Controller 全重構(gòu)一遍!

一、先吐個槽:你寫的 Controller,可能一半是 “廢話”

先給大家看個 “標(biāo)準(zhǔn)” 的 Controller 代碼,我賭五毛,你電腦里絕對有差不多的:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @PostMapping("/add")
    public Result<UserVO> addUser(@RequestBody UserDTO userDTO) {
        // 第一步:參數(shù)校驗(每個接口都要寫一遍,煩!)
        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);
            // 第三步:封裝返回結(jié)果(統(tǒng)一格式,但每個接口都要寫,蠢?。?            return Result.success(userVO, "新增用戶成功");
        } catch (BusinessException e) {
            // 業(yè)務(wù)異常要返回具體錯誤信息
            return Result.fail(e.getCode(), e.getMessage());
        } catch (Exception e) {
            // 未知異常要返回通用錯誤
            log.error("新增用戶失敗", e);
            return Result.fail("系統(tǒng)異常,請聯(lián)系管理員");
        }
    }
    // 下面還有update、delete、getById...全是重復(fù)的校驗、try-catch、結(jié)果封裝
    @PutMapping("/update")
    public Result<UserVO> updateUser(@RequestBody UserDTO userDTO) {
        // 又是參數(shù)校驗...
        // 又是try-catch...
        // 又是結(jié)果封裝...
    }
}

你品,你細(xì)品:一個接口真正的業(yè)務(wù)邏輯,可能就userService.addUser(userDTO)這一行,剩下的全是 “重復(fù)性勞動”。更要命的是,一旦項目里有幾十個 Controller、上百個接口,這些 “廢話代碼” 會讓項目變得巨難維護 —— 比如想加個 “所有接口都要校驗 token”,你得一個個加攔截;想改返回格式,你得一個個改Result。這時候肯定有人會說:“我用了 Bean Validation 啊,比如 @NotNull、@Min,能少寫點參數(shù)校驗代碼!”

確實,Bean Validation 能解決一部分問題,但也就僅限于參數(shù)校驗。異常捕獲、結(jié)果封裝、甚至接口權(quán)限校驗這些事兒,你該寫還是得寫。而且如果遇到復(fù)雜校驗(比如 “用戶年齡大于 18 才能注冊”),光靠注解還不夠,你還得寫自定義校驗器,最后還是逃不掉 “代碼冗余” 的坑。

那有沒有一種辦法,能把這些 “非業(yè)務(wù)邏輯” 全抽離出去,讓 Controller 只專注于 “做什么業(yè)務(wù)”,而不是 “怎么處理參數(shù) / 異常 / 返回值”?

答案就是:自定義注解 + AOP +  Spring 擴展。咱們今天的主角 ——@AutoController注解,就是干這個的!

二、核心思路:讓注解成為 Controller 的 “全能管家”

在開始寫代碼之前,咱先搞明白一個事兒:為什么一個注解能搞定這么多活兒?

其實原理很簡單,你可以把@AutoController理解成一個 “全能管家”。以前你得自己干 “開門(參數(shù)校驗)、看家(異常捕獲)、送客人(結(jié)果封裝)” 這些活兒,現(xiàn)在你給 Controller 貼個@AutoController標(biāo)簽,這個管家就會自動幫你把這些活兒全干了。

具體怎么實現(xiàn)呢?咱們分三步走:

  1. 定義注解:就是創(chuàng)建@AutoController這個 “標(biāo)簽”,規(guī)定它能貼在哪些地方(比如類上、方法上),能保存哪些配置(比如是否跳過參數(shù)校驗)。
  2. 寫 “管家邏輯”:用 AOP(面向切面編程)或者 Spring 的HandlerMethodArgumentResolver、ResponseBodyAdvice這些擴展點,實現(xiàn) “參數(shù)校驗、異常捕獲、結(jié)果封裝” 的具體邏輯。
  3. 讓 Spring 識別:把注解和 “管家邏輯” 注冊到 Spring 容器里,讓 Spring 知道 “遇到貼了 @AutoController 的 Controller,就用這個管家”。

聽起來好像有點復(fù)雜?別怕,咱一步步來,每一行代碼都給你講明白,保證你看完就能抄走用。

三、實戰(zhàn):手把手寫一個 “干翻 Controller” 的注解

咱們基于 Spring Boot 2.7.x 來寫,畢竟現(xiàn)在大部分公司都用這個版本。先準(zhǔn)備好依賴,在pom.xml里加這些(如果是 Gradle,對應(yīng)轉(zhuǎn)一下就行):

<!-- 核心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>
<!-- 參數(shù)校驗依賴,替代手寫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類或方法上,自動實現(xiàn):參數(shù)校驗、異常捕獲、結(jié)果封裝
 */
@Target({ElementType.TYPE, ElementType.METHOD}) // 能貼在類上或方法上
@Retention(RetentionPolicy.RUNTIME) // 運行時生效(因為要在程序跑的時候攔截)
@Documented // 生成文檔時會顯示這個注解
public @interface AutoController {
    /**
     * 是否跳過參數(shù)校驗(默認(rèn)不跳過)
     * 比如某些查詢接口不需要校驗參數(shù),就可以設(shè)置為true
     */
    boolean skipValidate() default false;
    /**
     * 操作描述(比如“新增用戶”“刪除訂單”,用于日志打?。?     */
    String operation() default "";
    /**
     * 是否需要登錄(默認(rèn)需要)
     * 比如登錄接口本身不需要登錄,就設(shè)置為false
     */
    boolean needLogin() default true;
}

看到?jīng)]?這個注解帶了三個 “配置項”,都是咱們?nèi)粘i_發(fā)常用的:是否跳過參數(shù)校驗、操作描述、是否需要登錄。這樣一來,注解就不是死的,能根據(jù)不同接口的需求靈活調(diào)整 —— 比如登錄接口,咱們就可以設(shè)置needLogin = false,避免自己攔截自己。

第二步:寫 “管家邏輯” 之一 —— 參數(shù)校驗(不用再寫 if-else)

以前咱們用 Bean Validation 的@NotNull、@Min這些注解,還得在 Controller 里加@Valid,然后用BindingResult手動處理校驗結(jié)果,還是有點麻煩。現(xiàn)在咱們用@AutoController,直接把這些活兒全自動化。

先寫一個 “參數(shù)校驗器”,用 Spring 的MethodArgumentResolver擴展點,專門處理貼了@AutoController的接口的參數(shù):

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;
/**
 * 自動參數(shù)校驗器:處理@AutoController注解的接口參數(shù)
 */
@Component
@RequiredArgsConstructor // Lombok的注解,自動生成構(gòu)造方法注入依賴
public class AutoControllerArgumentResolver implements HandlerMethodArgumentResolver {
    // Spring自帶的校驗器,不用自己寫
    private final Validator validator;
    // 第一步:判斷當(dāng)前參數(shù)是否需要用這個解析器處理
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 條件:1. 方法或類上有@AutoController注解;2. 不需要跳過校驗;3. 參數(shù)是實體類(不是基本類型)
        AutoController autoController = getAutoControllerAnnotation(parameter);
        return autoController != null 
                && !autoController.skipValidate() 
                && !parameter.getParameterType().isPrimitive();
    }
    // 第二步:處理參數(shù)(核心邏輯:校驗參數(shù),并拋出異常)
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 1. 獲取請求參數(shù)(比如@RequestBody的UserDTO)
        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        Object parameterValue = request.getAttribute(parameter.getParameterName());
        if (parameterValue == null) {
            // 如果參數(shù)為空,直接拋業(yè)務(wù)異常
            throw new BusinessException("參數(shù)不能為空");
        }
        // 2. 用Spring的校驗器校驗參數(shù)(比如@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(";");
            });
            // 拋自定義的業(yè)務(wù)異常,后面會統(tǒng)一處理
            throw new BusinessException(errorMsg.toString().substring(0, errorMsg.length() - 1));
        }
        // 4. 校驗通過,返回參數(shù)(給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);
    }
}

這里有個小細(xì)節(jié):咱們用BusinessException這個自定義異常來拋校驗錯誤,后面會統(tǒng)一捕獲這個異常,不用在每個接口里寫if (errors.hasErrors())了。當(dāng)然,還得把這個參數(shù)解析器注冊到 Spring 里,不然 Spring 不認(rèn)識它。寫個配置類:

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;
    // 構(gòu)造方法注入?yún)?shù)解析器
    public WebMvcConfig(AutoControllerArgumentResolver autoControllerArgumentResolver) {
        this.autoControllerArgumentResolver = autoControllerArgumentResolver;
    }
    // 注冊參數(shù)解析器
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        // 把咱們的解析器加進去,注意順序(加在前面,優(yōu)先執(zhí)行)
        resolvers.add(0, autoControllerArgumentResolver);
    }
}

到這兒,參數(shù)校驗的邏輯就搞定了。以后咱們在 DTO 里加@NotNull這些注解,再給 Controller 貼個@AutoController,就不用手動處理校驗結(jié)果了 —— 校驗失敗會自動拋異常,校驗成功直接把參數(shù)傳給 Controller 方法。

第三步:寫 “管家邏輯” 之二 —— 統(tǒng)一異常捕獲(不用再寫 try-catch)

以前每個接口都要包try-catch,現(xiàn)在咱們用 Spring 的@RestControllerAdvice+@ExceptionHandler,寫一個全局異常處理器,統(tǒng)一捕獲所有異常,包括咱們剛才拋的BusinessException。

先定義兩個自定義異常(業(yè)務(wù)異常和登錄異常),方便區(qū)分:

// 業(yè)務(wù)異常:比如參數(shù)錯誤、業(yè)務(wù)邏輯錯誤(如“用戶已存在”)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BusinessException extends RuntimeException {
    private Integer code = 400; // 默認(rèn)錯誤碼400(.bad request)
    private String message; // 錯誤信息
    // 重載構(gòu)造方法,方便只傳錯誤信息
    public BusinessException(String message) {
        this.message = message;
    }
}
// 登錄異常:比如“未登錄”“token過期”
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginException extends RuntimeException {
    private Integer code = 401; // 默認(rèn)錯誤碼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;
/**
 * 全局異常處理器:統(tǒng)一處理所有異常,配合@AutoController使用
 */
@RestControllerAdvice // 全局生效,只處理@RestController的異常
@Slf4j
public class AutoControllerExceptionHandler {
    // 處理業(yè)務(wù)異常(BusinessException)
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        // 打印錯誤日志(方便排查問題)
        log.warn("業(yè)務(wù)異常:{}", e.getMessage());
        // 返回錯誤結(jié)果(統(tǒng)一格式)
        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狀態(tài)碼
    public Result<?> handleUnknownException(Exception e) {
        // 未知異常要打印完整堆棧(方便排查)
        log.error("系統(tǒng)異常:", e);
        // 返回通用錯誤信息(別把具體異常信息返回給前端,不安全)
        return Result.fail(500, "系統(tǒng)開了個小差,請稍后再試~");
    }
}

這里有個關(guān)鍵點:咱們返回的是Result對象,這是統(tǒng)一的返回格式。咱們順便把Result類也寫了,以后所有接口都用這個格式返回:

import lombok.Data;
/**
 * 統(tǒng)一返回結(jié)果類
 * 所有接口都返回這個格式,前端好處理
 */
@Data
public class Result<T> {
    // 狀態(tài)碼:200成功,400業(yè)務(wù)錯誤,401未登錄,500系統(tǒng)錯誤
    private Integer code;
    // 提示信息
    private String message;
    // 業(yè)務(wù)數(shù)據(jù)(成功時返回)
    private T data;
    // 成功:帶數(shù)據(jù)
    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;
    }
    // 成功:不帶數(shù)據(jù)(比如刪除接口)
    public static <T> Result<T> success(String message) {
        return success(null, message);
    }
    // 失?。簬уe誤碼和信息
    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;
    }
    // 失?。耗J(rèn)錯誤碼400
    public static <T> Result<T> fail(String message) {
        return fail(400, message);
    }
}

到這兒,異常捕獲的邏輯就搞定了。以后 Controller 里不用再寫try-catch了 —— 不管是業(yè)務(wù)異常、登錄異常,還是未知異常,都會被這個處理器統(tǒng)一捕獲,然后返回統(tǒng)一格式的錯誤信息。

第四步:寫 “管家邏輯” 之三 —— 登錄校驗(不用再寫 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// 標(biāo)記這是一個AOP切面
@Component// 注冊到Spring容器
@Slf4j
@RequiredArgsConstructor
publicclass AutoControllerLoginAspect {

    // 假設(shè)這是登錄服務(wù),用來校驗token是否有效(實際項目里替換成你的登錄邏輯)
    privatefinal LoginService loginService;

    // 切入點:所有貼了@AutoController注解的方法或類
    @Pointcut("@annotation(com.xxx.annotation.AutoController) || @within(com.xxx.annotation.AutoController)")
    public void autoControllerPointcut() {}

    // 前置通知:在方法執(zhí)行前做登錄校驗
    @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(調(diào)用登錄服務(wù),實際項目里替換成你的邏輯)
        boolean isTokenValid = loginService.validateToken(token);
        if (!isTokenValid) {
            // token無效或過期,拋登錄異常(會被全局異常處理器捕獲)
            thrownew LoginException("登錄已過期,請重新登錄");
        }

        // 4. token有效,還可以把用戶信息存入ThreadLocal(方便后續(xù)業(yè)務(wù)使用)
        UserInfo userInfo = loginService.getUserInfoByToken(token);
        UserContextHolder.setUserInfo(userInfo);
        log.debug("登錄校驗通過,當(dāng)前用戶:{}", userInfo.getUsername());
    }
}

這里有兩個輔助類:UserContextHolder(用 ThreadLocal 存用戶信息)和LoginService(模擬登錄邏輯),咱們也寫一下:

// UserContextHolder:用ThreadLocal存用戶信息,避免參數(shù)傳遞
publicclass UserContextHolder {
    privatestaticfinal ThreadLocal<UserInfo> USER_INFO_THREAD_LOCAL = new ThreadLocal<>();

    // 設(shè)置用戶信息
    public static void setUserInfo(UserInfo userInfo) {
        USER_INFO_THREAD_LOCAL.set(userInfo);
    }

    // 獲取用戶信息(比如在Service里用)
    public static UserInfo getUserInfo() {
        return USER_INFO_THREAD_LOCAL.get();
    }

    // 清除用戶信息(避免內(nèi)存泄漏)
    public static void clear() {
        USER_INFO_THREAD_LOCAL.remove();
    }
}

// LoginService:模擬登錄邏輯(實際項目里替換成你的Redis或數(shù)據(jù)庫邏輯)
@Service
publicclass LoginService {

    // 模擬校驗token(實際項目里查Redis或數(shù)據(jù)庫)
    public boolean validateToken(String token) {
        if (token == null || token.trim().isEmpty()) {
            returnfalse;
        }
        // 這里只是模擬,實際要判斷token是否存在、是否過期
        return token.startsWith("valid_token_");
    }

    // 模擬根據(jù)token獲取用戶信息
    public UserInfo getUserInfoByToken(String token) {
        // 實際項目里從token解析用戶ID,再查數(shù)據(jù)庫
        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;
}

另外,別忘了在請求結(jié)束后清除 ThreadLocal 里的用戶信息,不然會有內(nèi)存泄漏風(fēng)險。寫一個攔截器:

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 清除ThreadLocal的攔截器:請求結(jié)束后清除用戶信息
 */
@Component
public class ClearUserContextInterceptor implements HandlerInterceptor {

    // 請求處理完成后執(zhí)行
    @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;

    // 構(gòu)造方法注入攔截器
    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(默認(rèn)就是 true),AOP 會自動幫你校驗 token。

第五步:寫 “管家邏輯” 之四 —— 統(tǒng)一結(jié)果封裝(不用再寫 Result.success)

最后一步,咱們解決 “每個接口都要寫Result.success()” 的問題。用 Spring 的ResponseBodyAdvice擴展點,在返回結(jié)果給前端之前,自動把 Controller 的返回值封裝成Result對象。

寫一個統(tǒng)一結(jié)果封裝器:

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;

/**
 * 統(tǒng)一結(jié)果封裝器:自動把Controller的返回值封裝成Result格式
 */
@ControllerAdvice// 全局生效
publicclass AutoControllerResponseAdvice implements ResponseBodyAdvice<Object> {

    // 第一步:判斷是否需要封裝結(jié)果(只處理貼了@AutoController的接口)
    @Override
    publicboolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 方法或類上有@AutoController注解,并且返回值不是Result類型(避免重復(fù)封裝)
        AutoController autoController = getAutoControllerAnnotation(returnType);
        return autoController != null
                && !returnType.getParameterType().isAssignableFrom(Result.class);
    }

    // 第二步:封裝結(jié)果(把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(比如刪除接口),就封裝成不帶數(shù)據(jù)的Result
        if (body == null || returnType.getParameterType().isAssignableFrom(Void.class)) {
            return Result.success(message);
        }

        // 否則,封裝成帶數(shù)據(jù)的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);
    }
}

這里有個細(xì)節(jié):咱們判斷了返回值是不是Result類型,如果是就不封裝了 —— 避免出現(xiàn)Result<Result<UserVO>>這種嵌套格式。另外,如果 Controller 方法返回 void(比如刪除接口),就封裝成不帶數(shù)據(jù)的Result.success(message)。

四、見證奇跡:用 @AutoController 重構(gòu) Controller

現(xiàn)在,咱們把最開始那個 “臃腫” 的 UserController,用@AutoController重構(gòu)一下,看看效果:

@RestController
@RequestMapping("/user")
@AutoController(operation = "用戶管理") // 類上貼注解,默認(rèn)所有方法都生效
public class UserController {

    @Autowired
    private UserService userService;

    // 新增用戶:不用寫參數(shù)校驗、try-catch、Result封裝
    @PostMapping("/add")
    @AutoController(operation = "新增用戶") // 方法上的注解會覆蓋類上的
    public UserVO addUser(@RequestBody UserDTO userDTO) {
        // 只有核心業(yè)務(wù)邏輯!
        returnuserService.addUser(userDTO);
    }

    // 更新用戶:同樣不用寫邊角料代碼
    @PutMapping("/update")
    @AutoController(operation = "更新用戶")
    publicUserVOupdateUser(@RequestBody UserDTO userDTO) {
        // 核心業(yè)務(wù)邏輯,沒了!
        returnuserService.updateUser(userDTO);
    }

    // 刪除用戶:返回void,會自動封裝成Result.success("刪除用戶成功")
    @DeleteMapping("/delete/{id}")
    @AutoController(operation = "刪除用戶")
    publicvoiddeleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
    }

    // 查詢用戶:跳過參數(shù)校驗(比如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 注解做參數(shù)校驗):

// 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;
}

// 分組校驗用的接口(比如新增和更新的校驗規(guī)則不同)
public interface AddGroup {}
public interface UpdateGroup {}

對比一下重構(gòu)前后的代碼:

  • 重構(gòu)前:每個接口平均 15 行代碼,其中 12 行是參數(shù)校驗、try-catch、Result 封裝;
  • 重構(gòu)后:每個接口平均 3 行代碼,只有核心業(yè)務(wù)邏輯!

而且不管是參數(shù)校驗、登錄校驗,還是異常處理、結(jié)果封裝,全都是自動的:

  1. 如果你傳的參數(shù)不符合規(guī)則(比如手機號格式錯),會自動返回{"code":400,"message":"phone:手機號格式錯誤","data":null};
  2. 如果你沒傳 token 就訪問需要登錄的接口,會自動返回{"code":401,"message":"登錄已過期,請重新登錄","data":null};
  3. 如果你調(diào)用刪除接口,會自動返回{"code":200,"message":"刪除用戶成功","data":null};
  4. 如果 Service 里拋了new BusinessException("用戶已存在"),會自動返回{"code":400,"message":"用戶已存在","data":null};
  5. 如果出現(xiàn)未知異常(比如數(shù)據(jù)庫連接失敗),會自動返回{"code":500,"message":"系統(tǒng)開了個小差,請稍后再試~","data":null},同時后臺打印完整堆棧。

五、進階:讓 @AutoController 更靈活(滿足復(fù)雜場景)

咱們寫的@AutoController已經(jīng)能滿足大部分場景了,但實際開發(fā)中可能會有更復(fù)雜的需求。比如:

  • 某些接口需要特殊的參數(shù)校驗規(guī)則(比如 “用戶注冊時,手機號必須未被使用”);
  • 某些接口需要自定義返回格式(比如給第三方接口返回 XML 格式);
  • 某些接口需要多角色校驗(比如只有管理員能刪除用戶)。

別擔(dān)心,咱們的@AutoController可以輕松擴展,咱們來一個個解決。

1. 擴展:復(fù)雜參數(shù)校驗(自定義校驗器)

比如 “用戶注冊時,手機號必須未被使用”,這種需要查數(shù)據(jù)庫的校驗,Bean Validation 的默認(rèn)注解搞不定,咱們可以寫個自定義校驗注解,配合@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 {};

    // 負(fù)載(很少用)
    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;

    // 校驗邏輯:調(diào)用Service查數(shù)據(jù)庫,判斷手機號是否存在
    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        if (phone == null || phone.trim().isEmpty()) {
            // 手機號為空的校驗,交給@NotNull處理,這里不處理
            returntrue;
        }
        // 查數(shù)據(jù)庫,判斷手機號是否已被注冊
        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的校驗規(guī)則
    public UserVO addUser(@Validated(AddGroup.class) UserDTO userDTO) {
        // 業(yè)務(wù)邏輯...
    }

    // 更新用戶:指定用UpdateGroup的校驗規(guī)則
    public UserVO updateUser(@Validated(UpdateGroup.class) UserDTO userDTO) {
        // 業(yè)務(wù)邏輯...
    }

    // 校驗手機號是否存在
    public boolean isPhoneExists(String phone) {
        // 查數(shù)據(jù)庫,返回true表示已存在,false表示不存在
        // 模擬邏輯:假設(shè)手機號13800138000已被注冊
        return"13800138000".equals(phone);
    }
}

這樣一來,當(dāng)你調(diào)用新增用戶接口,傳手機號 13800138000 時,@AutoController會自動觸發(fā)這個自定義校驗,返回 “手機號已被注冊” 的錯誤信息 —— 不用在 Controller 里寫任何額外代碼!

2. 擴展:自定義返回格式(比如 XML)

如果某些接口需要返回 XML 格式(比如給第三方系統(tǒng)),咱們可以在@AutoController里加個produce屬性,指定返回格式,然后在AutoControllerResponseAdvice里處理。

先修改@AutoController注解,加個produce屬性:

public @interface AutoController {
    // 省略其他屬性...

    /**
     * 返回格式(默認(rèn)JSON,支持application/json、application/xml)
     */
    String produce() default "application/json";
}

然后修改AutoControllerResponseAdvice,根據(jù)produce屬性設(shè)置返回格式:

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                              Class<? extends HttpMessageConverter<?>> selectedConverterType,
                              ServerHttpRequest request, ServerHttpResponse response) {
    // 獲取@AutoController的produce屬性,設(shè)置返回格式
    AutoController autoController = getAutoControllerAnnotation(returnType);
    response.getHeaders().setContentType(MediaType.parseMediaType(autoController.produce()));

    // 省略其他封裝邏輯...
}

然后在需要返回 XML 的接口上設(shè)置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>

這樣一來,調(diào)用這個接口時,會自動返回 XML 格式的結(jié)果,其他接口還是返回 JSON—— 靈活得很!

3. 擴展:多角色校驗(比如只有管理員能操作)

比如 “刪除用戶” 接口,只有管理員能調(diào)用,普通用戶調(diào)用會報錯。咱們可以在@AutoController里加個roles屬性,指定允許的角色,然后在 AOP 切面里校驗。

先修改@AutoController注解,加個roles屬性:

public @interface AutoController {
    // 省略其他屬性...

    /**
     * 允許訪問的角色(默認(rèn)所有角色都能訪問)
     * 比如{"admin", "super_admin"}表示只有這兩個角色能訪問
     */
    String[] roles() default {};
}

然后修改AutoControllerLoginAspect的doLoginCheck方法,加角色校驗邏輯:

@Before("autoControllerPointcut() && @annotation(autoController)")
publicvoid doLoginCheck(AutoController autoController) {
    // 省略登錄校驗邏輯...

    // 角色校驗:如果roles不為空,判斷當(dāng)前用戶的角色是否在允許的列表里
    String[] allowRoles = autoController.roles();
    if (allowRoles.length > 0) {
        UserInfo userInfo = UserContextHolder.getUserInfo();
        boolean hasPermission = Arrays.asList(allowRoles).contains(userInfo.getRole());
        if (!hasPermission) {
            thrownew BusinessException("沒有權(quán)限操作,請聯(lián)系管理員");
        }
        log.debug("角色校驗通過,當(dāng)前用戶角色:{},允許角色:{}", userInfo.getRole(), Arrays.toString(allowRoles));
    }
}

然后在需要角色校驗的接口上設(shè)置roles:

@DeleteMapping("/delete/{id}")
@AutoController(operation = "刪除用戶", roles = {"admin"}) // 只有admin角色能刪除
public void deleteUser(@PathVariable Long id) {
    userService.deleteUser(id);
}

這樣一來,普通用戶調(diào)用刪除接口時,會自動返回 “沒有權(quán)限操作,請聯(lián)系管理員” 的錯誤信息 —— 不用在 Service 里寫if (user.getRole() != admin) throw 異常了!

六、注意事項:別踩這些坑!

咱們的@AutoController雖然好用,但也有一些注意事項,避免你踩坑:

1. 不要重復(fù)封裝 Result

如果你的 Controller 方法已經(jīng)返回了Result對象,一定要確保@AutoController的supports方法會跳過它 —— 不然會出現(xiàn)Result<Result<UserVO>>這種嵌套格式。咱們之前在AutoControllerResponseAdvice里已經(jīng)加了判斷:!returnType.getParameterType().isAssignableFrom(Result.class),所以只要你不手動返回Result,就沒問題。

2. ThreadLocal 要記得清除

咱們用ThreadLocal存用戶信息,一定要在請求結(jié)束后清除(比如用攔截器的afterCompletion方法),不然會導(dǎo)致內(nèi)存泄漏 —— 因為 Tomcat 的線程是復(fù)用的,ThreadLocal 里的信息會一直存在,直到線程被銷毀。

3. 性能問題:AOP 會不會影響性能?

很多人擔(dān)心 AOP 會影響性能,其實大可不必。AOP 用的是動態(tài)代理,攔截方法的開銷非常小,對于大部分業(yè)務(wù)系統(tǒng)來說,完全可以忽略不計。而且咱們的 AOP 切面只攔截貼了@AutoController的接口,不會對其他接口造成影響。

如果你的系統(tǒng)是超高并發(fā)(比如每秒幾萬請求),可以考慮用 AspectJ 的編譯時織入(而不是 Spring AOP 的運行時織入),進一步降低性能開銷。

4. 兼容性問題:和其他框架沖突嗎?

咱們的@AutoController基于 Spring 的擴展點實現(xiàn),和 Spring Boot、Spring Cloud 等框架完全兼容。如果你的項目里用了 Spring Security、Shiro 等安全框架,只需要調(diào)整一下登錄校驗的邏輯(比如用 Security 的SecurityContextHolder代替咱們的UserContextHolder),就能完美配合。

七、總結(jié):一個注解,解放你的雙手!

看到這兒,你應(yīng)該明白為什么說 “一個注解干翻所有 Controller” 了吧?

以前寫 Controller,你得寫參數(shù)校驗、try-catch、Result 封裝、登錄校驗,這些活兒占了 80% 的代碼量,卻沒什么技術(shù)含量;現(xiàn)在你只需要貼個@AutoController,這些活兒全由注解自動搞定,你可以專注于真正有價值的業(yè)務(wù)邏輯 —— 寫代碼的效率至少提升 3 倍!

更重要的是,項目的可維護性也大大提升了:

  • 要改參數(shù)校驗規(guī)則?改 DTO 的注解就行,不用改 Controller;
  • 要改返回格式?改Result類或AutoControllerResponseAdvice就行,不用改幾十個接口;
  • 要加新的攔截邏輯(比如接口限流)?加個 AOP 切面就行,不用侵入業(yè)務(wù)代碼。
責(zé)任編輯:武曉燕 來源: 石杉的架構(gòu)筆記
相關(guān)推薦

2023-02-07 19:46:35

NIOCQ內(nèi)核

2023-11-16 15:10:39

RustJavaZig

2024-05-15 17:34:15

2020-06-04 17:13:12

JavaScript語言Web

2020-07-30 13:22:19

語言Android大數(shù)據(jù)

2022-11-30 09:33:56

語言Java系統(tǒng)

2022-05-26 10:42:30

數(shù)據(jù)權(quán)限注解

2024-11-11 14:57:56

JWTSession微服務(wù)

2021-10-09 20:13:03

ArrayListLinkedList java

2022-06-14 10:47:27

項目日志PUT

2021-07-14 15:06:50

SDK版本 jar

2022-06-27 08:36:27

分布式事務(wù)XA規(guī)范

2022-06-10 13:03:44

接口重試while

2025-05-30 08:20:54

2024-11-12 08:20:31

2021-12-02 15:30:55

命令內(nèi)存Linux

2020-08-04 11:03:50

Python內(nèi)置異常開發(fā)

2020-01-09 11:07:48

AI 數(shù)據(jù)機器學(xué)習(xí)

2022-05-16 10:45:22

Redis接口限流緩存

2024-10-14 08:46:50

Controller開發(fā)代碼
點贊
收藏

51CTO技術(shù)棧公眾號