百木园-与人分享,
就是让自己快乐。

如何写出优雅的Controller层代码

前言

一个完整的后端请求由 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();
    }
}

image.png

image.png

不统一处理返回

自定义注解

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\");
    }
}

image.pngimage.png

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

自定义异常

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;
    }
}

image.png

@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;
    }
}

image.pngimage.png

自定义校验规则

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

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

自定义注解类

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;
    }
}

image.png


来源:https://www.cnblogs.com/wandaren/p/17006196.html
本站部分图文来源于网络,如有侵权请联系删除。

未经允许不得转载:百木园 » 如何写出优雅的Controller层代码

相关推荐

  • 暂无文章