精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

【禁止血壓飆升】阿里大佬寫的 Controller 太優雅了!

開發 前端
按照這個法則寫出來的 Controller,代碼清爽、職責明確、易于維護,同事接手的時候不會罵街,你自己調試的時候也不會血壓飆升 —— 這才是阿里大佬真正的 “優雅”。

兄弟們,大家是不是也有過這樣的經歷:打開項目里的 Controller 文件,密密麻麻的代碼像一團亂麻,if-else 疊得比漢堡胚還多,參數校驗寫得比業務邏輯還長,好不容易找到個核心接口,調試的時候還得在一堆 try-catch 里繞圈圈?

上次我幫同事排查個接口問題,點開那個 UserController,直接給我整懵了:一個新增用戶的接口,從參數非空判斷到手機號格式校驗,再到業務邏輯處理,足足寫了 200 多行,中間還夾雜著好幾個 catch 塊,一會兒拋個 “參數錯誤”,一會兒又返回個 “系統異常”,前端同學吐槽說 “你們這接口返回的狀態碼比我銀行卡密碼還亂”。

后來跟阿里的一位大佬聊起這事兒,他甩過來一段 Controller 代碼,我看完直接拍大腿:這才叫優雅!沒有冗余的校驗,沒有混亂的異常處理,代碼清爽得像剛冰鎮過的可樂,喝一口都解膩。

今天就把阿里大佬這套優雅的 Controller 寫法拆解開,從參數校驗到異常處理,再到職責邊界,一步步教你怎么寫,以后再也不用對著亂糟糟的代碼血壓飆升了。

一、先吐槽:你寫的 Controller 是不是也這樣?

在講優雅寫法之前,咱先把 “反面教材” 擺出來,看看你中了幾條 ——

1. 參數校驗:if-else 寫成 “千層餅”

最常見的就是參數校驗,比如一個創建訂單的接口,要校驗訂單金額不能為負、商品 ID 不能為空、收貨地址不能太長... 很多人會這么寫:

@PostMapping("/createOrder")
public String createOrder(OrderDTO orderDTO) {
    // 校驗商品ID
    if (orderDTO.getGoodsId() == null || orderDTO.getGoodsId().isEmpty()) {
        return "商品ID不能為空";
    }
    // 校驗訂單金額
    if (orderDTO.getAmount() == null || orderDTO.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
        return "訂單金額必須大于0";
    }
    // 校驗收貨地址
    if (orderDTO.getAddress() == null || orderDTO.getAddress().length() > 200) {
        return "收貨地址不能為空且長度不能超過200字";
    }
    // 校驗支付方式
    if (orderDTO.getPayType() == null || !Arrays.asList(1,2,3).contains(orderDTO.getPayType())) {
        return "支付方式無效(1-微信,2-支付寶,3-銀行卡)";
    }
    // 后面才是業務邏輯...
    orderService.createOrder(orderDTO);
    return "創建訂單成功";
}

你瞅瞅,光參數校驗就寫了十幾行,要是參數再多一點,這 if-else 能疊到天上去。更坑的是,每個接口都要這么寫一遍,復制粘貼的時候還容易漏改,上次我同事就把 “收貨地址” 寫成了 “收貨地址 1”,線上報了錯才發現。

2. 異常處理:try-catch 裹成 “粽子”

再說說異常處理,很多人怕接口報錯,就把整個業務邏輯裹在 try-catch 里,有的甚至一個 Controller 里塞十幾個 catch 塊:

@GetMapping("/getOrderDetail")
public Result getOrderDetail(String orderId) {
    try {
        // 校驗訂單ID
        if (orderId == null || orderId.isEmpty()) {
            return Result.fail("訂單ID不能為空");
        }
        // 查訂單詳情
        OrderDetailDTO detail = orderService.getDetail(orderId);
        if (detail == null) {
            return Result.fail("訂單不存在");
        }
        // 轉換DTO
        OrderVO orderVO = new OrderVO();
        orderVO.setOrderId(detail.getOrderId());
        orderVO.setGoodsName(detail.getGoodsName());
        // 一堆轉換代碼...
        return Result.success(orderVO);
    } catch (NullPointerException e) {
        log.error("空指針異常", e);
        return Result.fail("系統異常,請重試");
    } catch (BusinessException e) {
        log.error("業務異常", e);
        return Result.fail(e.getMessage());
    } catch (Exception e) {
        log.error("未知異常", e);
        return Result.fail("系統繁忙,請稍后再試");
    }
}

這代碼看著就累 —— 每個接口都要寫一遍 try-catch,異常信息返回得還不統一,有的返回 “系統異常”,有的返回 “請重試”,前端同學還得專門做適配。更要命的是,一旦忘了加 log,出了問題連排查都沒法排查。

3. 職責混亂:Controller 變成 “大雜燴”

最離譜的是有些 Controller 里塞滿了業務邏輯,查數據庫、調第三方接口、數據轉換... 啥都干,比如這樣:

@PostMapping("/refundOrder")
public Result refundOrder(String orderId) {
    try {
        // 1. 校驗訂單狀態(業務邏輯)
        OrderDO orderDO = orderMapper.selectById(orderId);
        if (orderDO == null) {
            return Result.fail("訂單不存在");
        }
        if (orderDO.getStatus() != 2) { // 2代表已支付
            return Result.fail("只有已支付的訂單才能退款");
        }
        // 2. 調用支付接口退款(第三方交互)
        PayRefundRequest request = new PayRefundRequest();
        request.setOrderId(orderId);
        request.setAmount(orderDO.getAmount());
        PayRefundResponse response = payClient.refund(request);
        if (!"SUCCESS".equals(response.getCode())) {
            return Result.fail("退款失敗:" + response.getMsg());
        }
        // 3. 更新訂單狀態(數據庫操作)
        orderDO.setStatus(3); // 3代表已退款
        orderDO.setRefundTime(new Date());
        orderMapper.updateById(orderDO);
        // 4. 發送退款通知(消息推送)
        noticeClient.sendNotice(orderDO.getUserId(), "您的訂單" + orderId + "已退款");
        return Result.success();
    } catch (Exception e) {
        log.error("退款異常", e);
        return Result.fail("退款失敗");
    }
}

