搞定SpringBoot難題!設(shè)計(jì)優(yōu)秀的后端接口?輕松解決

1 概述

本篇文章以Spring Boot為基礎(chǔ),從以下三個(gè)方向講述了如何設(shè)計(jì)一個(gè)優(yōu)秀的后端接口體系:

  • 參數(shù)校驗(yàn):涉及Hibernate Validator的各種注解,快速失敗模式,分組,組序列以及自定義注解/Validator
  • 異常處理:涉及ControllerAdvice/@RestControllerAdvice以及@ExceptionHandler
  • 數(shù)據(jù)響應(yīng):涉及如何設(shè)計(jì)一個(gè)響應(yīng)體以及如何包裝響應(yīng)體

有了一個(gè)優(yōu)秀的后端接口體系,不僅有了規(guī)范,同時(shí)擴(kuò)展新的接口也很容易,本文演示了如何從零一步步構(gòu)建一個(gè)優(yōu)秀的后端接口體系。

2 新建工程

打開熟悉的IDEA,選擇依賴:

首先創(chuàng)建如下文件:

TestController.java:

@RestController
@RequestMapping("/")
@CrossOrigin(value = "http://localhost:3000")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {
    private final TestService service;
    @PostMapping("test")
    public String test(@RequestBody User user)
    {
        return service.test(user);
    }

使用了@RequiredArgsConstructor代替@Autowired,由于筆者使用Postwoman測(cè)試,因此需要加上跨域注解@CrossOrigin,默認(rèn)3000端口(Postwoman端口)。

TestService.java:


@Service
public class TestService {
    public String test(User user)
    {
        if(StringUtils.isEmpty(user.getEmail()))
            return "郵箱不能為空";
        if(StringUtils.isEmpty(user.getPassword()))
            return "密碼不能為空";
        if(StringUtils.isEmpty(user.getPhone()))
            return "電話不能為空";
//        持久化操作
        return "success";
    }
}

業(yè)務(wù)層首先進(jìn)行了參數(shù)校驗(yàn),這里省略了持久化操作。

User.java:


@Data
public class User {
    private String phone;
    private String password;
    private String email;
}

3 參數(shù)校驗(yàn)

首先來(lái)看一下參數(shù)校驗(yàn),上面的例子中在業(yè)務(wù)層完成參數(shù)校驗(yàn),這是沒(méi)有問(wèn)題的,但是,還沒(méi)進(jìn)行業(yè)務(wù)操作就需要進(jìn)行這么多的校驗(yàn)顯然這不是很好,更好的做法是,使用Hibernate Validator。

3.1 Hibernate Validator

3.1.1 介紹

JSR是Java Specification Requests的縮寫,意思是Java規(guī)范提案,是指向JCP(Java Community Process)提出新增一個(gè)標(biāo)準(zhǔn)化技術(shù)規(guī)范的正式請(qǐng)求。JSR-303是Java EE6中的一項(xiàng)子規(guī)范,叫作Bean Validation,Hibernate Validator是Bean Validator的參考實(shí)現(xiàn),除了實(shí)現(xiàn)所有JSR-303規(guī)范中的內(nèi)置constraint實(shí)現(xiàn),還有附加的constraint,詳細(xì)如下:

  • @Null:被注解元素必須為null(為了節(jié)省篇幅下面用“元素”代表“被注解元素必須為”)
  • @NotNull:元素不為null
  • @AssertTrue:元素為true
  • @AssertFalse:元素為false
  • @Min(value):元素大于或等于指定值
  • @Max(value):元素小于或等于指定值
  • @DecimalMin(value):元素大于指定值
  • @DecimalMax(value):元素小于指定值
  • @Size(max,min):元素大小在給定范圍內(nèi)
  • @Digits(integer,fraction):元素字符串中的整數(shù)位數(shù)規(guī)定最大integer位,小數(shù)位數(shù)規(guī)定最大fraction位
  • @Past:元素是一個(gè)過(guò)去日期
  • @Future:元素是將來(lái)日期
  • @Pattern:元素需要符合正則表達(dá)式

其中Hibernate Validator附加的constraint如下:

  • @Eamil:元素為郵箱
  • @Length:字符串大小在指定范圍內(nèi)
  • @NotEmpty:字符串必須非空(目前最新的6.1.5版本已棄用,建議使用標(biāo)準(zhǔn)的@NotEmpty)
  • @Range:數(shù)字在指定范圍內(nèi)

而在Spring中,對(duì)Hibernate Validation進(jìn)行了二次封裝,添加了自動(dòng)校驗(yàn),并且校驗(yàn)信息封裝進(jìn)了特定的BindingResult中。下面看看如何使用。

3.1.2 使用

在各個(gè)字段加上@NotEmpty,并且郵箱加上@Email,電話加上11位限制,并且在各個(gè)注解加上message,表示對(duì)應(yīng)的提示信息:


@Data
public class User {
    @NotEmpty(message = "電話不能為空")
    @Length(min = 11,max = 11,message = "電話號(hào)碼必須11位")
    private String phone;
    @NotEmpty(message = "密碼不能為空")
    @Length(min = 6,max = 20,message = "密碼必須為6-20位")
    private String password;
    @NotEmpty(message = "郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    private String email;
}

對(duì)于String來(lái)說(shuō)有時(shí)候會(huì)使用@NotNull或@NotBlank,它們的區(qū)別如下:

  • @NotEmpty:不能為null并且長(zhǎng)度必須大于0,除了String外,對(duì)于Collection/Map/數(shù)組也適用
  • @NotBlank:只用于String,不能為null,并且調(diào)用trim()后,長(zhǎng)度必須大于0,也就是必須有除空格外的實(shí)際字符
  • @NotNull:不能為null

接著把業(yè)務(wù)層的參數(shù)校驗(yàn)操作刪除,并把控制層修改如下:


@PostMapping("test")
public String test(@RequestBody @Valid User user, BindingResult bindingResult)
{
    if(bindingResult.hasErrors())
    {
        for(ObjectError error:bindingResult.getAllErrors())
            return error.getDefaultMessage();
    }
    return service.test(user);
}

在需要校驗(yàn)的對(duì)象上加上@Valid,并且加上BindingResult參數(shù),可以從中獲取錯(cuò)誤信息并返回。

3.1.3 測(cè)試

全部都使用錯(cuò)誤的參數(shù)設(shè)置,返回”郵箱格式不正確“:

第二次測(cè)試中除了密碼都使用正確的參數(shù),返回”密碼必須為6-20位“:

第三次測(cè)試全部使用正確的參數(shù),返回”success“:

3.2 校驗(yàn)?zāi)J皆O(shè)置

Hibernate Validator有兩種校驗(yàn)?zāi)J剑?/p>

  • 普通模式:默認(rèn)模式,會(huì)校驗(yàn)所有屬性,然后返回所有的驗(yàn)證失敗信息
  • 快速失敗模式:只要有一個(gè)驗(yàn)證失敗就返回

