前言

一个完整的后端请求由 4 部分组成:

  • 接口地址(也就是 URL 地址)
  • 请求方式(一般就是 get、set,当然还有 put、delete)
  • 请求数据(request,有 head 跟 body)
  • 响应数据(response)

Controller 主要的工作有以下几项:

  • 接收请求并解析参数
  • 调用 Service 执行具体的业务代码(可能包含参数校验)
  • 捕获业务逻辑异常做出反馈
  • 业务逻辑执行成功做出响应

解决以下 3 个问题:

  • 返回响应数据该如何统一的进行处理
  • 接收到请求,处理业务逻辑时抛出了异常又该如何处理
  • 当接收到请求时,如何优雅的校验参数

统一返回结构

封装Result

package com.wanqi.result;
public interface IResult {
    Integer getCode();
    String getMessage();
}

定义状态码

package com.wanqi.result;
public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源"),
    DATA_CONVERSION(2005, "数据转换异常");
    private Integer code;
    private String message;
    ResultEnum(int code, String message) {
        this.code=code;
        this.message=message;
    }
    @Override
    public Integer getCode() {
        return code;
    }
    @Override
    public String getMessage() {
        return message;
    }
}

Result包装类

package com.wanqi.result;
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
    public T getData() {
        return data;
    }
    public void setData(T data) {
        this.data = data;
    }
    public Result() {
    }
    public Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
    }
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
    }
    public static Result<?> failed() {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
    }
    public static Result<?> failed(String message) {
        return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
    }
    public static Result<?> failed(Integer code,String message) {
        return new Result<>(code, message, null);
    }
    public static Result<?> failed(IResult errorResult) {
        return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
    }
    public static <T> Result<T> instance(Integer code, String message, T data) {
        Result<T> result = new Result<>();
        result.setCode(code);
        result.setMessage(message);
        result.setData(data);
        return result;
    }
}

统一包装处理

package com.wanqi.advice;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wanqi.exception.APIException;
import com.wanqi.result.Result;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
// 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages = "com.wanqi.controller")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // response是Result类型不进行包装
        return !returnType.getParameterType().isAssignableFrom(Result.class);
    }
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
       if(returnType.getGenericParameterType().equals(String.class)){
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(Result.success(body));
            } catch (JsonProcessingException e) {
                throw new APIException(e.getMessage());
            }
        }
        return Result.success(body);
    }
}

Controller层

package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@RestController
public class TestController {
    @PostMapping("/test")
    public TestDTO test(@RequestBody TestDTO testDTO) {
        return testDTO;
    }
    @PostMapping("/test1")
    public String test1(@RequestBody TestDTO testDTO) {
        return testDTO.getName();
    }
}

如何写出优雅的Controller层代码

如何写出优雅的Controller层代码

不统一处理返回

自定义注解

package com.wanqi.advice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {
}

在增强过滤方法上过滤包含这个注解的方法

@RestControllerAdvice(basePackages = "com.wanqi.controller")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // response是Result类型,或者注释了NotControllerResponseAdvice都不进行包装
        return !(returnType.getParameterType().isAssignableFrom(Result.class)
                || returnType.hasMethodAnnotation(NotControllerResponseAdvice.class));
    }
.....

controller

  • 在不需要统一处理的方法上使用@NotControllerResponseAdvice注解即可
package com.wanqi.controller;
import com.wanqi.advice.NotControllerResponseAdvice;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import com.wanqi.result.Result;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
    @GetMapping("/health1")
    public Result health1() {
        return Result.success("dddd");
    }
}

如何写出优雅的Controller层代码如何写出优雅的Controller层代码

自定义异常与统一拦截异常

自定义异常

package com.wanqi.exception;
import com.wanqi.result.ResultEnum;
//自定义异常
public class APIException extends RuntimeException {
    private int code;
    private String msg;
    public int getCode() {
        return code;
    }
    public String getMsg() {
        return msg;
    }
    public APIException(String message) {
        super(message);
        this.code = ResultEnum.DATA_CONVERSION.getCode();
        this.msg = ResultEnum.DATA_CONVERSION.getMessage();
    }
}

统一拦截异常