這 Controller 簡直是個 “全能選手”,從業務校驗到數據庫操作,再到第三方調用,全堆在這兒了。后來要加 “退款金額校驗”,得在這堆代碼里插一句;要改通知模板,又得在這兒找半天。維護的時候,鼠標滾輪都快磨平了。如果你也寫過這樣的 Controller,別慌,不是你菜,是沒找對方法。接下來咱就跟著阿里大佬的思路,把這些問題一個個解決,讓 Controller 清爽起來。

二、第一步:參數校驗 —— 用注解代替 “千層餅” if-else

阿里大佬說:參數校驗不該是 Controller 的 “負擔”,用 Spring 自帶的校驗注解,一句話就能搞定。

咱先把 Spring Validation 這個工具用起來,它能幫你把參數校驗的邏輯從 Controller 里 “摘” 出去,用注解的方式定義規則,簡單又高效。

1. 基礎玩法:給 DTO 加注解

首先,把參數封裝成 DTO(數據傳輸對象),然后在字段上加上校驗注解,比如 @NotNull、@NotBlank、@Min 這些:

// 訂單創建DTO
@Data
public class OrderCreateDTO {
    // 商品ID:不能為空
    @NotBlank(message = "商品ID不能為空")
    private String goodsId;
    // 訂單金額:不能為null,且大于0
    @NotNull(message = "訂單金額不能為空")
    @DecimalMin(value = "0.01", message = "訂單金額必須大于0")
    private BigDecimal amount;
    // 收貨地址:不能為空,且長度不超過200
    @NotBlank(message = "收貨地址不能為空")
    @Size(max = 200, message = "收貨地址長度不能超過200字")
    private String address;
    // 支付方式:只能是1、2、3
    @NotNull(message = "支付方式不能為空")
    @InEnum(value = PayTypeEnum.class, message = "支付方式無效(1-微信,2-支付寶,3-銀行卡)")
    private Integer payType;
}

這里有幾個細節要注意:

  • @NotBlank 用于字符串,校驗 “不為空且不是純空格”;@NotNull 用于非字符串(比如 Integer、BigDecimal),校驗 “不為 null”;@NotEmpty 用于集合,校驗 “不為空且長度大于 0”—— 別用混了。
  • @InEnum 是自定義注解(后面會講),用來校驗參數是否在枚舉值里,比原來的 Arrays.asList 優雅多了。
  • 每個注解都加了 message,這樣校驗失敗時能直接返回明確的提示,不用再手動寫。

然后在 Controller 方法的參數前加 @Validated 注解,Spring 就會自動幫你校驗:

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderService orderService;
    @PostMapping("/create")
    public Result createOrder(@Validated @RequestBody OrderCreateDTO orderDTO) {
        // 這里不用寫一行校驗代碼!校驗失敗會自動拋異常
        orderService.createOrder(orderDTO);
        return Result.success("創建訂單成功");
    }
}

你看,原來十幾行的校驗代碼,現在一行都不用寫了!如果參數不符合規則,Spring 會拋出 MethodArgumentNotValidException 異常,比如傳的金額是 0,就會拋出 “訂單金額必須大于 0” 的異常信息。

2. 進階玩法:分組校驗

有時候同一個 DTO 要在不同場景下用不同的校驗規則,比如 “新增用戶” 和 “修改用戶”:新增時不用傳 userId(自動生成),但修改時必須傳 userId。這時候就需要 “分組校驗”。

首先定義兩個空接口,代表不同的分組:

// 新增分組
public interface AddGroup {}
// 修改分組
public interface UpdateGroup {}

然后在 DTO 的注解里指定分組:

@Data
public class UserDTO {
    // 修改時必須傳,新增時不用傳
    @NotNull(message = "用戶ID不能為空", groups = UpdateGroup.class)
    private Long userId;
    // 新增和修改都必須傳
    @NotBlank(message = "用戶名不能為空", groups = {AddGroup.class, UpdateGroup.class})
    private String username;
    // 新增時必須傳,修改時可選
    @NotBlank(message = "密碼不能為空", groups = AddGroup.class)
    private String password;
}

最后在 Controller 里指定要使用的分組:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    // 新增用戶:用AddGroup分組的校驗規則
    @PostMapping("/add")
    public Result addUser(@Validated(AddGroup.class) @RequestBody UserDTO userDTO) {
        userService.addUser(userDTO);
        return Result.success("新增用戶成功");
    }
    // 修改用戶:用UpdateGroup分組的校驗規則
    @PutMapping("/update")
    public Result updateUser(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
        userService.updateUser(userDTO);
        return Result.success("修改用戶成功");
    }
}

這樣一來,新增用戶時不傳 userId 也沒問題,修改時不傳 userId 就會校驗失敗 —— 不用再寫兩個 DTO,也不用在 Controller 里加 if-else 判斷場景,優雅!

3. 高級玩法:自定義校驗注解

有時候自帶的注解不夠用,比如要校驗 “手機號格式”,這時候就可以自定義校驗注解。

比如定義一個 @Phone 注解:

// 自定義手機號校驗注解
@Target({ElementType.FIELD}) // 作用在字段上
@Retention(RetentionPolicy.RUNTIME) // 運行時生效
@Constraint(validatedBy = PhoneValidator.class) // 指定校驗器
public @interface Phone {
    // 校驗失敗的提示信息
    String message() default "手機號格式不正確";

    // 分組
    Class<?>[] groups() default {};

    // 負載
    Class<? extends Payload>[] payload() default {};
}

然后寫一個校驗器 PhoneValidator,實現 ConstraintValidator 接口:

