Напишите приятный интерфейс API (2)

Java
Напишите приятный интерфейс API (2)

введение

Спецификация интерфейсов API особенно важна при разработке интерфейсов. Спецификация унифицированного формата уменьшит сложность совместной отладки и порадует последующих сопровождающих. При написании интерфейсов использование соответствующих методов проверки параметров также может повысить надежность кода. .Эта статья подробно объясним некоторые из моих переживаний, связанных с этими двумя моментами.

думать

  1. Бизнес-интерфейсы обычно содержат самые основные CRUD-операции, как максимально унифицировать эту часть интерфейса?
  2. Часть интерфейса API будет использоватьHttpServletRequestа такжеHttpServletResponseКак легко получить эти два объекта?
  3. Параметры интерфейса иногда передаются через один параметр, а иногда через объект.Как легко протестировать эти параметры без частого использования суждений if для скрининга критических значений или оценки нулей?

Имея в виду эти вопросы, мы будем решать их шаг за шагом.

Общая диаграмма классов

Единый базовый путь интерфейса

Как упоминалось в предыдущей статье, как использовать разные протоколы с правильной осанкой,GET,POST,PUT,PATCH,DELETEЭти пять протоколов очень часто используются при ежедневной разработке CRUD.Различные компании в основном перечисляют эти интерфейсы API, поэтому можем ли мы попытаться извлечь их в интерфейсы, а затем позволить различным уровням бизнес-контроля реализовать этот интерфейс, чтобы стандартизировать. интерфейсный путь?теория есть,начинается практика

упражняться

версия v1

Сначала определите общий класс интерфейса Java, напишите четыре метода, соответствующие четырем операциям добавления, удаления, поиска и модификации.注:PATCH接口修改单个属性值,因为不同业务中字段存在太大差异,所以她比较适合单独实现,这个接口中就不涵盖这个接口了.

public interface BaseCrud {
    R selectList(@ModelAttribute Map s);
    R selectOne(@PathVariable String id);
    R add(@RequestBody Map t);
    R upp(@PathVariable String id, @RequestBody Map t);
    R del(@PathVariable String id);
}

Я считаю, что каждый может видеть эти пять интерфейсов. Разве это не просто добавляет интерфейс пейджинга для добавления, удаления, проверки и модификации? С помощью этого интерфейса не просто для нас написать элемент управления Определенный бизнес, непосредственно реализуйте его, а затем реализуйте разные логики, идеально.

//这里以用户相关业务和User用户相关业务为例
//教师业务控制层
@Controller
@RequestMapping("user")
public class UserController implements BaseCrud {
    @Override
    @GetMapping
    @ResponseBody
    public R selectList(Map s) {
        return null;
    }
    @Override
    @GetMapping("{id}")
    @ResponseBody
    public R selectOne(String id) {
        return null;
    }
    @Override
    @PostMapping
    @ResponseBody
    public R add(Map t) {
        return null;
    }
    @Override
    @PutMapping("{id}")
    @ResponseBody
    public R upp(String id, Map t) {
        return null;
    }
    @Override
    @DeleteMapping("{id}")
    @ResponseBody
    public R del(String id) {
        return null;
    }
}
//教师业务控制层,跟上面一样的操作,这里节省篇幅,先省略....

С помощью теста postMan вы можете использовать префиксы учителя и пользователя для вызова интерфейсов соответствующих предприятий и просто добавлять, удалять, проверять и изменять единый путь.Однако будет и дефект, потому что мы все используем Map чтобы получить, сопоставить пары "ключ-значение"Поскольку поле является неопределенным, его трудно поддерживать, если оно не отлажено на более позднем этапе.Даже если поле удалено, его можно настроить, что не удобно для устранения неполадок.Во-вторых, если мы используем Mybatis для работы, нам нужно преобразовать его в объект.Хотя путь интерфейса унифицирован, следующие операции все еще очень громоздки, поэтому нам все еще нужно оптимизировать.

версия v2

я верю всемДженерикиВсе они знакомы. Мы также можем использовать дженерики для оптимизации здесь. Поскольку разбиение на страницы поиска должно содержать номера страниц, сортировку и т. д., мы реализуем его как два дженерика.

//需要注意一点小细节,因为这五个接口的路径协议都是可以统一的,所以我们在定义接口的时候就把后面拼接的路径写在接口中.
public interface BaseCrud<T, S> {

    /**
     * @description: 分页查询接口
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 11:40 上午
     */
    @GetMapping
    R selectList(@ModelAttribute S s);

