精品欧美一区二区三区在线观看 _久久久久国色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技術棧公眾號

亚洲天堂男人av| 国产精品一区二区av交换| 91亚洲一线产区二线产区 | 欧美一级片在线视频| 2019亚洲男人天堂| 成人黄色网址在线观看| chinese偷拍一区二区三区| 国产男女免费视频| 精品国产91乱码一区二区三区| 成人三级高清视频在线看| 日本黄大片一区二区三区| 日韩欧美中文字幕制服| 精品久久一区| 国产精品久久久久久久久久久久久久久久久 | 久久精品aaaaaa毛片| 一区二区三区四区在线播放| 只有精品亚洲| 日本男人操女人| 中文字幕国内精品| 蜜臀久久99精品久久久画质超高清 | 欧美视频在线观看免费网址| 超碰在线亚洲| 亚洲黄色激情视频| 日韩精品伦理第一区| 日本高清视频一区二区| 国产欧美日韩影院| 国产乱码精品一区二区| 91成人综合网| 亚洲人成电影在线| 国产综合成人久久大片91| 色婷婷av在线| 成熟人妻av无码专区| 91香蕉国产在线观看| 午夜电影一区二区| 国产精品97| 少妇人妻一区二区| 亚洲国产高清av| 欧美精品第一页在线播放| 久久婷婷国产综合国色天香 | 国产精品久久久久久五月尺| 亚洲视频一二三| 日韩中出av| 国产77777| 国产精品嫩草影院8vv8| 海角国产乱辈乱精品视频| 国产精品麻豆网站| 自拍欧美一区| 色播色播色播色播色播在线| 亚洲av无一区二区三区久久| 国产精品91久久久久久| 亚洲成人动漫在线观看| 樱桃成人精品视频在线播放| 欧美尤物美女在线| 中国特黄一级片| 亚洲国产一区在线| 中文一区二区视频| 久久精子c满五个校花| 伊人久久大香线蕉av不卡| 少妇精品视频一区二区| 青青草视频播放| 欧美中文娱乐网| 中文字幕久久亚洲| 中文字幕一区二区三区不卡在线 | 久久久久久久久影视| 这里只有精品丝袜| 中文字幕av一区二区三区| 久久美女视频| 超碰在线免费播放| 国产亚洲欧美精品久久久www| 激情视频在线观看一区二区三区| 夜夜精品浪潮av一区二区三区| 精品中文在线| 亚洲人午夜射精精品日韩| 黄色aaa视频| 欧美三级午夜理伦三级老人| 欧美激情性做爰免费视频| 欧美日韩中文字幕综合视频 | 色天使在线观看| 国产精品色婷婷视频| 欧美色中文字幕| 成人性生交大片| 国产99久久| 亚洲色图美国十次| 精品国产www| 亚洲图片综合网| 日韩和欧美的一区二区| 久久视频在线免费观看| 粉嫩av一区二区三区免费野| 奇米精品一区二区三区在线观看| 亚洲插插视频| 狠狠躁夜夜躁av无码中文幕| 无码熟妇人妻av| 成人黄色片免费| 国产欧美在线播放| 亚洲人成在线一二| 黑人狂躁日本妞一区二区三区| 欧美韩国一区| 欧美成人福利| 国外av在线| 久久青青草原亚洲av无码麻豆 | 美洲天堂一区二卡三卡四卡视频 | 99久久夜色精品国产网站| 成人毛片免费看| 无人区在线高清完整免费版 一区二 | 精品国产成人av| 国产精品1024| 欧美三级特黄| 人人九九精品视频| 国产一区二区三区精品在线| 天天夜碰日日摸日日澡性色av| 97国产在线观看| 亚洲精品一区二区在线观看| 亚洲午夜电影在线| 国产suv一区二区三区88区| 综合久久久久| 精品嫩草影院| 欧美片第1页| 国产精品视频一区二区久久| 中文字幕在线观看视频一区| 又色又爽的视频| 在线成人免费av| 国产一区二区网| 天堂精品视频| 91精品国产一区二区三区动漫 | 日韩一区在线播放| 国产成人8x视频一区二区| 在线国产欧美| 成人a'v在线播放| 精品国产一区二区三区性色av| 色天堂在线视频| 国产乱码精品一区二区三区精东| 国产高清成人久久| 日本日本19xxxⅹhd乱影响| 亚欧精品在线| 精品999在线观看| 国产日韩欧美综合| 7m第一福利500精品视频| 一区三区二区视频| 日韩精品有码在线观看| 正在播放亚洲一区| 色偷偷88欧美精品久久久| 亚洲欧美另类小说视频| 国产精品入口麻豆原神| 99re热这里只有精品视频| 蜜臀av一级做a爰片久久| 夜夜嗨av一区二区三区网站四季av| 国产亚洲高清在线观看| 日本国产欧美| 色婷婷综合久久久中字幕精品久久| 丰满人妻一区二区三区无码av| 国产精品综合激情| 国产精品无码在线| 国产大尺度视频| 亚洲熟女乱综合一区二区| 国产91色在线观看| 色戒在线免费观看| jizz18女人| 九九热精品在线播放| 黄色三级视频在线| 一路向西2在线观看| 亚洲黄色a v| 伊人色在线观看| 亚洲国产成人va在线观看麻豆| 亚洲欧美日韩精品综合在线观看| 亚洲**2019国产| 亚洲人av在线影院| 亚洲一区二区国产| 在线播放国产一区中文字幕剧情欧美| 欧美高清精品3d| 91精品国产aⅴ一区二区| 日韩欧美123| 国产视频亚洲精品| 久久av中文字幕| 韩国美女主播一区| 国产成人一区二区三区| 91免费的视频在线播放| 国产日韩亚洲精品| 一区二区高清视频| 蜜臀精品一区二区| 中文字幕天天干| 第四色在线视频| 精品在线观看一区| 国产成人自拍视频在线| 国产一区二区三区成人| 欧美一级性视频| 日本中文字幕电影在线免费观看 | 精品国产亚洲一区二区三区| 国内精品国产成人国产三级粉色 | 亚洲按摩av| 国产成人精品亚洲日本在线观看| 国产精品yjizz视频网| 人人精品久久| 欧美日韩水蜜桃| 久久久国产亚洲精品| 成人免费高清在线| 亚洲精品视频观看| 这里是久久伊人| 久久成人在线视频| 亚洲欧美一区二区三区在线| 欧美日韩极品在线观看一区| 色综合久久中文综合久久97| 日韩一区二区三区电影| 久久噜噜噜精品国产亚洲综合 | 国产精品免费观看高清| 国产奶头好大揉着好爽视频| 女人帮男人橹视频播放| 91高清国产视频| 另类小说综合欧美亚洲| 欧美日韩精品免费观看视频完整| 国产高清在线| fc2ppv国产精品久久| 性欧美gay| 欧美激情影院| 91久久久久| 成人精品视频一区二区三区| 日韩美女视频一区| 欧美日韩中文国产| 一本色道久久88综合日韩精品| 亚洲精品在线91| 午夜精品蜜臀一区二区三区免费 | 麻豆天美蜜桃91| 五月激情丁香网| 蜜桃传媒在线观看免费进入 | 久久精品人人做人人综合| 欧美色涩在线第一页| 色综合男人天堂| 欧美成人综合一区| 黄色一级片免费播放| 国产精品自拍视频一区| 都市激情一区| 国产欧美一区二区三区米奇| 日本亚洲天堂网| 一区二区在线观看视频在线观看| 精品日韩中文字幕| 国产亚洲精品高潮| 国产精品国产三级欧美二区| 91小视频网站| 日本天堂网在线| 暖暖在线中文免费日本| 波多野结衣在线观看一区二区| 黄色成人在线网站| 成人免费小视频| 亚洲人成电影网站色www| 91亚色免费| 熟妇无码乱子成人精品| 中文在线免费观看| 国模套图日韩精品一区二区| 欧美日韩国产精品一区二区亚洲| 国产传媒久久文化传媒| 欧美日韩精品一区二区三区| 国产精品av网站| 91热这里只有精品| 欧美激情一区二区三区免费观看 | 五月天亚洲色图| 久久久国产精华| 色爱区综合激月婷婷| 精品av综合导航| 国产欧美日韩免费| aaa黄色大片| 黄色电影免费在线看| 久久久久久影院| 成人精品gif动图一区| 欧美精品一级二级三级| 欧美国产精品va在线观看| 婷婷久久五月天| 久久久久久福利| 成人日韩在线观看| www.亚洲色图.com| xxxxx91麻豆| 亚洲乱码国产一区三区| 国产成人手机在线| 国产精品久久占久久| 91久久香蕉国产日韩欧美9色| 久久久精品影院| 毛片一区二区三区四区| 亚洲免费不卡视频| 香蕉综合视频| 欧美中文字幕一区二区三区| 色噜噜狠狠狠综合曰曰曰| 91久久在线视频| 激情五月俺来也| 一区二区三区在线免费观看视频 | www.66久久| 日韩一区二区在线观看视频播放| 精品国产伦一区二区三区观看体验 | 国产三级在线免费| 成人性生交大片免费看中文视频| 欧美另类女人| 成人少妇影院yyyy| 色老汉一区二区三区| 国产精品久久一| 日本一极黄色片| 中文字幕第17页| 国产在线观看无码免费视频| 欧美大喷水吹潮合集在线观看| 男人添女人下部视频免费| 四虎永久免费影院| 黄色aaa毛片| 欧美一级全黄| 国产欧美一区二区在线| 国产精品免费在线免费| 三级黄色在线观看| 8848成人影院| 韩曰欧美视频免费观看| 欧美日韩国产三区| 伊人免费在线观看高清版| 欧美不卡高清| 亚洲天堂av网| 亚洲AV成人精品| 四虎影视4hu4虎成人| 亚洲欧美区自拍先锋| 欧美日韩另类综合| 国产精品无码白浆高潮| 国产亚洲成人一区| 久久精品美女视频网站| 亚洲欧美一区二区三区情侣bbw| 国产亚洲精品久久久| jizzjizz国产精品喷水| 神马亚洲视频| 日韩av中文字幕一区二区三区| 欧美人xxxx| 一区二区在线高清视频| av男人天堂网| 日韩视频精品在线观看| 在线a欧美视频| 爱情岛论坛亚洲自拍| 久久电影网站| 国产女主播视频一区二区| 久久riav二区三区| 全部毛片永久免费看| 国产亚洲精aa在线看| 国产欧美日本一区二区三区| 久久视频在线播放| 182午夜在线观看| 亚洲经典一区二区三区| 香港欧美日韩三级黄色一级电影网站| 成人激情av网| 国产一区二区三区18| 亚洲中文字幕无码不卡电影| 老司机在线看片网av| 91丨porny丨户外露出| 91在线中文字幕| 日批视频免费在线观看| 一区在线视频| 欧美xxxx做受欧美.88| 美国黑人一级大黄| 亚洲人成网77777色在线播放 | 久久久国产成人| 欧美1级片网站| 国产亚洲精品久久久优势| japan高清日本乱xxxxx| 国产精品一区二区三区四区在线观看 | 天堂精品视频| 亚洲 欧美 变态 另类 综合| 天堂久久一区| 亚洲无人区一区| 欧美自拍资源在线| 91丨九色丨蝌蚪丨对白| 7777久久香蕉成人影院| 亚洲欧美国产va在线影院| 麻豆免费在线观看视频| 亚洲综合视频| 91精品国产欧美一区二区18| 黄色成人免费看| 国产亚洲精彩久久| 欧美日韩精品免费| 国内av一区二区| 国产熟女一区二区丰满| 成人资源在线| 色综合久久综合网| 97视频久久久| 国产在线高清视频| 国产精品蜜臀av| 国产精品av一区| 一级黄色大片网站| 久99久精品视频免费观看| 国产精品偷伦免费视频观看的| 欧美一级小视频| 精品欧美视频| 亚洲经典中文字幕| 人妻av无码一区二区三区| 人人狠狠综合久久亚洲婷婷| 久久五月天色综合| 国产无码精品在线观看| 亚洲国产精品第一区二区| 清纯唯美亚洲激情| 国产精品爽爽久久久久久| 成人黄色一级视频| 亚洲春色在线视频| 不卡的av影片| 欧美一区二区三区四区高清| 中文字幕在线播放一区| 91精品亚洲| 国产成人+综合亚洲+天堂| www.天天干.com| 国产精品mm| 国产日韩在线精品av| 欧美精品a∨在线观看不卡 | 四虎永久免费观看| 成人看的羞羞网站|