// 手機號校驗器
publicclass PhoneValidator implements ConstraintValidator<Phone, String> {

    // 手機號正則表達式
    privatestaticfinal Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 如果手機號為空,不校驗(空值校驗交給@NotBlank)
        if (value == null || value.isEmpty()) {
            returntrue;
        }
        // 匹配正則
        return PHONE_PATTERN.matcher(value).matches();
    }
}

之后在 DTO 里直接用 @Phone 注解:

@Data
public class UserDTO {
    // 其他字段...

    @NotBlank(message = "手機號不能為空")
    @Phone(message = "手機號格式不正確(請輸入11位有效手機號)")
    private String phone;
}

這樣一來,手機號格式不對就會自動校驗失敗,不用再寫 if (!PHONE_PATTERN.matcher(phone).matches()) 這種代碼了。阿里大佬說,自定義校驗注解能解決 90% 的復雜參數校驗場景,而且復用性極高,下次其他 DTO 要校驗手機號,直接加個注解就行。

三、第二步:異常處理 —— 全局 “抓包” 代替 “粽子” try-catch

參數校驗失敗會拋異常,業務邏輯出錯也會拋異常,總不能每個接口都寫 try-catch 吧?阿里大佬的做法是:用全局異常處理器,把所有異常統一 “抓包” 處理。

Spring 提供了 @RestControllerAdvice 和 @ExceptionHandler 注解,能幫你實現全局異常處理 —— 不管哪個 Controller 拋了異常,都會被對應的 @ExceptionHandler 方法捕獲,然后統一返回格式。

1. 先定義統一響應格式

首先得有個統一的響應類,讓所有接口返回的格式都一樣,比如這樣:

// 統一響應類
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass Result<T> {
    // 狀態碼:200成功,其他失敗
    private Integer code;
    // 提示信息
    private String message;
    // 響應數據
    private T data;

    // 成功:無數據
    public static Result<Void> success() {
        returnnew Result<>(200, "操作成功", null);
    }

    // 成功:有數據
    publicstatic <T> Result<T> success(T data) {
        returnnew Result<>(200, "操作成功", data);
    }

    // 成功:自定義提示
    public static Result<Void> success(String message) {
        returnnew Result<>(200, message, null);
    }

    // 失敗:自定義狀態碼和提示
    public static Result<Void> fail(Integer code, String message) {
        returnnew Result<>(code, message, null);
    }

    // 失敗:默認狀態碼(400)
    public static Result<Void> fail(String message) {
        returnnew Result<>(400, message, null);
    }
}

這樣不管是成功還是失敗,前端拿到的都是 {code:..., message:..., data:...} 的格式,不用再適配不同的返回值了。

2. 寫全局異常處理器

然后寫一個全局異常處理器,捕獲各種異常:

// 全局異常處理器
@RestControllerAdvice
@Slf4j
publicclass GlobalExceptionHandler {

    // 1. 捕獲參數校驗異常(MethodArgumentNotValidException)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        // 獲取校驗失敗的提示信息
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        log.warn("參數校驗失敗:{}", message);
        // 返回400狀態碼和提示信息
        return Result.fail(message);
    }

    // 2. 捕獲自定義業務異常(BusinessException)
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("業務異常:{}", e.getMessage());
        // 業務異常一般返回400或自定義狀態碼
        return Result.fail(e.getCode(), e.getMessage());
    }

    // 3. 捕獲空指針異常(NullPointerException)
    @ExceptionHandler(NullPointerException.class)
    public Result<Void> handleNullPointerException(NullPointerException e) {
        log.error("空指針異常:", e); // 打印堆棧信息,方便排查
        // 空指針屬于系統異常,返回500狀態碼,不暴露具體信息
        return Result.fail(500, "系統繁忙,請稍后再試");
    }

    // 4. 捕獲其他所有異常(Exception)
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("未知異常:", e); // 打印堆棧信息
        return Result.fail(500, "系統繁忙,請稍后再試");
    }
}

這里有幾個關鍵要點:

  • 分異常類型處理:參數校驗異常(用戶輸入錯了)返回具體提示,業務異常(比如 “訂單已退款”)返回業務提示,系統異常(空指針、數據庫異常)返回通用提示 —— 既給用戶明確的反饋,又不暴露系統內部信息。
  • 統一日志記錄:參數校驗和業務異常用 warn 級別,系統異常用 error 級別并打印堆棧,方便排查問題。以前每個接口都要寫 log,現在一次搞定。
  • 不用再寫 try-catch:Controller 里拋異常就行,比如業務邏輯里判斷 “訂單已退款”,就拋 BusinessException:
@Service
public class OrderService {

    public void refundOrder(String orderId) {
        OrderDO orderDO = orderMapper.selectById(orderId);
        if (orderDO.getStatus() == 3) { // 3代表已退款
            // 拋自定義業務異常
            throw new BusinessException(400, "訂單已退款,無需重復操作");
        }
        // 其他業務邏輯...
    }
}

Controller 里就不用加 try-catch 了,清爽得很:

@PostMapping("/refund")
public Result refundOrder(@RequestParam String orderId) {
    orderService.refundOrder(orderId);
    return Result.success("退款成功");
}

如果訂單已退款,就會自動返回 {code:400, message:"訂單已退款,無需重復操作", data:null},前端直接拿 message 提示用戶就行 —— 再也不用在 Controller 里寫 “return Result.fail (...)” 了。

3. 自定義業務異常

上面用到了自定義的 BusinessException,這里簡單實現一下:

// 自定義業務異常
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass BusinessException extends RuntimeException {
    // 狀態碼
    private Integer code;
    // 提示信息
    private String message;

    // 簡化構造方法:默認狀態碼400
    public BusinessException(String message) {
        this.code = 400;
        this.message = message;
    }
}

繼承 RuntimeException 是因為 Spring 只捕獲運行時異常(RuntimeException),如果繼承 Exception(受檢異常),就需要在方法上聲明 throws,麻煩。有了這個異常,業務邏輯里遇到不符合規則的情況,直接拋就行,比如 “庫存不足”、“用戶未登錄”,全局異常處理器會自動捕獲并返回統一格式。