    /**
     * @description: 根据id查询单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 11:44 上午
     */
    @GetMapping("{id}")
    R selectOne(@PathVariable String id);

    /**
     * @description: 新增单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 1:39 下午
     */
    @PostMapping
    R add(@RequestBody T t);

    /**
     * @description: 修改单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 1:39 下午
     */
    @PutMapping("{id}")
    R upp(@PathVariable String id, @RequestBody T t);

    /**
     * @description: 删除单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 1:40 下午
     */
    @DeleteMapping("{id}")
    R del(@PathVariable String id);
}

Определите два разных бизнес-объекта, которые используются для пейджинга и новых модификаций соответственно.

@Data
//对应泛型T,用于新增修改
public class User {
    private String mobile;
    private String name;
    private String email;
    private Integer age;
    private LocalDateTime birthday;
}
@Data
//对应泛型S,用于搜索
public class UserSearch {
    private Integer pageNum;
    private String mobile;
    private String name;
    private String email;
}

Далее идет реализация уровня бизнес-контроля.

@RestController 
@RequestMapping("teacher")
public class UserController implements BaseCrud<User, UserSearch> {
    @Override
    public R selectList(UserSearch userSearch) {
        return null;
    }
    @Override
    public R selectOne(String id) {
        return null;
    }
    @Override
    public R add(User user) {
        return null;
    }
    @Override
    public R upp(String id, User user) {
        return null;
    }
    @Override
    public R del(String id) {
        return null;
    }
}

Видно, что эта версия гораздо удобнее, чем версия V1, оптимизирует объекты передачи, и разные бизнесы могут создавать разные объекты для передачи.Есть небольшая деталь, @ResponseBody на методе мною опущен, т.к. RestController уже содержит аннотацию @ResponseBody.На данный момент завершена унифицированная демонстрация общего интерфейса API.

Единая параметрическая проверка

упражняться

Люди всегда стремятся к совершенству, и я не исключение.Хотя версия v2 выше унифицировала базовые интерфейсы, тяжело видеть столько полей, которые нужно проверять.Например, при добавлении нового пользователя никнейм пользователя не может быть пустым. Возраст пользователя должен быть не менее одного года и т. д. Еще раз, эквивалентный тест залит моим кодом. Глядя на полное решение, я должен найти способ его оптимизировать.

версия v3

Давайте сначала представим компонент проверки параметров пружиныvalidation, компонент может легко проверить входные параметры с помощью аннотаций, а если возникает исключение, его также можно вызвать напрямую, перехватив соответствующую информацию об исключении.

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

Вот некоторые часто используемые аннотации ограничений проверки

  • @Null аннотированный элемент должен быть нулевым
  • @NotNull аннотированный элемент не должен быть нулевым
  • @AssertTure аннотированный элемент должен быть истинным
  • @AssertFalse аннотированный элемент должен быть ложным
  • @Min(value) Аннотируемый элемент должен быть числом и должен быть больше или равен указанному значению.
  • @Max(value) Аннотируемый элемент должен быть числом и должен быть меньше или равен указанному значению.
  • @DecimalMin(value) Аннотированный элемент должен быть числом и должен быть больше или равен указанному значению.
  • @DecimalMax(значение) Аннотируемый элемент должен быть числом и должен быть меньше или равен указанному значению.
  • @Size(max, min) аннотируемый элемент должен находиться в пределах указанного диапазона
  • @Digits(целое число, дробь) Аннотируемый элемент должен быть числом, и его значение должно быть в заданном диапазоне
  • Аннотированный элемент @Past должен быть датой в прошлом.
  • Аннотированный элемент @Future должен быть датой в будущем.
  • @Pattern(value) Аннотированный элемент должен соответствовать заданному регулярному выражению.
  • @Email аннотированный элемент должен быть адресом электронной почты
  • @Length(min, max) Длина аннотируемого элемента должна быть в пределах указанного диапазона
  • @NotEmpty Аннотируемый элемент не должен быть пустым, как и пустая строка.
  • @Range аннотируемый элемент (который может быть числом или строкой, представляющей число) должен находиться в заданном диапазоне
  • Аннотированный элемент @URL должен быть URL-адресом.
  • @Valid проверяет класс сущности

Далее начинаем преобразовывать наш интерфейс, можно добавлять аннотации к сущностям, используемым в BaseCrud.

    /**
     * @description: 分页查询接口
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 11:40 上午
     */
    @GetMapping
    R selectList(@Validated @ModelAttribute S s);
    /**
     * @description: 新增单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 1:39 下午
     */
    @PostMapping
    R add(@Validated @RequestBody T t);
    /**
     * @description: 修改单条数据
     * @author: chenyunxuan
     * @updateTime: 2020/12/18 1:39 下午
     */
    @PutMapping("{id}")
    R upp(@PathVariable String id, @Validated @RequestBody T t);

