1 概述

本篇文章以Spring Boot为基础,从以下三个方向讲述了如何设计一个优秀的后端接口体系:

  • 参数校验:涉及Hibernate Validator的各种注解,快速失败模式,分组,组序列以及自定义注解/Validator
  • 异常处理:涉及ControllerAdvice/@RestControllerAdvice以及@ExceptionHandler
  • 数据响应:涉及如何设计一个响应体以及如何包装响应体

有了一个优秀的后端接口体系,不仅有了规范,同时扩展新的接口也很容易,本文演示了如何从零一步步构建一个优秀的后端接口体系。

2 新建工程

打开熟悉的IDEA,选择依赖:

首先创建如下文件:

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测试,因此需要加上跨域注解@CrossOrigin,默认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";
}
}

业务层首先进行了参数校验,这里省略了持久化操作。

User.java

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

3 参数校验

首先来看一下参数校验,上面的例子中在业务层完成参数校验,这是没有问题的,但是,还没进行业务操作就需要进行这么多的校验显然这不是很好,更好的做法是,使用Hibernate Validator

3.1 Hibernate Validator

3.1.1 介绍

JSRJava Specification Requests的缩写,意思是Java规范提案,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。JSR-303Java EE6中的一项子规范,叫作Bean ValidationHibernate ValidatorBean Validator的参考实现,除了实现所有JSR-303规范中的内置constraint实现,还有附加的constraint,详细如下:

  • @Null:被注解元素必须为null(为了节省篇幅下面用“元素”代表“被注解元素必须为”)
  • @NotNull:元素不为null
  • @AssertTrue:元素为true
  • @AssertFalse:元素为false
  • @Min(value):元素大于或等于指定值
  • @Max(value):元素小于或等于指定值
  • @DecimalMin(value):元素大于指定值
  • @DecimalMax(value):元素小于指定值
  • @Size(max,min):元素大小在给定范围内
  • @Digits(integer,fraction):元素字符串中的整数位数规定最大integer位,小数位数规定最大fraction
  • @Past:元素是一个过去日期
  • @Future:元素是将来日期
  • @Pattern:元素需要符合正则表达式

其中Hibernate Validator附加的constraint如下:

  • @Eamil:元素为邮箱
  • @Length:字符串大小在指定范围内
  • @NotEmpty:字符串必须非空(目前最新的6.1.5版本已弃用,建议使用标准的@NotEmpty
  • @Range:数字在指定范围内

而在Spring中,对Hibernate Validation进行了二次封装,添加了自动校验,并且校验信息封装进了特定的BindingResult中。下面看看如何使用。

3.1.2 使用

在各个字段加上@NotEmpty,并且邮箱加上@Email,电话加上11位限制,并且在各个注解加上message,表示对应的提示信息:

@Data
public class User {
@NotEmpty(message = "电话不能为空")
@Length(min = 11,max = 11,message = "电话号码必须11位")
private String phone;
@NotEmpty(message = "密码不能为空")
@Length(min = 6,max = 20,message = "密码必须为6-20位")
private String password;
@NotEmpty(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

对于String来说有时候会使用@NotNull@NotBlank,它们的区别如下:

  • @NotEmpty:不能为null并且长度必须大于0,除了String外,对于Collection/Map/数组也适用
  • @NotBlank:只用于String,不能为null,并且调用trim()后,长度必须大于0,也就是必须有除空格外的实际字符
  • @NotNull:不能为null

接着把业务层的参数校验操作删除,并把控制层修改如下:

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

在需要校验的对象上加上@Valid,并且加上BindingResult参数,可以从中获取错误信息并返回。

3.1.3 测试

全部都使用错误的参数设置,返回”邮箱格式不正确“:

第二次测试中除了密码都使用正确的参数,返回”密码必须为6-20位“:

第三次测试全部使用正确的参数,返回”success“:

3.2 校验模式设置

Hibernate Validator有两种校验模式:

  • 普通模式:默认模式,会校验所有属性,然后返回所有的验证失败信息
  • 快速失败模式:只要有一个验证失败就返回

使用快速失败模式需要通过HibernateValidateConfiguration以及ValidateFactory创建Validator,并且使用Validator.validate()进行手动验证。

首先添加一个生成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);
}
}

修改控制层的代码,通过@RequiredArgsConstructor注入FailFastValidator<User>,并把原来的在User上的@Valid去掉,在方法体进行手动验证:

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

测试(连续三次校验的结果):

如果是普通模式(修改.failFast(false)),一次校验便会连续输出三个信息:

3.3 @Valid@Validated