使用快速失敗模式需要通過(guò)HibernateValidateConfiguration以及ValidateFactory創(chuàng)建Validator,并且使用Validator.validate()進(jìn)行手動(dòng)驗(yàn)證。

首先添加一個(gè)生成Validator的類:

@Configuration
public class FailFastValidator<T> {
    private final Validator validator;
    public FailFastValidator()
    {
        validator = Validation
        .byProvider(HibernateValidator.class).configure()
        .failFast(true).buildValidatorFactory()
        .getValidator();
    }
 
    public Set<ConstraintViolation<T>> validate(T user)
    {
        return validator.validate(user);
    }

修改控制層的代碼,通過(guò)@RequiredArgsConstructor注入FailFastValidator<User>,并把原來(lái)的在User上的@Valid去掉,在方法體進(jìn)行手動(dòng)驗(yàn)證:

@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {
    private final TestService service;
    private final FailFastValidator<User> validator;
    @PostMapping("test")
    public String test(@RequestBody User user, BindingResult bindingResult)
    {
        Set<ConstraintViolation<User>> message = validator.validate(user);
        message.forEach(t-> System.out.println(t.getMessage()));
//        if(bindingResult.hasErrors())
//        {
//            bindingResult.getAllErrors().forEach(t->System.out.println(t.getDefaultMessage()));
//            for(ObjectError error:bindingResult.getAllErrors())
//                return error.getDefaultMessage();
//        }
        return service.test(user);
    }
}

測(cè)試(連續(xù)三次校驗(yàn)的結(jié)果):

如果是普通模式(修改.failFast(false)),一次校驗(yàn)便會(huì)連續(xù)輸出三個(gè)信息:

3.3 @Valid與@Validated

@Valid是javax.validation包里面的,而@Validated是org.springframework.validation.annotation里面的,是@Valid的一次封裝,相當(dāng)于是@Valid的增強(qiáng)版,供Spring提供的校驗(yàn)機(jī)制使用,相比起@Valid,@Validated提供了分組以及組序列的功能。下面分別進(jìn)行介紹。

3.4 分組

當(dāng)需要在不同的情況下使用不同的校驗(yàn)方式時(shí),可以使用分組校驗(yàn)。比如在注冊(cè)時(shí)不需要校驗(yàn)id,修改信息時(shí)需要校驗(yàn)id,但是默認(rèn)的校驗(yàn)方式在兩種情況下全部都校驗(yàn),這時(shí)就需要使用分組校驗(yàn)。

下面以不同的組別校驗(yàn)電話號(hào)碼長(zhǎng)度的不同進(jìn)行說(shuō)明,修改User類如下:

@Data
public class User {
    @NotEmpty(message = "電話不能為空")
    @Length(min = 11,max = 11,message = "電話號(hào)碼必須11位",groups = {GroupA.class})
    @Length(min = 12,max = 12,message = "電話號(hào)碼必須12位",groups = {GroupB.class})
    private String phone;
    @NotEmpty(message = "密碼不能為空")
    @Length(min = 6,max = 20,message = "密碼必須為6-20位")
    private String password;
    @NotEmpty(message = "郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    private String email;
 
    public interface GroupA{}
    public interface GroupB{}
}

在@Length中加入了組別,GroupA表示電話需要為11位,GroupB表示電話需要為12位,GroupA/GroupB是User中的兩個(gè)空接口,然后修改控制層:


public String test(@RequestBody @Validated({User.GroupB.class}) User user, BindingResult bindingResult)
{
    if(bindingResult.hasErrors())
    {
        bindingResult.getAllErrors().forEach(t->System.out.println(t.getDefaultMessage()));
        for(ObjectError error:bindingResult.getAllErrors())
            return error.getDefaultMessage();
    }
    return service.test(user);
}

在@Validated中指定為GroupB,電話需要為12位,測(cè)試如下:

3.5 組序列

默認(rèn)情況下,不同組別的約束驗(yàn)證的無(wú)序的,也就是說(shuō),對(duì)于下面的User類:

@Data
public class User {
    @NotEmpty(message = "電話不能為空")
    @Length(min = 11,max = 11,message = "電話號(hào)碼必須11位")
    private String phone;
    @NotEmpty(message = "密碼不能為空")
    @Length(min = 6,max = 20,message = "密碼必須為6-20位")
    private String password;
    @NotEmpty(message = "郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    private String email;
}

每次進(jìn)行校驗(yàn)的順序不同,三次測(cè)試結(jié)果如下:

有些時(shí)候順序并不重要,而有些時(shí)候順序很重要,比如:

