SpringBoot 規(guī)范接口開發(fā)流程

筆記內(nèi)容以及代碼是我閱讀了知乎作者RudeCrab的《【項(xiàng)目實(shí)踐】SpringBoot三招組合拳,手把手教你打出優(yōu)雅的后端接口》這篇文章后學(xué)習(xí)和總結(jié)的

原文鏈接: https://zhuanlan.zhihu.com/p/340620501

階段一:Validator + BindResult進(jìn)行校驗(yàn)

1. 引入validation依賴

Validation Starter no longer included in web starters
As of #19550, Web and WebFlux starters do not* depend on the validation starter by default anymore. If your application is using validation features, for example you find that javax.validation. imports are not being resolved, you'll need to add the starter yourself. For Maven builds, you can do that with the following:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

2. User實(shí)體類用Validator添加校驗(yàn)規(guī)則

  • Validator就是springboot的validation中的一些用于限制屬性的注解
/**
 * 用戶實(shí)體類
 */
@Data
public class User {
    @NotNull(message = "用戶id不能為空")
    private Long id;

    @NotNull(message = "用戶賬號(hào)不能為空")
    @Size(min = 6, max = 11, message = "賬號(hào)長(zhǎng)度必須是6-11個(gè)字符")
    private String account;

    @NotNull(message = "用戶密碼不能為空")
    @Size(min = 6, max = 16, message = "賬號(hào)長(zhǎng)度必須是6-16個(gè)字符")
    private String password;

    @NotNull(message = "用戶郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    private String email;
}

3. 給需要校驗(yàn)的參數(shù)加上@Valid注解

  • 在Controller中的入?yún)⒅?,有要校?yàn)的實(shí)體類就給該參數(shù)加上@Valid注解即可使實(shí)體類中用@NotNull等validator相關(guān)注解生效
  • 如果有參數(shù)校驗(yàn)失敗,會(huì)將錯(cuò)誤信息封裝成對(duì)象組裝在BindingResult里,即實(shí)體類中和validation相關(guān)的注解中的message屬性會(huì)封裝在這里
@RestController
@RequestMapping("user")
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/addUser")
    public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
        // 如果有參數(shù)校驗(yàn)失敗,會(huì)將錯(cuò)誤信息封裝成對(duì)象組裝在BindingResult里
        for (ObjectError error : bindingResult.getAllErrors()) {
            return error.getDefaultMessage();
        }

        return userService.addUser(user);
    }
}
  • 這樣子的話校驗(yàn)規(guī)則就不用在service層去做了,將校驗(yàn)和業(yè)務(wù)邏輯處理分離,service只用單純地處理業(yè)務(wù),不需要擔(dān)心字段的校驗(yàn)

UserService

@Service
public class UserServiceImpl implements UserService {
    @Override
    public String addUser(User user) {
        return "success";
    }
}

這種寫法每次都要在controller層傳入BindingResult,很不方便,接下來用自動(dòng)拋出異常的方式去進(jìn)一步優(yōu)化

階段二:Validator + 自動(dòng)拋出異常

1. 把BindingResult去除

@PostMapping("/add-user")
public String addUser(@RequestBody @Valid User user) {
    return userService.addUser(user);
}

這時(shí)候后端已經(jīng)引發(fā)了MethodArgumentNotValidException異常,并且前端收到的數(shù)據(jù)如下

{
    "timestamp": "2021-09-03T13:35:31.068+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "...",
    "message": "Validation failed for object='user'. Error count: 3",
    "errors": [
        {
            "codes": [
                "Email.user.email",
                "Email.email",
                "Email.java.lang.String",
                "Email"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.email",
                        "email"
                    ],
                    "arguments": null,
                    "defaultMessage": "email",
                    "code": "email"
                },
                [],
                {
                    "defaultMessage": ".*",
                    "codes": [
                        ".*"
                    ],
                    "arguments": null
                }
            ],
            "defaultMessage": "郵箱格式不正確",
            "objectName": "user",
            "field": "email",
            "rejectedValue": "975036719qq.com",
            "bindingFailure": false,
            "code": "Email"
        },
        {
            "codes": [
                "NotNull.user.password",
                "NotNull.password",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.password",
                        "password"
                    ],
                    "arguments": null,
                    "defaultMessage": "password",
                    "code": "password"
                }
            ],
            "defaultMessage": "用戶密碼不能為空",
            "objectName": "user",
            "field": "password",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        },
        {
            "codes": [
                "NotNull.user.account",
                "NotNull.account",
                "NotNull.java.lang.String",
                "NotNull"
            ],
            "arguments": [
                {
                    "codes": [
                        "user.account",
                        "account"
                    ],
                    "arguments": null,
                    "defaultMessage": "account",
                    "code": "account"
                }
            ],
            "defaultMessage": "用戶賬號(hào)不能為空",
            "objectName": "user",
            "field": "account",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotNull"
        }
    ],
    "path": "/user/add-user"
}

