Validation in Spring Boot

添加依赖

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

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

测试

PersonForm.java

@Data
public class PersonForm {

    @NotNull
    @Size(min=2, max=30)
    private String name;

    @NotNull
    @Min(18)
    private Integer age;
}

IndexController.java

@PostMapping("/check")
public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        FieldError fieldError = (FieldError) bindingResult.getAllErrors().get(0);
        System.out.println(fieldError.getField() + "-" + fieldError.getRejectedValue() + "-" +fieldError.getCode() + "-" + fieldError.getDefaultMessage());
        // output: age-11-Min-最小不能小于18
        return "form";
    }

    return "redirect:/results";
}

通过参数 @Valid 表示需要对PersonForm实例进行验证。第二个参数BindingResult 查看是否验证有错误。这个参数必须紧跟@Valid的Bean后面。如果没有指定参数 BindingResult,那么将会抛出异常 MethodArgumentNotValidException

编程式验证

  • 在spring环境下直接注入
private final Validator validator;
  • 验证
@GetMapping("/form")
public String form(PersonForm personForm) throws BindException {
    validate(personForm);
    return "form";
}

private <T> void validate(T t) throws BindException {
    Map<String, Object> map = new HashMap<>(16);
    BindingResult errors =  new MapBindingResult(map, t.getClass().getName());
    validator.validate(t, errors);
    if (errors.hasErrors()) {
        throw new BindException(errors);
    }
}

自定义验证规则

  • 添加验证器 PhoneValidator 验证手机号码
public class PhoneValidator implements ConstraintValidator<PhoneValid, String> {

    private static final String MOBILE_REGEX = "^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17[013678])|(18[0,5-9]))\\d{8}$";

    private static final int MOBILE_LENGTH = 11;

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext context) {
        if (Objects.isNull(phone)) {
            return true;
        }

        if (phone.length() != MOBILE_LENGTH) {
            return false;
        } else {
            Pattern p = Pattern.compile(MOBILE_REGEX);
            Matcher m = p.matcher(phone);
            if (m.matches()) {
                return true;
            }
        }
        return false;
    }
}
  • 添加注解 PhoneValid
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(
    validatedBy = {PhoneValidator.class}
)
public @interface PhoneValid {
    String message() default "手机号码格式不正确";

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

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

PersonForm 添加属性手机号:

@PhoneValid
private String phone;

验证:

@GetMapping("/form")
public String form(PersonForm personForm) throws BindException {
    personForm.setName("Rick.Xu");
    personForm.setAge(19);
    personForm.setPhone("1232");
    validate(personForm);
    return "form";
}

控制台打印

.validation.BindException: org.springframework.validation.MapBindingResult: 1 errors
Field error in object 'com.rick.security.api.PersonForm' on field 'phone': rejected value [1232]; codes [PhoneValid.com.rick.security.api.PersonForm.phone,PhoneValid.phone,PhoneValid]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [com.rick.security.api.PersonForm.phone,phone]; arguments []; default message [phone]]; default message [手机号码格式不正确]]

参考文章

sharp-common集成验证

集成

注册ValidatorHelper.java

@Configuration
@RequiredArgsConstructor
public class AppConfig {

    private final Validator validator;

    @Bean
    public ValidatorHelper validatorHelper() {
        return new ValidatorHelper(validator);
    }
}

测试

User.java

@Value
@Builder
public class User {

    @PhoneValid
    private String phone;

    @NotBlank
    private String name;
}

Test.java

@Autowired
private ValidatorHelper validatorHelper;

@Test
public void testValidate() {
    User user = User.builder().name("rick").phone("18898987765").build();
    validatorHelper.validate(user);
}

@Test(expected = IllegalArgumentException.class)
public void testValidate2() {
    User user = User.builder().name("Rick").phone("188").build();
    validatorHelper.validate(user);
}

自定义验证

枚举值验证

枚举SexEnum UserStatusEnum

@AllArgsConstructor
@Getter
public enum SexEnum {
    FEMALE(0, "FEMALE"),
    MALE(1, "MALE");

    private static final Map<Integer, SexEnum> codeMap = new HashMap<>();

    static {
        for (SexEnum e: values()) {
            codeMap.put(e.code, e);
        }
    }

    private final Integer code;

    private final String label;

    public static SexEnum valueOfCode(Integer code) {
        return codeMap.get(code);
    }
}

@AllArgsConstructor
@Getter
public enum UserStatusEnum {
    NORMAL("NORMAL"),
    LOCKED("LOCKED");

    private final String label;

    public String getCode() {
        return this.name();
    }

    public static UserStatusEnum valueOfCode(String code) {
        return valueOf(code);
    }
}

枚举中必须包含静态方法 valueOfCode 和 方法 getCode

User 中添加属性