package com.wanqi.advice;
import com.wanqi.exception.APIException;
import com.wanqi.result.IResult;
import com.wanqi.result.Result;
import com.wanqi.result.ResultEnum;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
//统一拦截异常
@RestControllerAdvice(basePackages = "com.wanqi")
public class ControllerExceptionAdvice {
    /**
     * 捕获 {@code APIException} 异常
     */
    @ExceptionHandler({APIException.class})
    public Result<?> handleBusinessException(APIException ex) {
        return Result.failed(ex.getMessage());
    }
    /**
     * {@code @RequestBody} 参数校验不通过时抛出的异常处理
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
        if (StringUtils.hasText(msg)) {
            return Result.failed(new IResult() {
                @Override
                public Integer getCode() {
                    return ResultEnum.VALIDATE_FAILED.getCode();
                }
                @Override
                public String getMessage() {
                    return msg;
                }
            });
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * {@code @PathVariable} 和 {@code @RequestParam} 参数校验不通过时抛出的异常处理
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        if (StringUtils.hasText(ex.getMessage())) {
            return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED);
    }
    /**
     * 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
     */
    @ExceptionHandler({Exception.class})
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

参数校验

  • 使用hibernate-validator,@Validated注解参数校验
        <!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-validator -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.2.5.Final</version>
        </dependency>

@RequestBody 参数校验

  • Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。
  • 对 @RequestBody 参数进行校验需要在对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。

UserDTO

package com.wanqi.pojo;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
public class UserDTO {
    private Long userId;
    @NotNull
    @Length(min = 2,max = 10)
    private String userName;
    @NotNull
    @Length(min = 6,max = 20)
    private String account;
    @NotNull
    @Length(min = 8,max = 20)
    private String password;
    public Long getUserId() {
        return userId;
    }
    public void setUserId(Long userId) {
        this.userId = userId;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getAccount() {
        return account;
    }
    public void setAccount(String account) {
        this.account = account;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    @Override
    public String toString() {
        return "UserDTO{" +
                "userId=" + userId +
                ", userName='" + userName + '\'' +
                ", account='" + account + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

controller

package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
	// 使用@Valid或者@Validated
    @PostMapping("/user")
    public UserDTO user(@RequestBody @Validated UserDTO userDTO) {
      return userDTO;
    }
}

如何写出优雅的Controller层代码

@PathVariable 和 @RequestParam 参数校验

  • Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。
  • 对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。

controller

package com.wanqi.controller;
import com.wanqi.pojo.TestDTO;
import com.wanqi.pojo.UserDTO;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
@Validated
@RestController
public class TestController {
    @GetMapping("/{num}")
    public Integer detail(@PathVariable("num") @Min(value = 1 ,message = "不能小于1") @Max(20) Integer num) {
        return num * num;
    }
    @GetMapping("/getByEmail")
    public String getByAccount(@RequestParam @NotBlank @Email String email) {
        return email;
    }
}

如何写出优雅的Controller层代码如何写出优雅的Controller层代码

自定义校验规则

自定义校验规则需要做两件事情:

  • 自定义注解类,定义错误信息和一些其他需要的内容
  • 注解校验器,定义判定规则

自定义注解类

package com.wanqi.validator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    /**
     * 是否允许为空
     */
    boolean required() default true;
    /**
     * 校验不通过返回的提示信息
     */
    String message() default "不是一个手机号码格式";
    /**
     * Constraint要求的属性,用于分组校验和扩展,留空就好
     */
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

注解校验器,实现ConstraintValidator接口

package com.wanqi.validator;
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
    private boolean required = false;
    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 验证手机号
    /**
     * 在验证开始前调用注解里的方法,从而获取到一些注解里的参数
     *
     * @param constraintAnnotation annotation instance for a given constraint declaration
     */
    @Override
    public void initialize(Mobile constraintAnnotation) {
        this.required = constraintAnnotation.required();
    }
    /**
     * 判断参数是否合法
     *
     * @param value   object to validate
     * @param context context in which the constraint is evaluated
     */
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
        if (this.required) {
            // 验证
            return isMobile(value);
        }
        if (StringUtils.hasText(value)) {
            // 验证
            return isMobile(value);
        }
        return true;
    }
    private boolean isMobile(final CharSequence str) {
        Matcher m = pattern.matcher(str);
        return m.matches();
    }
}

使用

package com.wanqi.controller;
import com.wanqi.validator.Mobile;
import org.hibernate.validator.constraints.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Validated
@RestController
public class TestController {
    @GetMapping("/phone")
    public String phone(@RequestParam @Mobile String phone) {
        return phone;
    }
}

如何写出优雅的Controller层代码

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。