后端直接將整個(gè)錯(cuò)誤對(duì)象相關(guān)信息都響應(yīng)給前端了,這是因?yàn)殡m然引發(fā)了異常,但是我們沒有去對(duì)其進(jìn)行處理,所以走了SpringBoot默認(rèn)的異常處理流程,現(xiàn)在開始進(jìn)行全局異常處理

2. 全局異常處理

在類上加上@ControllerAdvice或@RestControllerAdvice注解,這個(gè)類就配置成全局處理類了

在類中新建方法,在方法上加上@ExceptionHandler注解并指定你想處理的異常類型,接著在方法內(nèi)編寫對(duì)該異常的操作邏輯,就完成了對(duì)該異常的全局處理

package com.plasticine.demo.config;

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public String MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        // 從異常中拿到 ObjectError 對(duì)象
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        // 提取錯(cuò)誤信息返回
        return error.getDefaultMessage();
    }
}

3. 自定義異常

自定義異常的好處

  1. 自定義異??梢詳y帶更多的信息,不像這樣只能攜帶一個(gè)字符串。
  2. 項(xiàng)目開發(fā)中經(jīng)常是很多人負(fù)責(zé)不同的模塊,使用自定義異??梢越y(tǒng)一了對(duì)外異常展示的方式。
  3. 自定義異常語義更加清晰明了,一看就知道是項(xiàng)目中手動(dòng)拋出的異常。
  • 自定義異常 -- APIException
package com.plasticine.demo.exception;

@Getter // 不需要 setter,需要拋出異常的時(shí)候都是直接 new 一個(gè)調(diào)用構(gòu)造方法即可
public class APIException extends RuntimeException {
    private final int errcode;
    private final String errmsg;

    public APIException() {
        this(1001, "API Error");
    }

    public APIException(String errmsg) {
        this(1001, errmsg);
    }

    public APIException(int errcode, String errmsg) {
        this.errcode = errcode;
        this.errmsg = errmsg;
    }
}