  • 第二個(gè)組中的約束驗(yàn)證依賴于一個(gè)穩(wěn)定狀態(tài)運(yùn)行,而這個(gè)穩(wěn)定狀態(tài)由第一個(gè)組來(lái)進(jìn)行驗(yàn)證
  • 某個(gè)組的驗(yàn)證比較耗時(shí),CPU和內(nèi)存的使用率相對(duì)較大,最優(yōu)的選擇是將其放在最后進(jìn)行驗(yàn)證

因此在進(jìn)行組驗(yàn)證的時(shí)候需要提供一種有序的驗(yàn)證方式,一個(gè)組可以定義為其他組的序列,這樣就可以固定每次驗(yàn)證的順序而不是隨機(jī)順序,另外如果驗(yàn)證組序列中,前面的組驗(yàn)證失敗,則后面的組不會(huì)驗(yàn)證。

例子如下,首先修改User類并定義組序列:

@Data
public class User {
    @NotEmpty(message = "電話不能為空",groups = {First.class})
    @Length(min = 11,max = 11,message = "電話號(hào)碼必須11位",groups = {Second.class})
    private String phone;
    @NotEmpty(message = "密碼不能為空",groups = {First.class})
    @Length(min = 6,max = 20,message = "密碼必須為6-20位",groups = {Second.class})
    private String password;
    @NotEmpty(message = "郵箱不能為空",groups = {First.class})
    @Email(message = "郵箱格式不正確",groups = {Second.class})
    private String email;
 