@Value
@Builder
public class User {

    @PhoneValid
    private String phone;

    @NotBlank
    private String name;

//    @EnumValid(target = SexEnum.class, message = "性别有误,不存在值${validatedValue}")
    @EnumValid(target = SexEnum.class)
    private int sex;

    @EnumValid(target = UserStatusEnum.class, message = "用户状态有误,不存在值${validatedValue}")
    private String userStatus;
}

测试:

@Test
public void testValidate3() throws BindException {
    User user = User.builder().name("rick").phone("18898987765").sex(1).userStatus("LOCKED").build();
    validatorHelper.validate(user);
}

@Test(expected = BindException.class)
public void testValidate4() throws BindException {
    User user = User.builder().name("rick").phone("18898987765").sex(3).userStatus("ENABLED").build();
    validatorHelper.validate(user);
}

sharp-fileupload文档转换(五)

PDF 转图片

  • pdf按页转多张图片
@Test
public void testPdf2Image1() throws IOException {
    File file = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf/1.pdf");
    FileMeta fileMeta = FileMetaUtils.parse(file);

    List<byte[]> list = FileConvertUtils.pdf2Image(fileMeta.getData(), 150);
    File folder = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf");
    for (int i = 0; i < list.size(); i++) {
        FileUtils.writeByteArrayToFile(new File(folder, i + ".png"), list.get(i));
    }
}
  • pdf转一张图片
@Test
public void testPdf2Image2() throws IOException {
    File file = new File("/Users/rick/jkxyx205/tmp/fileupload/pdf/1.pdf");
    FileMeta fileMeta = FileMetaUtils.parse(file);

    FileConvertUtils.pdf2Image(fileMeta.getData(),
            new FileOutputStream("/Users/rick/jkxyx205/tmp/fileupload/pdf/full.png"),
            150);
}

sharp-fileupload客户端Restful(四)

客户端api可以用来上传、下载、浏览文件等操作,由 DocumentService 完成服务操作。DocumentService 主要工作:

  • 管理数据库维护元数据
  • FileStore 来完成存储
  • 依赖 ImageService 完成图片类型的浏览

准备工作

创建表