還能在全局異常處理中處理Exception異常,這樣無論遇到什么Exception都能夠統(tǒng)一返回給前端,不過這種一般建議是在項(xiàng)目上線之前才這樣做,開發(fā)的時(shí)候?yàn)榱朔奖阏{(diào)試還是不太建議這樣做

4. 數(shù)據(jù)統(tǒng)一響應(yīng)

  • 前面的處理異常的結(jié)果都是返回的錯(cuò)誤信息,而沒有返回別的比如errcode
  • 為了更加統(tǒng)一,應(yīng)當(dāng)讓異常和正常api數(shù)據(jù)請(qǐng)求的結(jié)果格式相同,都應(yīng)該包括errcode、errmsgdata,因此需要自定義一個(gè)統(tǒng)一的響應(yīng)體
  1. 自定義統(tǒng)一響應(yīng)體(VO,即View Object,是后端向模板引擎渲染數(shù)據(jù)時(shí)傳輸?shù)膶?duì)象,也可以是返回JSON數(shù)據(jù)的統(tǒng)一對(duì)象)

    package com.plasticine.demo.vo;
    
    @Getter
    public class ResultVO<T> {
        private final int errcode;    // api 響應(yīng)狀態(tài)碼
        private final String errmsg;  // api 請(qǐng)求信息
        private final T data;         // api 返回的數(shù)據(jù)
    
        public ResultVO(T data) {
            this(ResultCode.SUCCESS, data);
        }
    
        public ResultVO(int errcode, String errmsg, T data) {
            this.errcode = errcode;
            this.errmsg = errmsg;
            this.data = data;
        }
    
        public ResultVO(ResultCode resultCode, T data) {
            this.errcode = resultCode.getErrcode();
            this.errmsg = resultCode.getErrmsg();
            this.data = data;
        }
    }
    
  2. 修改全局異常處理的返回值類型為ResultVO

    package com.plasticine.demo.config;
    
    @RestControllerAdvice
    public class ExceptionControllerAdvice {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
            // 從異常中拿到 ObjectError 對(duì)象
            ObjectError error = e.getBindingResult().getAllErrors().get(0);
            // 提取錯(cuò)誤信息返回
            return new ResultVO<String>(1001, "參數(shù)校驗(yàn)失敗", error.getDefaultMessage());
        }
    
        /**
         * 統(tǒng)一全局處理自定義異常 APIException
         *
         * @param e APIException
         * @return 處理結(jié)果
         */
        @ExceptionHandler(APIException.class)
        public ResultVO<String> APIExceptionHandler(APIException e) {
            return new ResultVO<>(e.getErrcode(), "api請(qǐng)求失敗", e.getErrmsg());
        }
    }
    

    現(xiàn)在遇到異常時(shí)返回的就會(huì)是JSON數(shù)據(jù)格式的字符串了

    {
        "errcode": 1001,
        "errmsg": "參數(shù)校驗(yàn)失敗",
        "data": "郵箱格式不正確"
    }
    
  3. 修改Controller和Service返回值類型

    • service

      public interface UserService {
      
          /**
           * 添加 user
           *
           * @param user User 實(shí)體類對(duì)象
           * @return 成功返回 success,失敗返回錯(cuò)誤信息
           */
          ResultVO<User> addUser(User user);
      }
      
      @Service
      public class UserServiceImpl implements UserService {
          @Override
          public ResultVO<User> addUser(User user) {
              return new ResultVO<User>(user);
          }
      }
      
    • controller

      @RestController
      @RequestMapping("user")
      public class UserController {
      
          private UserService userService;
      
          @Autowired
          public void setUserService(UserService userService) {
              this.userService = userService;
          }
      
          @PostMapping("/add-user")
          public ResultVO<User> addUser(@RequestBody @Valid User user) {
              return userService.addUser(user);
          }
      }
      

    現(xiàn)在請(qǐng)求成功的api返回的數(shù)據(jù)也可以是自定義的了

    • 請(qǐng)求體

      {
          "id": 666,
          "account": "plasticine",
          "password": "abc123",
          "email": "975036719@qq.com"
      }
      
    • 響應(yīng)體

      {
          "errcode": 1000,
          "errmsg": "success",
          "data": {
              "id": 666,
              "account": "plasticine",
              "password": "abc123",
              "email": "975036719@qq.com"
          }
      }
      
  4. 響應(yīng)碼枚舉

    響應(yīng)碼都是純數(shù)字,不好理解errcode的意思,使用枚舉可以解決這一問題

    /**
     * errcode 枚舉
     */
    @Getter
    public enum ResultCode {
    
        SUCCESS(1000, "操作成功"),
        FAILED(1001, "操作失敗"),
        VALIDATE_FAILED(1002, "參數(shù)校驗(yàn)失敗"),
        ERROR(5000, "未知錯(cuò)誤");
    
        private int errcode;
        private String errmsg;
    
        ResultCode(int errcode, String errmsg) {
            this.errcode = errcode;
            this.errmsg = errmsg;
        }
    }
    

    修改全局異常處理的返回值

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        // 從異常中拿到 ObjectError 對(duì)象
        ObjectError error = e.getBindingResult().getAllErrors().get(0);
        // 提取錯(cuò)誤信息返回
        return new ResultVO<>(ResultCode.VALIDATE_FAILED, error.getDefaultMessage());
    }
    
    @ExceptionHandler(APIException.class)
    public ResultVO<String> APIExceptionHandler(APIException e) {
        return new ResultVO<>(ResultCode.FAILED, e.getErrmsg());
    }
    

5. 全局處理響應(yīng)數(shù)據(jù)

  1. 先創(chuàng)建一個(gè)類加上注解使其成為全局處理類
  2. 繼承ResponseBodyAdvice接口重寫其中的方法
package com.plasticine.demo.config;

@RestControllerAdvice(basePackages = {"com.plasticine.demo.controller"})
public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        /*
            返回 true 代表要走 beforeBodyWrite 方法去對(duì)返回結(jié)果進(jìn)行額外處理
            反之,false 代表不走 beforeBodyWrite 方法,直接返回 returnType
            如果返回的類型是 ResultVO 則不需要進(jìn)行額外操作,直接返回即可
         */
        return !returnType.getParameterType().equals(ResultVO.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

        // String 不能直接由 ResultVO 封裝,要經(jīng)過一些處理先
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(new ResultVO<>(body));
            } catch (JsonProcessingException e) {
                throw new APIException("返回 String 類型時(shí)封裝出錯(cuò)");
            }
        }

        // 將原本的數(shù)據(jù)經(jīng) ResultVO 統(tǒng)一響應(yīng)體封裝后再返回
        return new ResultVO<>(body);
    }
}