    public interface First{}
    public interface Second{}
    @GroupSequence({First.class,Second.class})
    public interface Group{}
}

定義了兩個(gè)空接口First和Second表示順序,同時(shí)在Group中使用@GroupSequence指定了順序。

接著修改控制層,在@Validated中定義組:

這樣就能按照固定的順序進(jìn)行參數(shù)校驗(yàn)了。

3.6 自定義校驗(yàn)

盡管Hibernate Validator中的注解適用情況很廣了,但是有時(shí)候需要特定的校驗(yàn)規(guī)則,比如密碼強(qiáng)度,人為判定弱密碼還是強(qiáng)密碼。也就是說(shuō),此時(shí)需要添加自定義校驗(yàn)的方式,有兩種處理方法:

  • 自定義注解
  • 自定義Validator

首先來(lái)看一下自定義注解的方法。

3.6.1 自定義注解

這里添加一個(gè)判定弱密碼的注解WeakPassword:

@Documented
@Constraint(validatedBy = WeakPasswordValidator.class)
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WeakPassword{
    String message() default "請(qǐng)使用更加強(qiáng)壯的密碼";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

同時(shí)添加一個(gè)實(shí)現(xiàn)了ConstraintValidator<A,T>的WeakPasswordValidator,當(dāng)密碼長(zhǎng)度大于10位時(shí)才符合條件,否則返回false表示校驗(yàn)不通過(guò):


public class WeakPasswordValidator implements ConstraintValidator<WeakPassword,String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        return s.length() > 10;
    }
    @Override
    public void initialize(WeakPassword constraintAnnotation) {}
}

接著可以修改User如下,在對(duì)應(yīng)的字段加上自定義注解@WeakPassword:


@Data
public class User {
    //...
    @WeakPassword(groups = {Second.class})
    private String password;
    //...
}

測(cè)試如下:

3.6.2 自定義Validator

除了自定義注解之外,還可以自定義Validator來(lái)實(shí)現(xiàn)自定義的參數(shù)校驗(yàn),需要實(shí)現(xiàn)Validator接口:

@Component
public class WeakPasswordValidator implements Validator{
    @Override
    public boolean supports(Class<?> aClass) {
        return User.class.equals(aClass);
    }
 
    @Override
    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors,"password","password.empty");
        User user = (User)o;
        if(user.getPassword().length() <= 10)
            errors.rejectValue("password","Password is not strong enough!");
    }
}

實(shí)現(xiàn)其中的supports以及validate:

  • support:可以驗(yàn)證該類是否是某個(gè)類的實(shí)例
  • validate:當(dāng)supports返回true后,驗(yàn)證給定對(duì)象o,當(dāng)出現(xiàn)錯(cuò)誤時(shí),向errors注冊(cè)錯(cuò)誤

ValidationUtils.rejectIfEmpty校驗(yàn)當(dāng)對(duì)象o中某個(gè)字段屬性為空時(shí),向其中的errors注冊(cè)錯(cuò)誤,注意并不會(huì)中斷語(yǔ)句的運(yùn)行,也就是即使password為空,user.getPassword()還是會(huì)運(yùn)行,這時(shí)會(huì)拋出空指針異常。下面的errors.rejectValue同樣道理,并不會(huì)中斷語(yǔ)句的運(yùn)行,只是注冊(cè)了錯(cuò)誤信息,中斷的話需要手動(dòng)拋出異常。

修改控制層中的返回值,改為getCode():


if(bindingResult.hasErrors())
{
    bindingResult.getAllErrors().forEach(t-> System.out.println(t.getCode()));
    for(ObjectError error:bindingResult.getAllErrors())
        return error.getCode();
}
return service.test(user);

測(cè)試:

4 異常處理

到這里參數(shù)校驗(yàn)就完成了,下一步是處理異常。

如果將參數(shù)校驗(yàn)中的BindingResult去掉,就會(huì)將整個(gè)后端異常返回給前端:


//public String test(@RequestBody @Validated({User.Group.class}) User user, BindingResult bindingResult)
public String test(@RequestBody @Validated({User.Group.class}) User user)
復(fù)制代碼

這樣雖然后端是方便了,不需要每一個(gè)接口都加上BindingResult,但是前端不好處理,整個(gè)異常都返回了,因此后端需要捕捉這些異常,但是,不能手動(dòng)去捕捉每一個(gè),這樣還不如之前使用BindingResult,這種情況下就需要用到全局的異常處理。

4.1 基本使用

處理全局異常的步驟如下:

  • 創(chuàng)建全局異常處理的類:加上@ControllerAdvice/@RestControllerAdvice注解(取決于控制層用的是@Controller/@RestController,@Controller可以跳轉(zhuǎn)到相應(yīng)頁(yè)面,返回JSON等加上@ResponseBody即可,而@RestController相當(dāng)于@Controller+@ResponseBody,返回JSON無(wú)需加上@ResponseBody,但是視圖解析器無(wú)法解析jsp以及html頁(yè)面)
  • 創(chuàng)建異常處理方法:加上@ExceptionHandler指定想要處理的異常類型
  • 處理異常:在對(duì)應(yīng)的處理異常方法中處理異常

這里增加一個(gè)全局異常處理類GlobalExceptionHandler:


@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return error.getDefaultMessage();
    }
}

首先加上@RestControllerAdvice,并在異常處理方法上加上@ExceptionHandler。

接著修改控制層,去掉其中的BindingResult:


@PostMapping("test")
public String test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}

然后就可以進(jìn)行測(cè)試了:

全局異常處理相比起原來(lái)的每一個(gè)接口都加上BindingResult方便很多,而且可以集中處理所有異常。

4.2 自定義異常

很多時(shí)候都會(huì)用到自定義異常,這里新增一個(gè)測(cè)試異常TestException:


@Data
public class TestException extends RuntimeException{
    private int code;
    private String msg;
 
    public TestException(int code,String msg)
    {
        super(msg);
        this.code = code;
        this.msg = msg;
    }
 
    public TestException()
    {
        this(111,"測(cè)試異常");
    }
 
    public TestException(String msg)
    {
        this(111,msg);
    }
}

接著在剛才的全局異常處理類中添加一個(gè)處理該異常的方法:


@ExceptionHandler(TestException.class)
public String testExceptionHandler(TestException e)
{
    return e.getMsg();
}

在控制層進(jìn)行測(cè)試:


@PostMapping("test")
public String test(@RequestBody @Validated({User.Group.class}) User user)
{
    throw new TestException("出現(xiàn)異常");
//        return service.test(user);
}

結(jié)果如下:

5 數(shù)據(jù)響應(yīng)

在處理好了參數(shù)校驗(yàn)以及異常處理之后,下一步就是要設(shè)置統(tǒng)一的規(guī)范化的響應(yīng)數(shù)據(jù),一般來(lái)說(shuō)無(wú)論響應(yīng)成功還是失敗都會(huì)有一個(gè)狀態(tài)碼,響應(yīng)成功還會(huì)攜帶響應(yīng)數(shù)據(jù),響應(yīng)失敗則攜帶相應(yīng)的失敗信息,因此,第一步是設(shè)計(jì)一個(gè)統(tǒng)一的響應(yīng)體。

5.1 統(tǒng)一響應(yīng)體

統(tǒng)一響應(yīng)體需要?jiǎng)?chuàng)建響應(yīng)體類,一般來(lái)說(shuō),響應(yīng)體需要包含:

  • 狀態(tài)碼:String/int
  • 響應(yīng)信息:String
  • 響應(yīng)數(shù)據(jù):Object/T(泛型)

這里簡(jiǎn)單的定義一個(gè)統(tǒng)一響應(yīng)體Result:


@Data
@AllArgsConstructor
public class Result<T> {
    private String code;
    private String message;
    private T data;
}

接著修改全局異常處理類:

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(error.getCode(),"參數(shù)校驗(yàn)失敗",error.getDefaultMessage());
    }
 
    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return new Result<>(e.getCode(),"失敗",e.getMsg());
    }

使用Result<String>封裝返回值,測(cè)試如下:

可以看到返回了一個(gè)比較友好的信息,無(wú)論是響應(yīng)成功還是響應(yīng)失敗都會(huì)返回同一個(gè)響應(yīng)體,當(dāng)需要返回具體的用戶數(shù)據(jù)時(shí),可以修改控制層接口直接返回Result<User>:


@PostMapping("test")
public Result<User> test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}

測(cè)試:

5.2 響應(yīng)碼枚舉

通常來(lái)說(shuō)可以把響應(yīng)碼做成枚舉類:

@Getter
public enum ResultCode {
    SUCCESS("111","成功"),FAILED("222","失敗");
 
    private final String code;
    private final String message;
    ResultCode(String code,String message)
    {
        this.code = code;
        this.message = message;
    }
}

枚舉類封裝了狀態(tài)碼以及信息,這樣在返回結(jié)果時(shí),只需要傳入對(duì)應(yīng)的枚舉值以及數(shù)據(jù)即可:


@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(ResultCode.FAILED,error.getDefaultMessage());
    }
 
    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return new Result<>(ResultCode.FAILED,e.getMsg());
    }
}

5.3 全局包裝響應(yīng)體

統(tǒng)一響應(yīng)體是個(gè)很好的想法,但是還可以再深入一步去優(yōu)化,因?yàn)槊看畏祷刂岸夹枰獙?duì)響應(yīng)體進(jìn)行包裝,雖然只是一行代碼但是每個(gè)接口都需要包裝一下,這是個(gè)很麻煩的操作,為了更進(jìn)一步“偷懶”,可以選擇實(shí)現(xiàn)ResponseBodyAdvice<T>來(lái)進(jìn)行全局的響應(yīng)體包裝。

修改原來(lái)的全局異常處理類如下:


@RestControllerAdvice
public class GlobalExceptionHandler implements ResponseBodyAdvice<Object> {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e)
    {
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        return new Result<>(ResultCode.FAILED,error.getDefaultMessage());
    }
 
    @ExceptionHandler(TestException.class)
    public Result<String> testExceptionHandler(TestException e)
    {
        return new Result<>(ResultCode.FAILED,e.getMsg());
    }
 
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return !methodParameter.getParameterType().equals(Result.class);
    }
 
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        return new Result<>(o);
    }
}

實(shí)現(xiàn)了ResponseBodyAdvice<Object>:

  • supports方法:判斷是否支持控制器返回方法類型,可以通過(guò)supports判斷哪些類型需要包裝,哪些不需要包裝直接返回
  • beforeBodyWrite方法:當(dāng)supports返回true后,對(duì)數(shù)據(jù)進(jìn)行包裝,這樣在返回?cái)?shù)據(jù)時(shí)就無(wú)需使用Result<User>手動(dòng)包裝,而是直接返回User即可

接著修改控制層,直接返回實(shí)體類User而不是響應(yīng)體包裝類Result<User>:


@PostMapping("test")
public User test(@RequestBody @Validated({User.Group.class}) User user)
{
    return service.test(user);
}

測(cè)試輸出如下:

5.4 繞過(guò)全局包裝

雖然按照上面的方式可以使后端的數(shù)據(jù)全部按照統(tǒng)一的形式返回給前端,但是有時(shí)候并不是返回給前端而是返回給其他第三方,這時(shí)候不需要code以及msg等信息,只是需要數(shù)據(jù),這樣的話,可以提供一個(gè)在方法上的注解來(lái)繞過(guò)全局的響應(yīng)體包裝。

比如添加一個(gè)@NotResponseBody注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface NotResponseBody {
}

接著需要在處理全局包裝的類中,在supports中進(jìn)行判斷:


@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
    return !(
        methodParameter.getParameterType().equals(Result.class) 
        ||
        methodParameter.hasMethodAnnotation(NotResponseBody.class)
    );
}

最后修改控制層,在需要繞過(guò)的方法上添加自定義注解@NotResponseBody即可:

@PostMapping("test")
@NotResponseBody
public User test(@RequestBody @Validated({User.Group.class}) User user)

6 總結(jié)

7 源碼

直接clone下來(lái)使用IDEA打開即可,每一次優(yōu)化都做了一次提交,可以看到優(yōu)化的過(guò)程,喜歡的話記得點(diǎn)個(gè)贊加關(guān)注哦


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容