@Validjavax.validation包里面的,而@Validatedorg.springframework.validation.annotation里面的,是@Valid的一次封装,相当于是@Valid的增强版,供Spring提供的校验机制使用,相比起@Valid@Validated提供了分组以及组序列的功能。下面分别进行介绍。

3.4 分组

当需要在不同的情况下使用不同的校验方式时,可以使用分组校验。比如在注册时不需要校验id,修改信息时需要校验id,但是默认的校验方式在两种情况下全部都校验,这时就需要使用分组校验。

下面以不同的组别校验电话号码长度的不同进行说明,修改User类如下:

@Data
public class User {
@NotEmpty(message = "电话不能为空")
@Length(min = 11,max = 11,message = "电话号码必须11位",groups = {GroupA.class})
@Length(min = 12,max = 12,message = "电话号码必须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/GroupBUser中的两个空接口,然后修改控制层:

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位,测试如下:

3.5 组序列

默认情况下,不同组别的约束验证的无序的,也就是说,对于下面的User类:

@Data
public class User {
@NotEmpty(message = "电话不能为空")
@Length(min = 11,max = 11,message = "电话号码必须11位")
private String phone;
@NotEmpty(message = "密码不能为空")
@Length(min = 6,max = 20,message = "密码必须为6-20位")
private String password;
@NotEmpty(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
}

每次进行校验的顺序不同,三次测试结果如下:

有些时候顺序并不重要,而有些时候顺序很重要,比如:

  • 第二个组中的约束验证依赖于一个稳定状态运行,而这个稳定状态由第一个组来进行验证
  • 某个组的验证比较耗时,CPU和内存的使用率相对较大,最优的选择是将其放在最后进行验证

因此在进行组验证的时候需要提供一种有序的验证方式,一个组可以定义为其他组的序列,这样就可以固定每次验证的顺序而不是随机顺序,另外如果验证组序列中,前面的组验证失败,则后面的组不会验证。

例子如下,首先修改User类并定义组序列:

@Data
public class User {
@NotEmpty(message = "电话不能为空",groups = {First.class})
@Length(min = 11,max = 11,message = "电话号码必须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{}
}

定义了两个空接口FirstSecond表示顺序,同时在Group中使用@GroupSequence指定了顺序。

接着修改控制层,在@Validated中定义组:

public String test(@RequestBody @Validated({User.Group.class}) User user, BindingResult bindingResult)

这样就能按照固定的顺序进行参数校验了。

3.6 自定义校验

尽管Hibernate Validator中的注解适用情况很广了,但是有时候需要特定的校验规则,比如密码强度,人为判定弱密码还是强密码。也就是说,此时需要添加自定义校验的方式,有两种处理方法:

  • 自定义注解
  • 自定义Validator

首先来看一下自定义注解的方法。

3.6.1 自定义注解

这里添加一个判定弱密码的注解WeakPassword

@Documented
@Constraint(validatedBy = WeakPasswordValidator.class)
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WeakPassword{
String message() default "请使用更加强壮的密码";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

同时添加一个实现了ConstraintValidator<A,T>WeakPasswordValidator,当密码长度大于10位时才符合条件,否则返回false表示校验不通过:

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如下,在对应的字段加上自定义注解@WeakPassword

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

测试如下:

3.6.2 自定义Validator

除了自定义注解之外,还可以自定义Validator来实现自定义的参数校验,需要实现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!");
}
}

实现其中的supports以及validate

  • support:可以验证该类是否是某个类的实例
  • validate:当supports返回true后,验证给定对象o,当出现错误时,向errors注册错误

ValidationUtils.rejectIfEmpty校验当对象o中某个字段属性为空时,向其中的errors注册错误,注意并不会中断语句的运行,也就是即使password为空,user.getPassword()还是会运行,这时会抛出空指针异常。下面的errors.rejectValue同样道理,并不会中断语句的运行,只是注册了错误信息,中断的话需要手动抛出异常。

修改控制层中的返回值,改为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);

测试:

4 异常处理

到这里参数校验就完成了,下一步是处理异常。

如果将参数校验中的BindingResult去掉,就会将整个后端异常返回给前端:

//public String test(@RequestBody @Validated({User.Group.class}) User user, BindingResult bindingResult)
public String test(@RequestBody @Validated({User.Group.class}) User user)



这样虽然后端是方便了,不需要每一个接口都加上BindingResult,但是前端不好处理,整个异常都返回了,因此后端需要捕捉这些异常,但是,不能手动去捕捉每一个,这样还不如之前使用BindingResult,这种情况下就需要用到全局的异常处理。

4.1 基本使用

处理全局异常的步骤如下:

  • 创建全局异常处理的类:加上@ControllerAdvice/@RestControllerAdvice注解(取决于控制层用的是@Controller/@RestController@Controller可以跳转到相应页面,返回JSON等加上@ResponseBody即可,而@RestController相当于@Controller+@ResponseBody,返回JSON无需加上@ResponseBody,但是视图解析器无法解析jsp以及html页面)
  • 创建异常处理方法:加上@ExceptionHandler指定想要处理的异常类型
  • 处理异常:在对应的处理异常方法中处理异常

这里增加一个全局异常处理类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);
}