寫一個(gè)新的api,返回的類型不再是ResultVo,而是直接返回實(shí)體類對(duì)象,這樣就會(huì)走beforeBodyWrite方法去包裝,然后真正返回給前端的還是ResultVO,這樣做的目的就是可以省去我們自己手動(dòng)封裝數(shù)據(jù)到ResultVO的過程

  • 對(duì)比返回ResultVO和直接返回User實(shí)體類對(duì)象

    /**
     * 返回的類型是 ResultVO 類型,不會(huì)經(jīng)過 ResponseControllerAdvice 的 beforeBodyWrite 處理
     *
     * @param user 要添加的用戶
     * @return 經(jīng)過 ResultVO 封裝的統(tǒng)一響應(yīng)體
     */
    @PostMapping("/add-user")
    public ResultVO<User> addUser(@RequestBody @Valid User user) {
        return userService.addUser(user);
    }
    
    /**
     * 返回的類型是 User 不是 ResultVO 類型,會(huì)經(jīng)過 ResponseControllerAdvice 的 beforeBodyWrite 處理
     * 將 user 封裝進(jìn) ResultVO 的 data 屬性中返回
     *
     * @return user 封裝到 ResultVO 后的統(tǒng)一響應(yīng)體
     */
    @GetMapping("/get-user")
    public User getUser() {
        return new User(666L, "plasticine", "abc123", "975036719@qq.com");
    }
    
  • 如果beforeBodyWrite()中不處理返回類型是String的情況會(huì)怎樣呢?

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return new ResultVO<>(body);
    }
    

    會(huì)遇到ClassCastException異常

    java.lang.ClassCastException: com.plasticine.demo.vo.ResultVO cannot be cast to java.lang.String
    

階段三:自定義注解實(shí)現(xiàn)繞過數(shù)據(jù)統(tǒng)一響應(yīng)

前面的方案中,正常數(shù)據(jù)返回的狀態(tài)碼都是統(tǒng)一的,封裝在enum狀態(tài)碼枚舉中,這沒什么問題

可是在異常的處理中,字段校驗(yàn)錯(cuò)誤的狀態(tài)碼統(tǒng)一都是1002,如果想給每個(gè)字段自定義狀態(tài)碼以及錯(cuò)誤信息時(shí),就做不到了,這時(shí)就可以在要校驗(yàn)的字段上加上我們自定義的注解,然后通過反射來獲取注解從而對(duì)每個(gè)字段的校驗(yàn)錯(cuò)誤實(shí)現(xiàn)自定義錯(cuò)誤碼的功能

1. 自定義注解

package com.plasticine.demo.annotation;