CREATE TABLE `sys_document` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) NOT NULL,
  `extension` varchar(16) DEFAULT NULL,
  `content_type` varchar(16) DEFAULT NULL,
  `size` int(11) DEFAULT NULL,
  `group_name` varchar(255) NOT NULL,
  `path` varchar(255) NOT NULL,
  `created_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加依赖

<dependency>
    <groupId>com.rick.db</groupId>
    <artifactId>sharp-database</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.47</version>
</dependency>

配置数据库

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/fastdfs?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2b8
    username: root
    password: jkxyx205

接口测试

批量上传

  • 请求
POST /documents/upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

Content-Disposition: form-data; name="file"; filename="/Users/rick/Desktop/StockSnap_STDZONQNPW.jpg
Content-Disposition: form-data; name="file"; filename="/Users/rick/Desktop/java技能树.jpg


------WebKitFormBoundary7MA4YWxkTrZu0gW--
  • 后端处理
@PostMapping("/upload")
public Result<List<?>> fileUpload(MultipartHttpServletRequest multipartRequest) throws IOException {
    return ResultUtils.success(documentService.store(FileMetaUtils.parse(multipartRequest, UPLOAD_NAME), "upload"));
}

@PostMapping("/upload2")
public Result<List<?>> fileUpload2(@RequestParam(UPLOAD_NAME) List<MultipartFile> fileList) throws IOException {
    return ResultUtils.success(documentService.store(FileMetaUtils.parse(fileList), "upload"));
}

@PostMapping("/upload3")
public Result<List<?>> fileUpload3(@RequestBody List<FileMeta> fileMetaList) throws IOException {
    return ResultUtils.success(documentService.store(fileMetaList, "upload"));
}
  • 响应
{
    "success": true,
    "code": 0,
    "msg": "OK",
    "data": [
        {
            "name": "StockSnap_STDZONQNPW",
            "extension": "jpg",
            "contentType": "image/jpeg",
            "size": 30079171,
            "groupName": "upload",
            "path": "475304840076365824.jpg",
            "url": "http://localhost:7892/upload/475304840076365824.jpg",
            "id": 475304840302858240,
            "createdAt": "2021-10-01T04:24:42.474Z",
            "fullName": "StockSnap_STDZONQNPW.jpg",
            "fullPath": "upload/475304840076365824.jpg"
        },
        {
            "name": "java技能树",
            "extension": "jpg",
            "contentType": "image/jpeg",
            "size": 337707,
            "groupName": "upload",
            "path": "475304840239943680.jpg",
            "url": "http://localhost:7892/upload/475304840239943680.jpg",
            "id": 475304840332218368,
            "createdAt": "2021-10-01T04:24:42.481Z",
            "fullName": "java技能树.jpg",
            "fullPath": "upload/475304840239943680.jpg"
        }
    ]
}

查看信息

  • 请求
GET /documents/475304840302858240 HTTP/1.1
  • 后端处理
/**
 * 文件详情
 * @return
 */
@GetMapping(value = "/{id}")
public Result<Document> documentInfo(@PathVariable Long id) {
    return ResultUtils.success(documentService.findById(id));
}
  • 响应
{
    "success": true,
    "code": 0,
    "msg": "OK",
    "data": {
        "name": "StockSnap_STDZONQNPW",
        "extension": "jpg",
        "contentType": "image/jpeg",
        "size": 30079171,
        "groupName": "upload",
        "path": "475304840076365824.jpg",
        "url": "http://localhost:7892/upload/475304840076365824.jpg",
        "id": 475304840302858240,
        "createdAt": "2021-10-01T04:24:42Z",
        "fullName": "StockSnap_STDZONQNPW.jpg",
        "fullPath": "upload/475304840076365824.jpg"
    }
}

下载

  • 请求
# 单文件下载

http://localhost:8080/documents/download?id=475308098194935808

# 批量下载

http://localhost:8080/documents/download?id=475308098194935808&id=475308098169769984

  • 后端处理
@GetMapping(value = "/download/{id}")
public void download(HttpServletRequest request, HttpServletResponse response, @PathVariable long id) throws IOException {
    documentService.download(request, response , id);
}

@GetMapping(value = "/download")
public void download(HttpServletRequest request, HttpServletResponse response, @RequestParam(name = "id") long[] ids) throws IOException {
    documentService.download(request, response, ids);
}

浏览

  • 请求
# 客户端跳转

http://localhost:8080/documents/preview/475308098194935808

# 图片进行处理

http://localhost:8080/documents/preview2/475308098194935808?rw=9&rh=5

# office预览必须带上文件扩展名

https://view.officeapps.live.com/op/view.aspx?src=http%3A%2F%2Fa923-112-87-216-2.ngrok.io%2Fdocuments%2Fpreview2%2F477893013692383232/小程序部署准备工作.docx


https://view.officeapps.live.com/op/view.aspx?src=http%3A%2F%2Fa923-112-87-216-2.ngrok.io%2Fdocuments%2Fpreview2%2F477897073204035588/演示.pptx


https://view.officeapps.live.com/op/view.aspx?src=http%3A%2F%2Fa923-112-87-216-2.ngrok.io%2Fdocuments%2Fpreview2%2F477897073204035587/需求.xlsx

Note: the needs to be URL encoded, and the document must be publicly accessible on the internet.

微软接口文档

  • 后端处理
 /**
 * 预览文件: 通过nginx,这种访问速度会更快些
 * http://localhost:8080/documents/preview/475029213054144512
 * @return
 * @throws IOException
 */
@GetMapping(value = "/preview/{id:\\d+}")
public void view(HttpServletResponse response, @PathVariable Long id) throws IOException {
    response.sendRedirect(documentService.getURL(id));
}

/**
 * 预览普通文件,将文件写到流中
 * http://localhost:8080/documents/preview2/475029213054144512
 * 预览office,必须有后缀docx/xlsx/pptx,否则会File not found
 * https://view.officeapps.live.com/op/view.aspx?src=http%3A%2F%2Fa923-112-87-216-2.ngrok.io%2Fdocuments%2Fpreview2%2F477896371325009920/hello.docx
 * @return
 * @throws IOException
 */
@GetMapping(value = {"/preview2/{id:\\d+}", "/preview2/{id:\\d+}/{fileName}.{extension:(?i)docx|xlsx|pptx}"})
public void view2(HttpServletRequest request, HttpServletResponse response, @PathVariable Long id, ImageParam imageParam) throws IOException {
    Document document = documentService.findById(id);
    documentService.preview(id, imageParam, HttpServletResponseUtils.getOutputStreamAsView(request, response, document.getFullName()));
    }

重命名

  • 请求
PUT /documents/475029213070921728/rename?name=hello world HTTP/1.1```
  • 后端处理
@PutMapping("/{id}/rename")
public Result rename(@PathVariable Long id,  String name) {
    documentService.rename(id, name);
    return ResultUtils.success();
}

删除

  • 请求
DELETE /documents/475010776193994752 HTTP/1.1
  • 响应
{
    "success": true,
    "code": 0,
    "msg": "OK"
}
  • 后端处理
    @DeleteMapping(value = "/{id}")
    public Result deleteDocument(@PathVariable Long id) {
        documentService.delete(id);
        return ResultUtils.success();
    }
  • 响应
{
    "success": true,
    "code": 0,
    "msg": "OK"
}