В то же время наш класс сущностей также должен быть соответствующим образом преобразован, добавление нужных аннотаций проверки.

public class User {
    /**
     * @description: 自定义参数效验(电话号码校验)
     * @author: chenyunxuan
     * @updateTime: 2019-12-18 17:30
     */
    @MobileVail(groups = {Add.class})
    private String mobile;
    //用户名称最短两位,最长30位
    //这里的group分组后面会介绍其作用
    @Size(min = 2, max = 30, groups = {Upp.class})
    private String name;
    /**
     * @description: 自定义错误信息
     * @author: chenyunxuan
     * @updateTime: 2019-12-18 17:30
     */
    //校验注解都可以自定义message,可配合异常拦截返回你想要message
    @NotEmpty(message = "自定义错误信息,Email不能为空")
    @Email
    private String email;
    @NotNull
    @Min(18)
    @Max(100)
    private Integer age;
    @DateTimeFormat(pattern = "MM/dd/yyyy")
    //不可为空且必须是在系统时间之前
    @NotNull
    @Past
    private LocalDateTime birthday;
}

Из приведенного выше кода мы можем сделать следующие выводы

  1. Аннотации проверки могут быть наложены
  2. Вы можете настроить аннотации, если предварительно заданные аннотации проверки не могут быть удовлетворены.
  3. Различные бизнес-требования и различные методы проверки могут быть реализованы путем группировки
  4. Информация для проверки может быть установлена ​​вами самостоятельно
Пользовательские аннотации проверки

Предустановленная проверка иногда не может соответствовать требованиям бизнес-проверки, таким как проверка номера телефона, проверка удостоверения личности и т. д. К счастью, проверка также учитывает эту часть требований и предоставляет интерфейс ConstraintValidator для настройки правил сопоставления.

Сначала нам нужно настроить аннотацию и класс правила проверки.