然后就可以进行测试了:

全局异常处理相比起原来的每一个接口都加上BindingResult方便很多,而且可以集中处理所有异常。

4.2 自定义异常

很多时候都会用到自定义异常,这里新增一个测试异常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,"测试异常");
} public TestException(String msg)
{
this(111,msg);
}
}

接着在刚才的全局异常处理类中添加一个处理该异常的方法:

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

在控制层进行测试:

@PostMapping("test")
public String test(@RequestBody @Validated({User.Group.class}) User user)
{
throw new TestException("出现异常");
// return service.test(user);
}

结果如下:

5 数据响应

在处理好了参数校验以及异常处理之后,下一步就是要设置统一的规范化的响应数据,一般来说无论响应成功还是失败都会有一个状态码,响应成功还会携带响应数据,响应失败则携带相应的失败信息,因此,第一步是设计一个统一的响应体。

5.1 统一响应体

统一响应体需要创建响应体类,一般来说,响应体需要包含:

  • 状态码:String/int
  • 响应信息:String
  • 响应数据:Object/T(泛型)

这里简单的定义一个统一响应体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(),"参数校验失败",error.getDefaultMessage());
} @ExceptionHandler(TestException.class)
public Result<String> testExceptionHandler(TestException e)
{
return new Result<>(e.getCode(),"失败",e.getMsg());
}
}

使用Result<String>封装返回值,测试如下:

可以看到返回了一个比较友好的信息,无论是响应成功还是响应失败都会返回同一个响应体,当需要返回具体的用户数据时,可以修改控制层接口直接返回Result<User>

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

测试:

5.2 响应码枚举

通常来说可以把响应码做成枚举类:

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

枚举类封装了状态码以及信息,这样在返回结果时,只需要传入对应的枚举值以及数据即可:

@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 全局包装响应体

统一响应体是个很好的想法,但是还可以再深入一步去优化,因为每次返回之前都需要对响应体进行包装,虽然只是一行代码但是每个接口都需要包装一下,这是个很麻烦的操作,为了更进一步“偷懒”,可以选择实现ResponseBodyAdvice<T>来进行全局的响应体包装。

修改原来的全局异常处理类如下:

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

实现了ResponseBodyAdvice<Object>

  • supports方法:判断是否支持控制器返回方法类型,可以通过supports判断哪些类型需要包装,哪些不需要包装直接返回
  • beforeBodyWrite方法:当supports返回true后,对数据进行包装,这样在返回数据时就无需使用Result<User>手动包装,而是直接返回User即可

接着修改控制层,直接返回实体类User而不是响应体包装类Result<User>

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

测试输出如下:

5.4 绕过全局包装

虽然按照上面的方式可以使后端的数据全部按照统一的形式返回给前端,但是有时候并不是返回给前端而是返回给其他第三方,这时候不需要code以及msg等信息,只是需要数据,这样的话,可以提供一个在方法上的注解来绕过全局的响应体包装。

比如添加一个@NotResponseBody注解:

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

接着需要在处理全局包装的类中,在supports中进行判断:

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

最后修改控制层,在需要绕过的方法上添加自定义注解@NotResponseBody即可:

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

6 总结

7 源码

直接clone下来使用IDEA打开即可,每一次优化都做了一次提交,可以看到优化的过程,喜欢的话欢迎给个star:

8 参考

1、UncleChen的博客-SpringBoot自定义请求参数校验

2、简书-@Valid和@Validated的总结区分

3、博客园-@Controller与@RestController的区别

4、简书-【项目实践】-SpringBoot三招组合拳,手把手教你打出优雅的后端接口

5、简书-【项目实践】后端接口统一规范的同时,如何优雅得扩展规范