四、第三步:職責邊界 ——Controller 只做 “傳話筒”

阿里大佬反復強調:Controller 的職責只有三個:接收請求、返回響應、調用 Service,別把業務邏輯、數據庫操作、第三方調用塞進來。

咱先看一個優雅的 Controller 應該長什么樣:

@RestController
@RequestMapping("/order")
@Slf4j
publicclass OrderController {

    @Autowired
    private OrderService orderService;

    // 創建訂單
    @PostMapping("/create")
    public Result<OrderVO> createOrder(@Validated@RequestBody OrderCreateDTO orderDTO) {
        log.info("創建訂單:{}", JSON.toJSONString(orderDTO));
        OrderVO orderVO = orderService.createOrder(orderDTO);
        return Result.success(orderVO);
    }

    // 訂單詳情
    @GetMapping("/detail")
    public Result<OrderVO> getOrderDetail(@NotBlank(message = "訂單ID不能為空") @RequestParamString orderId) {
        log.info("查詢訂單詳情:orderId={}", orderId);
        OrderVO orderVO = orderService.getOrderDetail(orderId);
        return Result.success(orderVO);
    }

    // 訂單退款
    @PostMapping("/refund")
    public Result<Void> refundOrder(@NotBlank(message = "訂單ID不能為空") @RequestParamString orderId) {
        log.info("訂單退款:orderId={}", orderId);
        orderService.refundOrder(orderId);
        return Result.success("退款成功");
    }
}

你看,每個方法就三行左右代碼:打印日志(可選)、調用 Service、返回結果。沒有任何業務邏輯,沒有數據庫操作,沒有第三方調用 ——Controller 就像個 “傳話筒”,把請求傳給 Service,把 Service 的結果返回給前端。那原來 Controller 里的那些邏輯,該放哪兒呢?

1. 業務邏輯:全交給 Service

比如 “訂單退款” 的邏輯,應該放在 Service 里:

@Service
@Slf4j
publicclass OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private PayClient payClient;

    @Autowired
    private NoticeClient noticeClient;

    @Transactional// 事務注解也在Service里加
    public void refundOrder(String orderId) {
        // 1. 校驗訂單狀態(業務邏輯)
        OrderDO orderDO = getOrderById(orderId);
        checkOrderRefundStatus(orderDO);

        // 2. 調用支付接口退款(第三方交互)
        PayRefundResponse response = callPayRefund(orderDO);

        // 3. 更新訂單狀態(數據庫操作)
        updateOrderRefundStatus(orderDO);

        // 4. 發送退款通知(消息推送)
        sendRefundNotice(orderDO);

        log.info("訂單退款成功:orderId={}", orderId);
    }

    // 私有方法:拆分邏輯,提高可讀性
    private OrderDO getOrderById(String orderId) {
        OrderDO orderDO = orderMapper.selectById(orderId);
        if (orderDO == null) {
            thrownew BusinessException("訂單不存在");
        }
        return orderDO;
    }

    private void checkOrderRefundStatus(OrderDO orderDO) {
        if (orderDO.getStatus() != 2) { // 2代表已支付
            thrownew BusinessException("只有已支付的訂單才能退款");
        }
        if (orderDO.getRefundStatus() == 1) { // 1代表已申請退款
            thrownew BusinessException("訂單已申請退款,請勿重復操作");
        }
    }

    private PayRefundResponse callPayRefund(OrderDO orderDO) {
        PayRefundRequest request = new PayRefundRequest();
        request.setOrderId(orderDO.getOrderId());
        request.setAmount(orderDO.getAmount());
        PayRefundResponse response = payClient.refund(request);
        if (!"SUCCESS".equals(response.getCode())) {
            thrownew BusinessException("調用支付接口失敗:" + response.getMsg());
        }
        return response;
    }

    private void updateOrderRefundStatus(OrderDO orderDO) {
        OrderDO updateDO = new OrderDO();
        updateDO.setId(orderDO.getId());
        updateDO.setStatus(3); // 3代表已退款
        updateDO.setRefundStatus(1);
        updateDO.setRefundTime(new Date());
        int rows = orderMapper.updateById(updateDO);
        if (rows != 1) {
            thrownew BusinessException("更新訂單狀態失敗");
        }
    }

    private void sendRefundNotice(OrderDO orderDO) {
        try {
            noticeClient.sendNotice(orderDO.getUserId(), "您的訂單" + orderDO.getOrderId() + "已退款");
        } catch (Exception e) {
            // 通知失敗不影響主流程,記錄日志即可
            log.error("發送退款通知失敗:userId={}, orderId={}", orderDO.getUserId(), orderDO.getOrderId(), e);
        }
    }
}

這樣拆分后,每個方法只做一件事,可讀性極高 —— 要改 “退款狀態校驗”,就找 checkOrderRefundStatus 方法;要改支付接口參數,就找 callPayRefund 方法。以后維護的時候,不用再在 Controller 里翻來翻去了。

2. DTO/VO 轉換:用工具代替 “手擼”

很多人在 Controller 里寫 DTO 轉 Entity、Entity 轉 VO 的代碼,比如這樣:

// 不優雅的轉換方式
OrderVO orderVO = new OrderVO();
orderVO.setOrderId(orderDO.getOrderId());
orderVO.setGoodsName(orderDO.getGoodsName());
orderVO.setAmount(orderDO.getAmount());
orderVO.setStatusDesc(orderDO.getStatus() == 1 ? "待支付" : "已支付");
// 一堆set方法...

如果字段多,這代碼能寫幾十行,還容易漏改。阿里大佬的做法是:用 MapStruct 工具自動生成轉換代碼,不用手動寫 set 方法。首先在 pom.xml 里加依賴(以 Maven 為例):

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.3.Final</version>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.3.Final</version>
    <scope>provided</scope>
</dependency>

然后定義一個轉換接口:

// DTO/VO/Entity 轉換接口
@Mapper(componentModel = "spring") // componentModel="spring" 表示生成的實現類會被Spring管理
publicinterface OrderConverter {

    // 單例(MapStruct會自動實現)
    OrderConverter INSTANCE = Mappers.getMapper(OrderConverter.class);

    // Entity轉VO
    OrderVO doToVo(OrderDO orderDO);

    // DTO轉Entity
    @Mapping(target = "id", ignore = true) // 忽略id字段(新增時自動生成)
    @Mapping(target = "createTime", expression = "java(new java.util.Date())") // 自定義createTime為當前時間
    OrderDO dtoToDo(OrderCreateDTO orderDTO);

    // 批量轉換:List<Entity>轉List<VO>
    List<OrderVO> doListToVoList(List<OrderDO> orderDOList);
}

這里的 @Mapping 注解很強大:

  • ignore = true:忽略某個字段,比如新增時不用傳 id。
  • expression:自定義字段值,比如 createTime 設為當前時間。
  • source:指定源字段,比如 DTO 里的 goodsId 對應 Entity 里的 productId,可以寫 @Mapping (source = "goodsId", target = "productId")。

然后在 Service 里直接用:

// Entity轉VO
OrderVO orderVO = OrderConverter.INSTANCE.doToVo(orderDO);

// DTO轉Entity
OrderDO orderDO = OrderConverter.INSTANCE.dtoToDo(orderDTO);

// 批量轉換
List<OrderVO> orderVOList = OrderConverter.INSTANCE.doListToVoList(orderDOList);

MapStruct 會在編譯時自動生成實現類,底層還是 set 方法,但不用你手動寫了 —— 既優雅又不容易出錯。如果字段名一致,連 @Mapping 都不用加,直接寫方法就行。

3. 數據庫操作:Service 調用 Mapper

數據庫操作(CRUD)應該放在 Mapper 層(MyBatis 或 JPA),Service 調用 Mapper,Controller 不直接碰數據庫。

比如 OrderMapper:

@Mapper
public interface OrderMapper {
    OrderDO selectById(String orderId);

    int insert(OrderDO orderDO);

    int updateById(OrderDO orderDO);

    List<OrderDO> selectByUserId(Long userId);
}

Service 里注入 Mapper 調用:

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public OrderDO getOrderById(String orderId) {
        return orderMapper.selectById(orderId);
    }
}

這樣分層清晰:Controller -> Service -> Mapper,每個層只做自己的事,符合 “單一職責原則”。以后要換數據庫框架(比如從 MyBatis 換成 JPA),只需要改 Mapper 層,Service 和 Controller 都不用動。

五、第四步:錦上添花 —— 讓 Controller 更專業

解決了參數校驗、異常處理、職責邊界這三個核心問題,Controller 已經很優雅了。但阿里大佬還會加一些 “細節”,讓 Controller 更專業、更好用。

1. 接口版本控制

隨著業務迭代,接口可能需要升級,比如 V1 版的訂單接口返回的字段少,V2 版需要加更多字段。這時候不能直接改舊接口,否則會影響正在使用 V1 接口的用戶。

阿里常用的做法是 “URL 路徑版本控制”,在 URL 里加版本號:

@RestController
@RequestMapping("/order/{version}") // 版本號放在URL路徑里
publicclass OrderController {

    // V1版接口:返回基礎字段
    @PostMapping("/create")
    public Result<OrderVO> createOrderV1(
            @PathVariable("version") String version, // 版本號
            @Validated@RequestBody OrderCreateDTO orderDTO) {
        if (!"v1".equals(version)) {
            thrownew BusinessException("版本號無效");
        }
        OrderVO orderVO = orderService.createOrderV1(orderDTO);
        return Result.success(orderVO);
    }

    // V2版接口:返回更多字段
    @PostMapping("/create")
    public Result<OrderVO> createOrderV2(
            @PathVariable("version") String version,
            @Validated@RequestBody OrderCreateDTO orderDTO) {
        if (!"v2".equals(version)) {
            thrownew BusinessException("版本號無效");
        }
        OrderVO orderVO = orderService.createOrderV2(orderDTO);
        return Result.success(orderVO);
    }
}

調用的時候,V1 接口是 /order/v1/create,V2 接口是 /order/v2/create—— 舊用戶繼續用 V1,新用戶用 V2,互不影響。也可以用 “請求頭版本控制”,在請求頭里加 X-API-Version: v1,然后在 Controller 里用 @RequestHeader 獲取版本號,這種方式 URL 更簡潔,但需要前端配合傳請求頭。

2. 接口文檔自動生成

手寫接口文檔又麻煩又容易錯,阿里大佬都會用 Swagger 或 Knife4j 自動生成接口文檔 —— 寫代碼的時候加幾個注解,就能生成在線文檔,前端同學可以直接在文檔上測試接口。

以 Knife4j(Swagger 的增強版,更符合國內習慣)為例,先加依賴:

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

然后寫個配置類:

@Configuration
@EnableOpenApi// 啟用Swagger
public class Knife4jConfig {

    @Bean
    public Docket createRestApi() {
        returnnewDocket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                // 掃描所有有@RestController注解的類
                .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class))
                .paths(PathSelectors.any())
                .build();
    }

    // 文檔信息
    privateApiInfoapiInfo() {
        returnnewApiInfoBuilder()
                .title("訂單系統接口文檔")
                .description("訂單系統的所有接口,包括創建訂單、查詢訂單、退款等")
                .version("1.0.0")
                .contact(new Contact("技術團隊", "https://xxx.com", "xxx@xxx.com"))
                .build();
    }
}

然后在 Controller 和 DTO 里加注解:

// Controller 注解
@RestController
@RequestMapping("/order")
@Api(tags = "訂單管理接口") // 接口分組名稱
public class OrderController {

    // 接口注解
    @PostMapping("/create")
    @ApiOperation("創建訂單") // 接口名稱
    @ApiImplicitParams({
            @ApiImplicitParam(name = "orderDTO", value = "訂單創建參數", required = true, dataType = "OrderCreateDTO")
    }) // 接口參數描述
    public Result<OrderVO> createOrder(@Validated@RequestBody OrderCreateDTO orderDTO) {
        // ...
    }
}