@Documented
// 指定真正实现校验规则的类
@Constraint(validatedBy = MobileValidator.class)
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface MobileVail {
    String message() default "不是正确的手机号码";

    Class<?>[] groups() default { };

    Class<? extends Payload>[] payload() default { };

    @Target({ElementType.METHOD,ElementType.FIELD,ElementType.PACKAGE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        MobileVail[] value();
    }
}

//ConstraintValidator接口使用了泛型,需要指定两个参数,第一个自定义注解类,第二个为需要校验的数据类型。
public class MobileValidator  implements ConstraintValidator<MobileVail, String> {
    //这里是具体的匹配规则
    private static final Pattern PHONE_PATTERN = Pattern.compile(
            "^((13[0-9])|(15[^4])|(18[0,2,3,5-9])|(17[0-8])|(147))\\d{8}$"
    );

    @Override
    public void initialize(MobileVail constraintAnnotation) {

    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        //实现验证方法
        if ( value == null || value.length() == 0 ) {
            return false;
        }
        Matcher m = PHONE_PATTERN.matcher(value);
        return m.matches();
    }
}

После завершения этого шага вам нужно только добавить @MobileVail в поля, которые необходимо проверить, и вы можете использовать его с радостью.

Групповая проверка

Глядя на код, реализованный в v3, при проверке имени и мобильных атрибутов мы используем функцию группировки, которая заключается в использовании разных методов проверки для разных бизнес-сценариев.

    ......
    @MobileVail(groups = {Add.class})
    private String mobile;
    @Size(min = 2, max = 30, groups = {Upp.class})
    private String name;
    ......

Определения классов Add и Upp относительно просты, допустим пустой класс аннотаций.

public @interface Add {}
public @interface Upp {}

После выполнения этого шага вам нужно только добавить разные группы в разные сервисы, чтобы изолировать проверку по группам.Например, в примере пользователя выше, при добавлении данных нам нужно проверить правильность номера телефона и изменить его Верификация не требуется, нам нужно проверить длину ника пользователя при модификации, и не требуется проверка при добавлении, нам нужно только указать его группировку в @Validated.

    @PostMapping
    R add(@Validated(value = Add.class) @RequestBody T t);
    @PutMapping("{id}")
    R upp(@PathVariable String id, @Validated(value = Upp.class) @RequestBody T t);
поймать исключение

После использования проверки, если вы не настроите исключение захвата, выдаваемая информация об исключении будет очень подробной и неудобной для клиента, поэтому нам нужно перехватить эту часть исключения и создать специальное сообщение. упоминалось в предыдущей статье.Унифицированный класс перехватчика исключений для .

@ControllerAdvice
@Log4j2
public class GlobalExceptionHandler {
    ......
    /**
     * @description: JSON传值出现异常(对应@RequestBody传值错误)
     * @author: chenyunxuan
     * @date: 2019-12-18 16:37
     * @version: 1.0.0
     * @updateTime: 2019-12-18 16:37
     */
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    @ResponseBody
    public R handleMethodArgumentNotValidException(HttpServletRequest req, MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        StringBuilder sb = new StringBuilder();
        sb.append("url=");
        sb.append(req.getRequestURI().replace("/", ""));
        sb.append(",");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append("field=");
            sb.append(fieldError.getObjectName());
            sb.append(".");
            sb.append(fieldError.getField());
            sb.append(",error=");
            sb.append(fieldError.getDefaultMessage());
            sb.append(";");
        }
        String msg = sb.toString();
        log.error(String.format("MethodArgumentNotValidException RequestURI:%s msg:%s", req.getRequestURI(), msg), e);
        return ResultUtil.error(400, bindingResult.getFieldError().getDefaultMessage());
    }

    /**
     * @title: 单个参数参数异常(对应单个参数传值错误)
     * @author: chenyunxuan
     * @date: 2019-12-18 16:37
     * @version: 1.0.0
     * @updateTime: 2019-12-18 16:37
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    @ResponseBody
    public R handleMethodArgumentNotValidException(HttpServletRequest req, ConstraintViolationException e) {
        log.error(String.format("ConstraintViolationException RequestURI:%s", req.getRequestURI()), e);
        return ResultUtil.error(400, e.getMessage());
    }

    /**
     * @title: 提交FORM参数异常(对应form表单传值错误)
     * @author: chenyunxuan
     * @date: 2019-12-18 16:41
     * @version: 1.0.0
     * @updateTime: 2019-12-18 16:41
     */
    @ExceptionHandler(value = BindException.class)
    @ResponseBody
    public R handleBindException(HttpServletRequest req, BindException e) throws BindException {
        // ex.getFieldError():随机返回一个对象属性的异常信息。如果要一次性返回所有对象属性异常信息,则调用ex.getAllErrors()
        FieldError fieldError = e.getFieldError();
        StringBuilder sb = new StringBuilder();
        sb.append(fieldError.getDefaultMessage());
        // 生成返回结果
        log.error("BindException requestURI:{} paramName:{} msg:{}", req.getRequestURI(), e.getObjectName(), fieldError.getDefaultMessage());
        return ResultUtil.error(400, fieldError.getDefaultMessage());
    }
}

версия v4

Часть интерфейса API будет использоватьHttpServletRequestа такжеHttpServletResponseЧтобы легко получить эти два объекта, нужно добавлять данные в соответствующий метод уровня управления к параметрам каждый раз, когда они используются.

public R selectOne(String id, HttpServletRequest request) {
    return null;
}

Таким образом, когда входные параметры печатаются в аспекте, используется еще один объект запроса. Если он используется в нескольких местах, необходимо искать один и тот же объект HttpServletRequest. Здесь я выбираю использование абстрактного класса для внедрения этих два объекта.

/**
 * @description: 控制层基类
 * @author: chenyunxuan
 * @updateTime: 2020/12/18 3:36 下午
 */
public abstract class BaseController {
    @Autowired
    protected HttpServletRequest request;
    @Autowired
    protected HttpServletResponse response;
}

Затем наследуйте этот абстрактный класс на уровне управления, и вы можете с радостью использовать объекты запроса и ответа непосредственно в методе.

public class UserController extends BaseController implements BaseCrud<User, UserSearch> {

    @Override
    public R selectList(UserSearch userSearch) {
        request.getRequestURI();
        return null;
    }
}

постскриптум

После четырех версий оптимизации вам будет представлена ​​полная структура уровня управления бизнесом.

  1. Единый базовый адрес запроса CRUD
  2. Оптимизированная проверка ввода
  3. Оптимизировать способ получения объектов запросов и ответов

Есть больше навыков общения в области комментариев, и я надеюсь пообщаться с вами, чтобы написать более приятный интерфейс API.Следующая статья будет начинаться с интерфейса版本控制Начнем, и расскажем маленькие хитрости по контролю версий старого и нового интерфейсов.Добро пожаловать в продолжение внимания.