/**
 * 自定義參數(shù)校驗(yàn)錯(cuò)誤碼以及錯(cuò)誤信息注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})        // 作用在類的屬性上
public @interface ExceptionCode {
    // 響應(yīng)碼 errcode
    int errcode() default 100000;
    // 響應(yīng)信息 errmsg
    String errmsg() default "參數(shù)校驗(yàn)錯(cuò)誤";
}

2. 給實(shí)體類中要自定義校驗(yàn)響應(yīng)體的字段加上注解

package com.plasticine.demo.pojo;

/**
 * 用戶實(shí)體類
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    @NotNull(message = "用戶id不能為空")
    private Long id;

    @NotNull(message = "用戶賬號(hào)不能為空")
    @Size(min = 6, max = 11, message = "賬號(hào)長(zhǎng)度必須是6-11個(gè)字符")
    @ExceptionCode(errcode = 100001, errmsg = "賬號(hào)不符合校驗(yàn)規(guī)則")
    private String account;

    @NotNull(message = "用戶密碼不能為空")
    @Size(min = 6, max = 16, message = "賬號(hào)長(zhǎng)度必須是6-16個(gè)字符")
    @ExceptionCode(errcode = 100002, errmsg = "密碼不符合校驗(yàn)規(guī)則")
    private String password;

    @NotNull(message = "用戶郵箱不能為空")
    @Email(message = "郵箱格式不正確")
    @ExceptionCode(errcode = 100003, errmsg = "郵箱不符合校驗(yàn)規(guī)則")
    private String email;
}

3. 修改全局異常處理MethodArgumentNotValidException的代碼

package com.plasticine.demo.config;

@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVO<String> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) throws NoSuchFieldException {
        // 從異常對(duì)象中拿到錯(cuò)誤信息
        String defaultMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        // 獲取參數(shù)的 Class 對(duì)象,從而獲取到 Field 對(duì)象,便可以拿到自定義的注解內(nèi)容了
        Class<?> parameterType = e.getParameter().getParameterType();
        // 拿到錯(cuò)誤字段名稱,然后根據(jù)錯(cuò)誤字段名獲取到 Field 對(duì)象
        String fieldName = e.getBindingResult().getFieldError().getField();
        Field field = parameterType.getDeclaredField(fieldName);
        // 獲取 Field 的自定義注解
        ExceptionCode annotation = field.getAnnotation(ExceptionCode.class);
        // 有注解則用注解中自定義的 errcode 和 errmsg 作為統(tǒng)一響應(yīng)體返回
        if (annotation != null) {
            return new ResultVO<>(annotation.errcode(), annotation.errmsg(), defaultMessage);
        }

        // 沒有使用自定義注解的字段則用枚舉中的 VALIDATE_FAILED 進(jìn)行統(tǒng)一返回
        return new ResultVO<>(ResultCode.VALIDATE_FAILED, defaultMessage);
    }
}

效果

上面實(shí)體類中,id字段沒有加上自定義注解,所以會(huì)走統(tǒng)一的VALIDATE_FAILED封裝的響應(yīng)體,而其他字段都加上了自定義注解,所以遇到字段校驗(yàn)出錯(cuò)時(shí)就會(huì)用自定義注解中的errcode和errmsg去封裝返回

  • id為空

    // 請(qǐng)求體
    {
        "account": "plasticine",
        "password": "abc123",
        "email": "975036719@qq.com"
    }
    
    // 響應(yīng)體
    {
        "errcode": 1002,
        "errmsg": "參數(shù)校驗(yàn)失敗",
        "data": "用戶id不能為空"
    }
    
  • account為空

    // 請(qǐng)求體
    {
        "id": 666,
        "password": "abc123",
        "email": "975036719@qq.com"
    }
    
    // 響應(yīng)體
    {
        "errcode": 100001,
        "errmsg": "賬號(hào)不符合校驗(yàn)規(guī)則",
        "data": "用戶賬號(hào)不能為空"
    }
    

4. 繞過數(shù)據(jù)統(tǒng)一響應(yīng)

上面返回的響應(yīng)體都是經(jīng)過ResultVO封裝過的,如果不想用ResultVO封裝,而是自定義返回?cái)?shù)據(jù)的格式,也就是要繞過數(shù)據(jù)統(tǒng)一響應(yīng),那可以給要繞過統(tǒng)一響應(yīng)的方法加上一個(gè)自定義注解,然后在統(tǒng)一處理的部分對(duì)有該注解的方法不進(jìn)行封裝處理

  • 自定義繞過統(tǒng)一響應(yīng)注解

    package com.plasticine.demo.annotation;
    
    /**
     * 繞過統(tǒng)一數(shù)據(jù)響應(yīng)
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface BypassResultVO {
    }
    
  • 修改ResponseControllerAdvice

    package com.plasticine.demo.config;
    
    @RestControllerAdvice(basePackages = {"com.plasticine.demo.controller"})
    public class ResponseControllerAdvice implements ResponseBodyAdvice<Object> {
    
        @Override
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            /*
                返回 true 代表要走 beforeBodyWrite 方法去對(duì)返回結(jié)果進(jìn)行額外處理
                反之,false 代表不走 beforeBodyWrite 方法,直接返回 returnType
                如果返回的類型是 ResultVO 則不需要進(jìn)行額外操作,直接返回即可
             */
            if (returnType.getParameterType().equals(ResultVO.class)) return false;     // 返回類型已經(jīng)是 ResultVO,不需要經(jīng)過額外處理
            if (returnType.hasMethodAnnotation(BypassResultVO.class)) return false;     // 加上了 @BypassResultVO 注解,不需要經(jīng)過額外處理
    
            return true;
        }
    }
    

    即在supports()中加上returnType.hasMethodAnnotation(BypassResultVO.class);即可,也就是說有BypassResultVO這個(gè)注解的方法就不走beforeBodyWrite()方法處理

  • 測(cè)試:給getUser()加上BypassResultVO

    @GetMapping("/get-user")
    @BypassResultVO
    public User getUser() {
        return new User(666L, "plasticine", "abc123", "975036719@qq.com");
    }
    

    未加上@BypassResultVO注解之前的響應(yīng)體:

    {
        "errcode": 1000,
        "errmsg": "操作成功",
        "data": {
            "id": 666,
            "account": "plasticine",
            "password": "abc123",
            "email": "975036719@qq.com"
        }
    }
    

    加上注解后:

    {
        "id": 666,
        "account": "plasticine",
        "password": "abc123",
        "email": "975036719@qq.com"
    }
    
最后編輯于
?著作權(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)容