// DTO 注解
@Data
@ApiModel("訂單創建參數") // DTO描述
public class OrderCreateDTO {

    @NotBlank(message = "商品ID不能為空")
    @ApiModelProperty(value = "商品ID", required = true, example = "goods123") // 字段描述
    private String goodsId;

    @NotNull(message = "訂單金額不能為空")
    @DecimalMin(value = "0.01", message = "訂單金額必須大于0")
    @ApiModelProperty(value = "訂單金額", required = true, example = "99.99")
    private BigDecimal amount;
}

啟動項目后,訪問 http://localhost:8080/doc.html,就能看到在線接口文檔,還能直接填寫參數測試接口 —— 前端同學再也不用追著你要文檔了,你也不用再手動維護文檔了。

3. 接口限流(可選)

如果接口訪問量很大,比如秒殺接口,需要加限流,防止系統被打垮。阿里常用的是 Redis + 注解實現限流,比如自定義一個 @RateLimit 注解:

// 限流注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    // 限流key前綴
    String prefix() default "rate_limit:";

    // 限流時間(秒)
    int time() default 60;

    // 限流次數
    int count() default 100;
}

然后寫一個切面,攔截加了 @RateLimit 注解的方法:

@Aspect
@Component
@Slf4j
publicclass RateLimitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Pointcut("@annotation(com.xxx.annotation.RateLimit)")
    publicvoid rateLimitPointcut() {}

    @Around("rateLimitPointcut() && @annotation(rateLimit)")
    publicObject around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        // 1. 生成限流key(比如:rate_limit:order:create:192.168.1.1)
        String ip = getClientIp(); // 獲取客戶端IP
        String methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
        String key = rateLimit.prefix() + methodName + ":" + ip;

        // 2. 用Redis實現限流(INCR + EXPIRE)
        Long currentCount = redisTemplate.opsForValue().increment(key, 1);
        if (currentCount == 1) {
            redisTemplate.expire(key, rateLimit.time(), TimeUnit.SECONDS);
        }

        // 3. 判斷是否超過限流次數
        if (currentCount > rateLimit.count()) {
            log.warn("接口限流:key={}, count={}, limit={}", key, currentCount, rateLimit.count());
            thrownew BusinessException("請求過于頻繁,請稍后再試");
        }

        // 4. 沒超過限流,執行原方法
        return joinPoint.proceed();
    }

    // 獲取客戶端IP(簡化版)
    privateString getClientIp() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

然后在需要限流的接口上加 @RateLimit 注解:

@PostMapping("/seckill")
@RateLimit(time = 60, count = 10) // 60秒內最多訪問10次
public Result<Void> seckillOrder(@RequestParam String goodsId) {
    orderService.seckillOrder(goodsId);
    return Result.success("秒殺成功");
}

這樣一來,同一個 IP 60 秒內最多只能訪問 10 次秒殺接口,防止惡意刷接口 —— 這個功能不是所有接口都需要,但對于高并發接口來說很有用。

六、總結:優雅 Controller 的 “黃金法則”

看到這里,你應該明白阿里大佬的 Controller 為什么優雅了 —— 不是用了多高深的技術,而是把 “簡單的事情做到極致”。最后總結一下優雅 Controller 的 “黃金法則”:

  • 參數校驗:注解化

用 Spring Validation 注解代替 if-else,復雜場景自定義校驗注解,分組校驗適配多場景。

  • 異常處理:全局化

用 @RestControllerAdvice + @ExceptionHandler 統一捕獲異常,自定義業務異常區分業務錯誤和系統錯誤,統一響應格式。

  • 職責邊界:清晰化

Controller 只做 “接收請求、返回響應、調用 Service”,業務邏輯放 Service,數據庫操作放 Mapper,DTO/VO 轉換用 MapStruct。

  • 細節優化:專業化

加接口版本控制避免兼容問題,用 Knife4j 自動生成接口文檔,高并發接口加限流保護系統。

按照這個法則寫出來的 Controller,代碼清爽、職責明確、易于維護,同事接手的時候不會罵街,你自己調試的時候也不會血壓飆升 —— 這才是阿里大佬真正的 “優雅”。

責任編輯:武曉燕 來源: 石杉的架構筆記
相關推薦

2024-09-18 00:26:25

Excel服務器框架

2025-02-05 14:28:19

2024-06-24 14:19:48

2024-11-25 13:49:00

2024-04-30 08:05:15

Rust代碼計算

2025-03-03 08:49:59

2024-10-28 08:32:22

統一接口響應SpringBoot響應框架

2022-09-07 12:00:26

Python3D游戲

2024-09-03 10:44:32

2014-02-28 13:46:35

Angular代碼

2023-09-02 11:21:54

代碼ChatGPT

2025-07-25 09:33:40

2025-05-30 08:20:54

2024-11-12 08:20:31

2022-02-22 12:51:39

存儲過程JavaSQL

2021-04-13 05:40:01

抓包藍屏Linux

2024-02-04 19:15:09

Nest.js管理項目

2021-07-05 08:30:18

阿里技術工程師

2021-04-27 06:37:33

ForkJoin面試

2024-10-14 08:46:50

Controller開發代碼
點贊
收藏

51CTO技術棧公眾號