SpringBoot-如何设计优秀的后端接口?的更多相关文章

  1. SpringBoot写后端接口,看这一篇就够了!

    摘要:本文演示如何构建起一个优秀的后端接口体系,体系构建好了自然就有了规范,同时再构建新的后端接口也会十分轻松. 一个后端接口大致分为四个部分组成:接口地址(url).接口请求方式(get.post等 ...

  2. 【项目实践】SpringBoot三招组合拳,手把手教你打出优雅的后端接口

    以项目驱动学习,以实践检验真知 前言 一个后端接口大致分为四个部分组成:接口地址(url).接口请求方式(get.post等).请求数据(request).响应数据(response).如何构建这几个 ...

  3. vue菜鸟从业记:公司项目里如何进行前后端接口联调

    最近我的朋友王小闰进入一家新的公司,正好公司项目采用的是前后端分离架构,技术栈是王小闰非常熟悉的vue全家桶,后端用的是Java语言. 在前后端开发人员碰面之后,协商确定好了前端需要的数据接口(扯那么 ...

  4. SpringCloud微服务之跨服务调用后端接口

    SpringCloud微服务系列博客: SpringCloud微服务之快速搭建EurekaServer:https://blog.csdn.net/egg1996911/article/details ...

  5. 前阿里P8架构师谈如何设计优秀的API

    随着大数据.公共平台等互联网技术的日益成熟,API接口的重要性日益凸显,从公司的角度来看,API可以算作是公司一笔巨大的资产,公共API可以捕获用户.为公司做出许多贡献.对于个人来说,只要你编程,你就 ...

  6. springboot + redis + 注解 + 拦截器 实现接口幂等性校验

    一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多 ...

  7. 单机Web后端接口服务压力测试

    单机Web后端接口服务压力测试 工具:Apache jmeter 环境:Window 10 语言:Kotlin + java 架构:SpringBoot + + Mysql + redis + Spr ...

  8. DRF框架之使用Django框架完成后端接口(API)的定义

    学习DRF框架,首先我们就需要明白为什么要学习这个框架. 接下来我们就先用原生的Django框架来定义一个符合RESTful设计方法的接口(API). RESTful接口的需求如下: GET /boo ...

  9. 苹果教你六招:设计优秀的icon

    在iOS 7测试版发布后,网上开始出现大量关于iOS 7设计的资源.在WWDC期间,苹果曾为开发者举办了多场主题演讲,其中有一场是苹果UX布道师Mike Stern的精彩演讲-- 优秀iOS设计最佳实 ...

随机推荐

  1. [Python] Matplotlib 图表的绘制和美化技巧

    目录 在一张画布中绘制多个图表 加图表元素 气泡图 组合图 直方图 雷达图 树状图 箱形图 玫瑰图 在一张画布中绘制多个图表 Matplotlib模块在绘制图表时,默认先建立一张画布,然后在画布中显示 ...

  2. JS数字每三位加逗号的最简单方法

    <script> function thousands(num){ var str = num.toString(); var reg = str.indexOf("." ...

  3. pdf转换成文本解决格式不统一问题

    pdf转换成文本解决格式不统一问题 懒得调OCR服务了,所以快速解决的方法是: pdf转png:https://pdf2png.com/zh/ png转统一格式pdf:adobe acrobat自带增 ...

  4. 后端程序员之路 48、memcached

    memcached - a distributed memory object caching systemhttp://memcached.org/ Memcached 教程 | 菜鸟教程http: ...

  5. 007-变量的作用域和LED点阵

    变量 一.局部变量和全局变量 局部变量:函数内申明的变量,只在函数内有效. 全局变量:函数外部申明的变量.一个源程序文件有一个或者多个函数,全局变量对他们都起作用. 备注:全局变量有副作用,降低了函数 ...

  6. 如何在Bash脚本中引入alias

    更多精彩内容,请关注微信公众号:后端技术小屋 alias的使用 在日常开发中,为了提高运维效率,我们会用alias(命令别名)来定义命令的简称.比如在~/.bash_profile中添加: alias ...

  7. C# 基础 - 堆栈跟踪使用

    使用一:可用于捕获报错时. using System.Diagnostics; ... StackTrace st = new StackTrace(true); string stackIndent ...

  8. struct2中package的参数解析

    struct2框架的核心组件是action和拦截器,它使用包来管理action和拦截器,每个包就是多个action.多个拦截器引用的集合.在struct.xml中,package元素用于定义包的配置, ...

  9. 6、Spring教程之自动装配

    自动装配说明 自动装配是使用spring满足bean依赖的一种方法 spring会在应用上下文中为某个bean寻找其依赖的bean. Spring中bean有三种装配机制,分别是: 在xml中显式配置 ...

  10. APIView里如何获取HTTP里的数据

    request.data.get()  获取post方法表单里的数据 request.post.get()  获取post方法表单里的数据 request.GET.get()  获取URL里的数据 r ...