无码h黄肉3d动漫在线观看| 欧美黑吊大战白妞| 涩涩涩久久久成人精品| 亚洲人成网站精品片在线观看 | 自拍偷拍一区二区三区| 午夜久久久久久久久久| 母乳一区在线观看| 久久精品国产清自在天天线| 制服丝袜在线第一页| 主播大秀视频在线观看一区二区| 亚洲三级在线免费观看| 欧美精品一区在线发布| 国产免费一区二区三区免费视频| 在线不卡亚洲| 日韩在线观看av| 中文字字幕码一二三区| 麻豆精品在线| 欧美性色黄大片| 亚洲自偷自拍熟女另类| 97超碰在线公开在线看免费| 久久亚区不卡日本| 成人av片网址| 97在线公开视频| 丝瓜av网站精品一区二区| 欧美激情久久久久| 国产亚洲精品久久久久久豆腐| caoporn成人免费视频在线| 欧美色偷偷大香| 国产女女做受ⅹxx高潮| 俺来也官网欧美久久精品| 国产精品成人午夜| 日韩久久在线| 你懂得在线网址| 成人国产电影网| 18成人免费观看网站下载| 国产男人搡女人免费视频| 99国产精品99久久久久久粉嫩| 久久精品国产亚洲一区二区| 国产精品av久久久久久无| 青青草原在线亚洲| 亚洲精品98久久久久久中文字幕| 熟妇无码乱子成人精品| 成人在线分类| 欧美人成免费网站| www.日本一区| 国产精品亲子伦av一区二区三区| 日韩欧美国产成人| 99999精品视频| 色综合桃花网| 福利精品视频在线| 熟女性饥渴一区二区三区| 蜜桃视频动漫在线播放| 亚洲一区二区三区四区五区中文| 亚洲欧美日韩不卡| 特级毛片在线| 亚洲国产欧美在线| 狠狠干 狠狠操| 日韩伦理福利| 色av成人天堂桃色av| 成人免费毛片播放| 免费高清视频在线一区| 欧美日韩一区二区三区在线 | 伊人男人综合视频网| 久久久久久国产精品无码| 久草成人资源| 中文字幕一精品亚洲无线一区| 99热这里只有精品4| 久久久精品久久久久久96| 久久亚洲欧美日韩精品专区 | 视频一区二区三区在线| 国产精品扒开腿爽爽爽视频| 中文天堂在线资源| 国产一区在线看| 国产精品初高中精品久久| 污视频网站在线播放| 久久久久久久久蜜桃| 亚洲日本一区二区三区在线不卡 | |精品福利一区二区三区| 国产小视频免费| 福利在线免费视频| 在线精品视频免费播放| 91欧美一区二区三区| 成人在线超碰| 一区二区三区视频免费在线观看 | 性做久久久久久久久| 日本三级免费观看| 先锋影音一区二区| 亚洲国产一区自拍| 调教驯服丰满美艳麻麻在线视频| 91精品啪在线观看国产81旧版| 欧美激情18p| 岛国av中文字幕| 国产一区 二区 三区一级| 国内一区在线| 免费黄色网页在线观看| 亚洲大尺度视频在线观看| 精品久久久久久无码国产| 蜜桃精品一区二区三区| 亚洲欧美日韩一区二区在线| 老司机成人免费视频| 在线亚洲国产精品网站| 国产一区深夜福利| 四虎精品在线| 伊人开心综合网| 噼里啪啦国语在线观看免费版高清版| 精品中文视频| 国产一区二区三区久久精品| 国产一级在线播放| 麻豆国产欧美日韩综合精品二区| 国产视色精品亚洲一区二区| 麻豆网在线观看| 色成人在线视频| 800av在线播放| 综合日韩在线| 国产精品久久久91| 五月激情丁香婷婷| 伊人夜夜躁av伊人久久| 9l视频白拍9色9l视频| 一本色道久久综合狠狠躁的番外| 欧美成人激情图片网| 中文字幕乱码视频| 久久久精品欧美丰满| 日韩欧美不卡在线| 亚洲小说春色综合另类电影| 少妇高潮久久77777| 黄色一级片免费在线观看| 国产suv一区二区三区88区| 制服国产精品| 久久99国产精品二区高清软件| 日韩成人在线视频网站| 国产精品99无码一区二区| 国产成人aaaa| 国内外成人激情免费视频| 啪啪av大全导航福利综合导航| 亚洲免费视频观看| 日本特级黄色片| 99久久久国产精品| 91专区在线观看| 欧美福利在线播放网址导航| 欧美高清视频在线| 亚洲乱色熟女一区二区三区| 亚洲日本中文字幕区| 激情文学亚洲色图| 欧美国产美女| 成人乱人伦精品视频在线观看| 91在线不卡| 欧美日韩一区三区| 欧美h片在线观看| 激情图区综合网| 中文字幕一区二区三区四区五区人| 成人久久网站| 日韩一区二区欧美| 99国产精品99| 一区二区三区欧美日| 免费观看一区二区三区| 国产综合色产| 精品一区二区国产| 一区二区三区四区日本视频| 亚洲欧美国内爽妇网| 国产suv精品一区二区33| 国产午夜精品理论片a级大结局| 四虎永久在线精品无码视频| 国语产色综合| 91久久精品美女高潮| 成人av福利| 日韩一区二区免费在线电影| 免费三片在线播放| 99在线精品视频| 免费在线观看日韩视频| 欧美日韩在线二区| 96国产粉嫩美女| 国产第一页在线| 亚洲毛片在线观看| 91麻豆成人精品国产| 亚洲激情图片一区| 噜噜噜在线视频| 蜜桃av一区二区三区电影| 精品一区二区成人免费视频| 精品综合久久88少妇激情| 日本乱人伦a精品| 国产网站在线免费观看| 亚洲成色777777女色窝| 波多野结衣理论片| 一区二区三区在线不卡| 性欧美成人播放77777| 久久国产剧场电影| 国产原创中文在线观看| 四季av在线一区二区三区| 成人女人免费毛片| 日本少妇一区| 欧美国产激情18| 成人亚洲性情网站www在线观看| 欧美一区二区成人6969| 国产精品999在线观看| 国产精品久久免费看| 久久性爱视频网站| 日韩成人精品在线观看| 国产精品久久久久9999爆乳| 欧美少妇性xxxx| 国产原创精品| 国产精品高清一区二区 | 黄色成人小视频| 国内精品久久久久久中文字幕| 成人免费黄色网页| 亚洲国产成人av在线| 亚洲最大成人av| 欧美午夜激情在线| 久草资源在线视频| 国产精品美女一区二区三区| 亚洲精品女人久久久| 国产乱子伦视频一区二区三区| aaa毛片在线观看| 国产精品xvideos88| 手机看片福利永久国产日韩| 卡一精品卡二卡三网站乱码| 91免费观看网站| 国产91在线播放精品| 欧美中文在线观看国产| 美足av综合网| 久久影视电视剧免费网站| 国产视频在线看| 日韩精品视频中文在线观看| 精品人妻一区二区三区含羞草| 欧美性猛片xxxx免费看久爱| 一级成人黄色片| 亚洲成a人片综合在线| 欧美国产日韩在线观看成人| 中文字幕不卡在线| 国产毛片久久久久久久| 99精品黄色片免费大全| 熟妇高潮一区二区| 成人免费观看男女羞羞视频| 亚洲精品一二三四| 国产最新精品免费| 国产传媒免费观看| 极品少妇一区二区三区精品视频| 亚洲xxxx2d动漫1| 日韩电影在线观看网站| www.xxx亚洲| 日韩精品久久久久久| 熟妇人妻va精品中文字幕| 亚洲一区免费| 37pao成人国产永久免费视频| 日韩一区二区免费看| 少妇人妻在线视频| 亚洲欧美日本国产专区一区| 国产又黄又大又粗视频| 久久精品电影| 男人天堂网视频| 三级久久三级久久久| 一级特黄性色生活片| 奇米精品一区二区三区四区| 久久久久久蜜桃一区二区| 久久高清免费观看| 日韩a在线播放| 日韩高清在线不卡| 中文字幕国产免费| 韩国成人在线视频| 亚洲美女高潮久久久| 99精品视频在线观看| 中文字幕在线免费看线人| 国产欧美在线观看一区| 超薄肉色丝袜一二三| 亚洲狼人国产精品| 久久中文字幕无码| 欧美午夜久久久| 中文av免费观看| 日韩欧美一区二区三区在线| 免费看国产片在线观看| 亚洲欧洲国产精品| 久草免费在线| 国内精品视频久久| 羞羞影院欧美| 91久久久在线| 免费福利视频一区| 视频一区国产精品| 中文精品电影| 免费无遮挡无码永久视频| 日本不卡123| 中文在线字幕观看| 久久久久国产一区二区三区四区| 免费在线观看黄色小视频| 亚洲福利电影网| 亚洲 小说区 图片区| 日韩欧美中文一区| 欧美女子与性| 欧美成人四级hd版| 日本高清不卡一区二区三区视频| 91久久久久久久久久久久久| 伦理一区二区| 一区二区三区日韩视频| 在线综合亚洲| 6080国产精品| 久久久久久电影| 久久精品国产亚洲av麻豆色欲 | 成人久久综合| 欧美高清中文字幕| 日本午夜一区二区| 中国免费黄色片| 中文字幕一区二区三区四区| 97超碰人人干| 欧美一二三在线| 3p视频在线观看| 欧美亚洲视频一区二区| 欧州一区二区三区| 视频一区免费观看| 99国产精品视频免费观看一公开| 性chinese极品按摩| 91热门视频在线观看| 欧美偷拍第一页| 欧美私人免费视频| 男女视频在线观看免费| 久久久免费高清电视剧观看| 成人豆花视频| 亚洲欧洲国产精品久久| 久久婷婷影院| 加勒比精品视频| 亚洲国产精品综合小说图片区| 91高潮大合集爽到抽搐| 亚洲天堂男人天堂女人天堂| 毛片在线网站| 国产精品久久久久免费| 欧美ab在线视频| www.51色.com| 亚洲欧洲一区二区在线播放| 一级一级黄色片| 亚洲无限av看| 偷拍精品精品一区二区三区| 精品欧美一区二区在线观看视频| 国产精品第十页| 亚洲av午夜精品一区二区三区| 亚洲欧美一区二区三区国产精品| 在线观看毛片av| 中文字幕在线看视频国产欧美在线看完整| 婷婷电影在线观看| 久久亚洲免费| 免费日韩视频| 中国毛片在线观看| 日韩欧美国产黄色| 免费在线国产| 久久免费视频观看| 激情小说亚洲图片| 国产日韩av网站| 成人黄色一级视频| 精品成人久久久| 亚洲精品中文字幕有码专区| 成年美女黄网站色大片不卡| 欧洲视频一区二区三区| 日本欧美加勒比视频| 天天操天天摸天天舔| 91.com在线观看| 色呦呦网站在线观看| 成人黄色片视频网站| 国产欧美精品久久| 日本黄色特级片| 91福利区一区二区三区| 在线免费观看的av网站| 亚洲精品欧美一区二区三区| 国产精品第十页| 亚洲成人日韩在线| 欧美主播一区二区三区美女| 免费**毛片在线| 国产精品美女诱惑| 久久婷婷av| 卡通动漫亚洲综合| 亚洲爱爱爱爱爱| 美女网站视频一区| 日本女人高潮视频| 成人精品小蝌蚪| 无码人妻丰满熟妇精品区| 在线成人中文字幕| 日韩在线视频一区二区三区| 黄色一级片在线看| 国产丝袜美腿一区二区三区| 国产影视一区二区| 国语自产精品视频在线看| 国产成人精品三级高清久久91| 天天影视色综合| 午夜影视日本亚洲欧洲精品| 裸体xxxx视频在线| 亚洲a在线播放| 亚洲一区二区动漫| 小泽玛利亚一区| 日韩高清免费在线| 99tv成人影院| 男人靠女人免费视频网站 | 亚洲欧美三级伦理| 国产高清日韩| 国产肥臀一区二区福利视频| 中文字幕在线观看一区二区| 色欲久久久天天天综合网| 国产精品夫妻激情| 国内综合精品午夜久久资源| 丁香花五月婷婷| 精品久久久网站| 日韩成人综合网| 国产女女做受ⅹxx高潮| 亚洲一区二区在线观看视频| av资源种子在线观看| 精品欧美一区二区精品久久|