当前位置: 首页 > article >正文

从零搭建微服务项目Pro(第2-2章——JSR303自定义文件校验+整合至微服务公共模块)

前言:

JSR 303,即 Bean Validation,是 Java EE 6 中的一项子规范,旨在为 Java Bean 提供一种标准化的数据验证机制。它通过注解的方式,允许开发者在 Java 类的字段或方法上直接定义验证规则,从而将验证逻辑从业务代码中分离出来,提升代码的可维护性和灵活性。

核心功能与特点

  1. 注解驱动:JSR 303 提供了一系列内置的验证注解,如 @NotNull@Size@Min@Max@Email 等,开发者可以直接在字段上使用这些注解来定义验证规则125。

  2. 分组校验:支持根据不同的业务场景(如新增、更新)对字段进行分组校验。例如,新增时某些字段可以为空,而更新时则必须不为空135。

  3. 自定义约束:除了内置的注解,JSR 303 还允许开发者定义自定义的验证注解和逻辑,以满足特定的业务需求135。

  4. 统一异常处理:通过与 Spring 等框架的集成,JSR 303 可以方便地处理验证失败的情况,并将错误信息返回给前端356。

其主要用于表单数据的后端验证,确保数据的合法性。尽管前端通常也会进行验证,但后端验证是必不可少的,因为前端验证容易被绕过(如通过 Postman 等工具直接发送请求)。通过 JSR 303,开发者可以在控制器中方便地对表单提交的数据进行验证,并将验证结果返回给前端。

本章在上章基础上对原有自定义字符串校验注解进行适当修改,添加自定义文件验证注解,校验前端传输的文件大小、文件名、拓展名,优化原处理逻辑。并将其整合至微服务公共模块由各子服务模块共享,并统一进行异常处理。同时支持配置文件配置注解规范、支持枚举统一配置注解或各项分别配置。

本章使用的微服务项目博客链接如下,内有对应项目源码。

从零搭建微服务项目Pro(第1-3章——Quartz定时任务模块整合)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145727495?spm=1001.2014.3001.5502本项目源码链接如下:

wlf728050719/SpringCloudPro2-2https://github.com/wlf728050719/SpringCloudPro2-2以及本专栏会持续更新微服务项目,每一章的项目都会基于前一章项目进行功能的完善,欢迎小伙伴们关注!同时如果只是对单章感兴趣也不用从头看,只需下载前一章项目即可,每一章都会有前置项目准备部分,跟着操作就能实现上一章的最终效果,当然如果是一直跟着做可以直接跳过这一部分。专栏目录链接如下,其中Base篇为基础微服务搭建,Pro篇为复杂模块实现。

从零搭建微服务项目(全)-CSDN博客https://blog.csdn.net/wlf2030/article/details/145799620


一、前置项目准备

1.从github下载对应项目解压,重命名为Pro2_2打开。

2.重命名模块为Pro2_2。

3.父工程pom.xml中<name>改成Pro2_2。

4.选择环境为dev,并重新加载maven。

5.启动nacos(安装和启动见第三章)。

6.进入nacos网页 配置管理->配置列表确认有这些yaml文件。

(如果不是一直跟着专栏做自然是没有的,需要看第四章的环境隔离和配置拉取,记得把父工程pom文件中namespace的值与nacos中命名空间生成的保持一致)

7.配置数据源,更换两服务的resources下yml文件的数据库配置,数据库sql如下:

create table tb_order
(id           int auto_incrementprimary key,create_time  datetime     not null,status       int          not null,product_id   int          not null,seller_id    int          not null,buyer_id     int          not null,amount       int          not null,get_location varchar(255) not null
);
create table tb_task
(id              int auto_incrementprimary key,task_name       varchar(255)  not null,task_group      varchar(255)  not null,type            int           not null,bean_name       varchar(255)  null,class_name      varchar(255)  null,path            varchar(255)  null,method_name     varchar(255)  null,params          varchar(255)  null,cron_expression varchar(255)  not null,description     text          null,status          int default 0 not null,result          int           null
);
create table tb_task_log
(id             int auto_incrementprimary key,task_id        int          not null,start_time     datetime     not null,execute_time   varchar(255) not null,result         tinyint      not null,message        varchar(255) not null,exception_info text         null
);
create table tb_user
(id       int auto_incrementprimary key,username varchar(255) not null,sport    varchar(255) null,fruit    varchar(255) null,email    varchar(255) not null,password varchar(255) not null,account  varchar(255) not null,constraint accountunique (account),constraint emailunique (email),constraint usernameunique (username)
);

测试数据库连接 属性->点击数据源->测试连接->输入用户名密码

测试连接

8.添加运行配置 服务->加号->运行配置类型->spring boot。

9.启动服务,测试接口。

常规业务测试

quartz定时模块测试:(确保tb_task中没有数据)


二、JSR合并至common模块

1.common模块导入jsr所需依赖

common的pom内容如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>cn.bit</groupId><artifactId>Pro2_2</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>common</artifactId><packaging>jar</packaging><name>common</name><url>http://maven.apache.org</url><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>3.8.1</version><scope>test</scope></dependency><!-- JSR --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency></dependencies>
</project>

2.在微服务项目的公共模块common下创建jsr包以及子包,具体如下:

其中annotation存放自定义校验注解,config存放配置类,constant存放默认前缀以及默认属性值,enums存放枚举,pojo存放封装格式的中间类,validator存放实际校验类。

3.先定义默认值常量,防止魔法值的出现。

定义各文件默认大小、命名、拓展名等配置常量。定义yml文件使用前缀。

package cn.bit.common.jsr.constant;import lombok.Getter;@Getter
public class DefaultValue {//Stringpublic static final String DEFAULT_ANY_REGEX = ".*";public static final String DEFAULT_PHONE_REGEX = "^1[3-9]\\d{9}$";public static final String DEFAULT_EMAIL_REGEX = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";//Filepublic static final Long DEFAULT_ANY_FILE_MAX_SIZE = 5L*1024*1024*1024; //5Gpublic static final Long DEFAULT_IMAGE_FILE_MAX_SIZE = 50L * 1024 * 1024; //50Mpublic static final Long DEFAULT_VIDEO_FILE_MAX_SIZE = 1024L * 1024 * 1024; //1Gpublic static final Long DEFAULT_AUDIO_FILE_MAX_SIZE = 100L * 1024 * 1024; //100Mpublic static final Long DEFAULT_TEXT_FILE_MAX_SIZE = 50L * 1024 * 1024; //50Mpublic static final Long DEFAULT_COMPRESSED_FILE_MAX_SIZE = 1024L * 1024 * 1024; //1Gpublic static final String[] DEFAULT_ANY_FILE_EXTENSIONS = new String[]{};public static final String[] DEFAULT_IMAGE_FILE_EXTENSIONS = new String[]{"jpg", "jpeg", "png", "gif"};public static final String[] DEFAULT_VIDEO_FILE_EXTENSIONS =  new String[]{"mp4", "avi", "mkv", "mov"};public static final String[] DEFAULT_AUDIO_FILE_EXTENSIONS = new String[]{"mp3", "wav", "aac"};public static final String[] DEFAULT_TEXT_FILE_EXTENSIONS = new String[]{"txt", "pdf", "docx", "doc"};public static final String[] DEFAULT_COMPRESSED_FILE_EXTENSIONS = new String[]{"zip", "rar", "7z"};public static final Integer DEFAULT_FILE_MAX_NAME_LENGTH = 255;public static final String DEFAULT_FILE_NAME_PATTERN = "^[a-zA-Z0-9_.-]+$";}
package cn.bit.common.jsr.constant;public class Prefix {//String Enumpublic static final String ANY_STRING_PREFIX = "any-string";public static final String PHONE_STRING_PREFIX = "phone-string";public static final String EMAIL_STRING_PREFIX = "email-string";//File Enumpublic static final String ANY_FILE_PREFIX = "any-file";public static final String IMAGE_FILE_PREFIX= "image-file";public static final String VIDEO_FILE_PREFIX = "video-file";public static final String AUDIO_FILE_PREFIX = "audio-file";public static final String TEXT_FILE_PREFIX = "text-file";public static final String COMPRESSED_FILE_PREFIX = "compressed-file";
}

4.枚举类

package cn.bit.common.jsr.enums;import cn.bit.common.jsr.constant.Prefix;
import lombok.AllArgsConstructor;
import lombok.Getter;@Getter
@AllArgsConstructor
public enum FileEnum {ANY_FILE(Prefix.ANY_FILE_PREFIX), // 任意文件IMAGE_FILE(Prefix.IMAGE_FILE_PREFIX), // 图片文件VIDEO_FILE(Prefix.VIDEO_FILE_PREFIX), // 视频文件AUDIO_FILE(Prefix.AUDIO_FILE_PREFIX), // 音频文件TEXT_FILE(Prefix.TEXT_FILE_PREFIX), // 文本文件COMPRESSED_FILE(Prefix.COMPRESSED_FILE_PREFIX); // 压缩包文件private final String prefix; // 文件类型前缀
}
package cn.bit.common.jsr.enums;import cn.bit.common.jsr.constant.Prefix;
import lombok.AllArgsConstructor;
import lombok.Getter;@AllArgsConstructor
@Getter
public enum StringEnum {ANY_STRING(Prefix.ANY_STRING_PREFIX),PHONE_STRING(Prefix.PHONE_STRING_PREFIX),EMAIL_STRING(Prefix.EMAIL_STRING_PREFIX),;private final String prefix;
}

5.封装数据类

package cn.bit.common.jsr.pojo;import lombok.AllArgsConstructor;
import lombok.Data;@Data
@AllArgsConstructor
public class FileLimit {private Long maxSize;private Integer maxFileNameLength;private String[] allowedExtensions;private String fileNameRegex;
}

6.新增系统异常类

package cn.bit.common.exception;import lombok.NoArgsConstructor;@NoArgsConstructor
public class SysException extends RuntimeException {public SysException(String message) {super(message);}public SysException(Throwable cause) {super(cause);}public SysException(String message, Throwable cause) {super(message, cause);}public SysException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {super(message, cause, enableSuppression, writableStackTrace);}}

7.配置类,有对应前缀对应配置时使用配置,否则使用默认配置。

package cn.bit.common.jsr.config;import cn.bit.common.exception.SysException;
import cn.bit.common.jsr.constant.DefaultValue;
import cn.bit.common.jsr.constant.Prefix;
import cn.bit.common.jsr.enums.FileEnum;
import cn.bit.common.jsr.enums.StringEnum;
import cn.bit.common.jsr.pojo.FileLimit;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;@Slf4j
@Component
@ConfigurationProperties(prefix = "jsr")
@Data
public class JSRConfig {private Map<String, String> regexMap;private Map<String, String> defaultRegexMap=new HashMap<>();private Map<String, FileLimit> fileLimitMap;private Map<String, FileLimit> defaultFileLimitMap=new HashMap<>();@PostConstructpublic void init() {//初始化字符串正则规则this.defaultRegexMap.put(Prefix.ANY_STRING_PREFIX,DefaultValue.DEFAULT_ANY_REGEX);this.defaultRegexMap.put(Prefix.PHONE_STRING_PREFIX,DefaultValue.DEFAULT_PHONE_REGEX);this.defaultRegexMap.put(Prefix.EMAIL_STRING_PREFIX,DefaultValue.DEFAULT_EMAIL_REGEX);// 初始化文件限制映射this.defaultFileLimitMap.put(Prefix.ANY_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_ANY_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_ANY_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));this.defaultFileLimitMap.put(Prefix.IMAGE_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_IMAGE_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_IMAGE_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));this.defaultFileLimitMap.put(Prefix.VIDEO_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_VIDEO_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_VIDEO_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));this.defaultFileLimitMap.put(Prefix.AUDIO_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_AUDIO_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_AUDIO_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));this.defaultFileLimitMap.put(Prefix.TEXT_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_TEXT_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_TEXT_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));this.defaultFileLimitMap.put(Prefix.COMPRESSED_FILE_PREFIX, new FileLimit(DefaultValue.DEFAULT_COMPRESSED_FILE_MAX_SIZE, DefaultValue.DEFAULT_FILE_MAX_NAME_LENGTH, DefaultValue.DEFAULT_COMPRESSED_FILE_EXTENSIONS, DefaultValue.DEFAULT_FILE_NAME_PATTERN));}public String getRegex(StringEnum stringEnum) {if(regexMap==null || regexMap.get(stringEnum.getPrefix())==null){String defaultRegex = defaultRegexMap.get(stringEnum.getPrefix());if (defaultRegex != null) {log.warn("{} is null, use default regex", stringEnum.name());return defaultRegex;}elsethrow new SysException("regex analyze error: "+ stringEnum.name());}elsereturn regexMap.get(stringEnum.getPrefix());}public FileLimit getFileLimit(FileEnum fileEnum) {if(fileLimitMap==null || fileLimitMap.get(fileEnum.getPrefix())==null){FileLimit defaultFileLimit = defaultFileLimitMap.get(fileEnum.getPrefix());if (defaultFileLimit != null) {log.warn("{} is null, use default file limit", fileEnum.name());return defaultFileLimit;}elsethrow new SysException("file limit analyze error: "+fileEnum.name());}elsereturn fileLimitMap.get(fileEnum.getPrefix());}
}

8.自定义注解,默认优先使用枚举

package cn.bit.common.jsr.annotation;import cn.bit.common.jsr.enums.FileEnum;
import cn.bit.common.jsr.validator.FileValidator;import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = FileValidator.class)
public @interface ValidFile {String message() default "Invalid file";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};boolean useEnum() default true;//默认优先使用枚举FileEnum fileEnum() default FileEnum.ANY_FILE;long maxSize() default 5L*1024*1024*1024; // 文件最大字节大小String[] allowedExtensions() default {}; // 允许的文件扩展名int maxFileNameLength() default 255; // 文件名的最大长度String fileNameRegex() default "^[a-zA-Z0-9_.-]+$"; // 文件名的正则表达式规则
}
package cn.bit.common.jsr.annotation;import cn.bit.common.jsr.enums.StringEnum;
import cn.bit.common.jsr.validator.StringValidator;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;@Target({FIELD,PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = StringValidator.class)
public @interface ValidString {boolean useEnum() default true;//默认优先使用枚举StringEnum regexEnum() default StringEnum.ANY_STRING;String regex() default ".*";String message() default "invalid string";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};}

9.校验类

package cn.bit.common.jsr.validator;import cn.bit.common.jsr.annotation.ValidFile;
import cn.bit.common.jsr.config.JSRConfig;
import cn.bit.common.jsr.enums.FileEnum;
import cn.bit.common.jsr.pojo.FileLimit;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.regex.Pattern;@Slf4j
@Component
@RequiredArgsConstructor
public class FileValidator implements ConstraintValidator<ValidFile, MultipartFile> {@NonNullprivate JSRConfig jsrConfig;private long maxSize;private String[] allowedExtensions;private int maxFileNameLength;private Pattern fileNamePattern;@Overridepublic void initialize(ValidFile constraintAnnotation) {boolean useEnum = constraintAnnotation.useEnum();if(useEnum) {FileEnum fileEnum = constraintAnnotation.fileEnum();if(fileEnum==FileEnum.ANY_FILE)log.warn("use any file");FileLimit fileLimit = jsrConfig.getFileLimit(fileEnum);maxSize = fileLimit.getMaxSize();allowedExtensions = fileLimit.getAllowedExtensions();maxFileNameLength = fileLimit.getMaxFileNameLength();fileNamePattern = Pattern.compile(fileLimit.getFileNameRegex());}else {maxSize = constraintAnnotation.maxSize();allowedExtensions = constraintAnnotation.allowedExtensions();maxFileNameLength = constraintAnnotation.maxFileNameLength();fileNamePattern = Pattern.compile(constraintAnnotation.fileNameRegex());}}@Overridepublic boolean isValid(MultipartFile file, ConstraintValidatorContext context) {if (file == null || file.isEmpty()) {return true; // 如果文件为空,认为是无效的}// 校验文件大小if (file.getSize() > maxSize) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("File size must be less than " + maxSize + " bytes").addConstraintViolation();return false;}// 校验文件扩展名if (allowedExtensions.length > 0) {String fileName = file.getOriginalFilename();if (fileName == null) {return false;}String fileExtension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();if (!Arrays.asList(allowedExtensions).contains(fileExtension)) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("File extension must be one of " + Arrays.toString(allowedExtensions)).addConstraintViolation();return false;}}// 校验文件名长度String fileName = file.getOriginalFilename();if (fileName != null && fileName.length() > maxFileNameLength) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("File name length must be less than " + maxFileNameLength + " characters").addConstraintViolation();return false;}// 校验文件名是否符合命名规范if (fileName != null && !fileNamePattern.matcher(fileName).matches()) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate("File name must match the pattern: " + fileNamePattern.pattern()).addConstraintViolation();return false;}return true;}
}
package cn.bit.common.jsr.validator;import cn.bit.common.jsr.annotation.ValidString;
import cn.bit.common.jsr.config.JSRConfig;
import cn.bit.common.jsr.enums.StringEnum;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;@Slf4j
@Component
@RequiredArgsConstructor
public class StringValidator implements ConstraintValidator<ValidString, String> {@NonNullprivate JSRConfig jsrConfig;private String regex;@Overridepublic void initialize(ValidString constraintAnnotation) {boolean useEnum = constraintAnnotation.useEnum();if(useEnum) {StringEnum stringEnum = constraintAnnotation.regexEnum();if(stringEnum == StringEnum.ANY_STRING)log.warn("use any string");regex = jsrConfig.getRegex(stringEnum);}elseregex = constraintAnnotation.regex();}@Overridepublic boolean isValid(String str, ConstraintValidatorContext context) {Pattern pattern = Pattern.compile(regex);if(!pattern.matcher(str).matches()){context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate(str+"不符合正则表达式:"+regex).addConstraintViolation();;return false;}return true;}
}

10.新增异常处理方法,即修改原有GlobalExceptionHandler类

package cn.bit.common.handler;import cn.bit.common.exception.BizException;
import cn.bit.common.exception.SysException;
import cn.bit.common.pojo.vo.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;import java.nio.file.AccessDeniedException;
import java.util.List;@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 全局异常.* @param e the e* @return R*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public R handleGlobalException(Exception e) {log.error("全局异常信息 ex={}", e.getMessage(), e);return R.failed(e.getLocalizedMessage());}/*** AccessDeniedException* @param e the e* @return R*/@ExceptionHandler(AccessDeniedException.class)@ResponseStatus(HttpStatus.FORBIDDEN)public R handleAccessDeniedException(AccessDeniedException e) {log.error("拒绝授权异常信息 ex={}", e.getLocalizedMessage(),e);return R.failed(e.getLocalizedMessage());}/*** 服务器异常* @param e the e* @return R*/@ExceptionHandler(SysException.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public R handleSysException(SysException e) {log.error("服务器异常信息 ex={}", e.getMessage(), e);return R.failed(e.getLocalizedMessage());}/*** 业务处理类* @param e the e* @return R*/@ExceptionHandler({ BizException.class })@ResponseStatus(HttpStatus.BAD_REQUEST)public R bizExceptionHandler(BizException e) {log.warn("业务处理异常,ex = {}", e.getMessage());return R.failed(e.getMessage());}/*** validation Exception* @param e the e* @return R*/@ExceptionHandler({ MethodArgumentNotValidException.class})@ResponseStatus(HttpStatus.BAD_REQUEST)public R handleBodyValidException(MethodArgumentNotValidException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();StringBuilder errorMsg = new StringBuilder();fieldErrors.forEach(fieldError -> {errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(" ");});log.warn("参数绑定异常,ex = {}",errorMsg);return R.failed(errorMsg.toString());}/*** validation Exception (以form-data形式传参)* @param e the e* @return R*/@ExceptionHandler({ BindException.class})@ResponseStatus(HttpStatus.BAD_REQUEST)public R bindExceptionHandler(BindException e) {List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();StringBuilder errorMsg = new StringBuilder();fieldErrors.forEach(fieldError -> {errorMsg.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("\n");});log.warn("参数绑定异常(form-data),ex = {}",errorMsg);return R.failed(errorMsg.toString());}
}

12.common模块创建spring.factories使jsr配置类以及异常处理类的bean能够被其他服务获取。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\cn.bit.common.jsr.config.JSRConfig,\cn.bit.common.handler.GlobalExceptionHandler

13.修改order-service的controller作为测试接口

package cn.bit.orderservice.controller;import cn.bit.common.jsr.annotation.ValidFile;
import cn.bit.common.jsr.enums.FileEnum;
import cn.bit.common.pojo.po.UserPO;
import cn.bit.common.pojo.vo.OrderInfoVO;
import cn.bit.common.pojo.vo.R;
import cn.bit.orderservice.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.validation.Valid;
import java.io.IOException;@Slf4j
@RestController
@RequestMapping("/order")
@CrossOrigin(origins = "*")
@Validated
public class OrderController {@Autowiredprivate OrderService orderService;@GetMapping("/test/{id}")public String test(@PathVariable Integer id) {System.out.println(id);return id.toString();}@PostMapping("/jsr")public String jsr(@RequestBody @Valid UserPO user) {return "ok";}@PostMapping("/upload")public String uploadImage(@RequestParam("file") @Valid @ValidFile(fileEnum = FileEnum.IMAGE_FILE) MultipartFile file) throws IOException {return file.getOriginalFilename();}@GetMapping("/info/{id}")public R getOrderInfoById(@PathVariable Integer id, @RequestHeader(value = "source",required = false) String source) {log.debug("debug");log.info("info");log.warn("warning");System.out.println(source);OrderInfoVO orderInfoVO = orderService.getOrderInfoById(id);if (orderInfoVO == null) {return R.failed("订单不存在");}elsereturn R.ok(orderInfoVO);}
}

14.项目根目录创建.html文件夹用于存放后续测试前端页面,并创建jsr.html内容如下:
 

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件上传示例</title>
</head>
<body>
<h1>文件上传</h1>
<form id="uploadForm"><input type="file" id="fileInput" name="file" required><button type="submit">上传</button>
</form><p id="responseMessage"></p><script>document.getElementById('uploadForm').addEventListener('submit', function(event) {event.preventDefault(); // 阻止表单默认提交行为const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (file) {const formData = new FormData();formData.append('file', file);fetch('http://localhost:1235/order/upload', {method: 'POST',body: formData}).then(response => response.text()).then(data => {document.getElementById('responseMessage').textContent = '文件上传成功: ' + data;}).catch(error => {document.getElementById('responseMessage').textContent = '文件上传失败: ' + error.message;});} else {document.getElementById('responseMessage').textContent = '请选择一个文件';}});
</script>
</body>
</html>

三、测试

启动order-service

 打开前端页面:

选择上传一个文档(能够看到返回错误码,以及报错信息文件拓展名错误,上传成功是因为前端是ai生成的,后端没有设置响应码状态所以误判为成功)

选择上传图片(显示文件命名不符合规范)

选择上传正确格式图片,成功返回上传的图片名称。


最后:

规范log目录,使非源码文件夹命名以.开头。

因为最近老师push导致2-2在2-1更新一两周才写出来,后续计划按序做oss的整合、Java POI库使用、rabbitmq使用三章。还请多多支持!

相关文章:

从零搭建微服务项目Pro(第2-2章——JSR303自定义文件校验+整合至微服务公共模块)

前言&#xff1a; JSR 303&#xff0c;即 Bean Validation&#xff0c;是 Java EE 6 中的一项子规范&#xff0c;旨在为 Java Bean 提供一种标准化的数据验证机制。它通过注解的方式&#xff0c;允许开发者在 Java 类的字段或方法上直接定义验证规则&#xff0c;从而将验证逻辑…...

如何用URDF文件构建机械手模型并与MoveIt集成

机械手URDF文件的编写 我们用urdf文件来描述我们的机械手的外观以及物理性能。这里为了简便&#xff0c;就只用了基本的圆柱、立方体了。追求美观的朋友&#xff0c;还可以用dae文件来描述机械手的外形。 import re def remove_comments(text):pattern r<!--(.*?)-->…...

uv pip install -r requirements.txt-报错,python版本过低

升级Python版本&#xff08;推荐&#xff09; browser-use0.1.40 需要 Python ≥3.11&#xff0c;但你的环境是 Python 3.10.12。升级Python版本是最直接的解决方案&#xff1a; 安装Python 3.11&#xff1a; 使用 pyenv&#xff08;Linux/macOS&#xff09;&#xff1a;pyenv…...

【训练细节解读】文本智能混合分块(Mixtures of Text Chunking,MoC)引领RAG进入多粒度感知智能分块阶段

喜欢本文可以在主页订阅专栏哟 核心创新&#xff1a;双重评估指标与混合分块架构&#xff1a; 第一章&#xff1a;检索增强生成&#xff08;RAG&#xff09;技术演进与分块挑战 1.1 RAG架构的核心演变 检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09…...

招聘信息|基于SprinBoot+vue的招聘信息管理系统(源码+数据库+文档)

招聘信息管理系统 目录 基于SprinBootvue的招聘信息管理系统 一、前言 二、系统设计 三、系统功能设计 5.1系统功能模块 5.2管理员功能模块 5.3企业后台管理模块 5.4用户后台管理模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、…...

HCIA-AI人工智能笔记1:大模型技术演进与发展历程

一、大模型发展的技术演进图谱 timelinetitle 大模型发展关键里程碑1958 : 感知机模型诞生&#xff08;Frank Rosenblatt&#xff09;1986 : BP反向传播算法&#xff08;Rumelhart&#xff09;2012 : AlexNet开启深度学习时代2017 : Transformer架构提出&#xff08;《Attenti…...

躲藏博弈中的策略优化:整合历史数据、概率论与博弈论

躲藏博弈中的策略优化&#xff1a;整合历史数据、概率论与博弈论 一、引言 躲藏博弈(Hiding Games)作为一类特殊的博弈模型&#xff0c;广泛存在于军事对抗、网络安全、商业竞争甚至日常生活中。其核心在于一方(躲藏者)试图避免被另一方(寻找者)发现&#xff0c;双方各自选择…...

C语言,记录一次局部变量被意外修改的问题

背景:单片机开发过程中,我在函数体内(begin_face_record)定义了一个局部变量data_length,在使用的时候,该局部变量一直别改变,每次调用其他函数,例如c库里面的函数memcpy,不知什么情况data_length值就会被改变。 1、源码分析 void main(void) { init_gpio();init_fa…...

机器学习——正则化、欠拟合、过拟合、学习曲线

过拟合&#xff08;overfitting&#xff09;:模型只能拟合训练数据的状态。即过度训练。 避免过拟合的几种方法&#xff1a; ①增加全部训练数据的数量&#xff08;最为有效的方式&#xff09; ②使用简单的模型&#xff08;简单的模型学不够&#xff0c;复杂的模型学的太多&am…...

在微信小程序或前端开发中,picker 和 select 都是用户交互中用于选择的组件,但它们在功能、设计和使用场景上有一定的区别

在微信小程序或前端开发中&#xff0c;picker 和 select 都是用户交互中用于选择的组件&#xff0c;但它们在功能、设计和使用场景上有一定的区别。 1. picker 的特点 描述&#xff1a; picker 是微信小程序中的原生组件&#xff0c;通常用于选择单项或多项值&#xff0c;如时…...

向量数据库对比以及Chroma操作

一、向量数据库与传统类型数据库 向量数据库&#xff08;Vector Storage Engine&#xff09;与传统类型的数据库如关系型数据库&#xff08;MySQL&#xff09;、文档型数据库&#xff08;MongoDB&#xff09;、键值存储&#xff08;Redis&#xff09;、全文搜索引擎&#xff0…...

Python Matplotlib面试题精选及参考答案

绘制函数 y2x5 在区间 [1,10] 的折线图&#xff0c;设置标题和坐标轴标签 要绘制函数 y 2x 5 在区间 [1, 10] 的折线图&#xff0c;并设置标题和坐标轴标签&#xff0c;可借助 Python 的 matplotlib 库来实现。以下是详细的实现步骤与代码示例。 首先&#xff0c;要导入 mat…...

FiddlerScript学习笔记

参考官方文档&#xff1a;https://www.fiddlerbook.com/fiddler/dev/scriptsamples.asp json // 反序列化 static function jsonDecode(str : String){return Fiddler.WebFormats.JSON.JsonDecode(str).JSONObject; } // 序列化 static function jsonEncode(jsonObject : Obje…...

正点原子[第三期]Arm(iMX6U)Linux移植学习笔记-5.1 uboot顶层Makefile分析-VSCode工程创建

前言&#xff1a; 本文是根据哔哩哔哩网站上“Arm(iMX6U)Linux系统移植和根文件系统构键篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。 引用&#xff1a; …...

Linux内核IPoIB驱动中的RSS队列选择机制分析

摘要 本文深入分析了Linux内核中InfiniBand over Ethernet(IPoIB)驱动程序的发送队列选择函数ipoib_select_queue_sw_rss的实现。该函数通过复杂的条件编译和逻辑分支,实现了基于软件的接收端扩展(RSS)功能,确保网络流量在多队列环境下的高效分发,提升网络性能和吞吐量…...

js数组遍历十种方法

在JavaScript中&#xff0c;数组遍历可以有多种方法。以下列举了10种常用的数组遍历方式&#xff1a; For 循环 let array [1, 2, 3, 4, 5]; for (let i 0; i < array.length; i) {console.log(array[i]); }For-of 循环&#xff08;ES6&#xff09; let array [1, 2, 3, …...

OTP单片机调试工具之—单线数据编码

OTP单片机调试工具在实现过程中离不开单线数据的传输&#xff0c;那么使用哪一种方式的数据编码会比较好呢&#xff1f; 我所了解的主要有以下三种&#xff1a; 1.UART&#xff08;串口&#xff09;&#xff0c;这种方式在单片机和pc之间进行传输都非常常见&#xff0c;效率比较…...

SWPU 2022 新生赛

webdog1__start if (isset($_GET[web])) {$first$_GET[web];if ($firstmd5($first)) md5 自等 web0e215962017 &#xff08;md5后也是 0e) 登入后得到提示&#xff0c;robots.txt 访问 f14g.php 返回包里发现 hint > if (isset($_GET[get])){$get$_GET[get];if(!strs…...

Java 基础到进阶企业技巧(二)

在 Java 学习的旅程中&#xff0c;我们逐步探索了其丰富的知识体系&#xff0c;从基础的数据类型、字符串操作&#xff0c;到流程控制、运算符的运用&#xff0c;每一步都为我们构建强大的编程能力奠定基石。同时&#xff0c;了解这些知识在 Java 全栈开发中的应用场景&#xf…...

【后端】【django】【related_name】`related_name` 的作用

related_name 的作用 related_name 用于 Django 的 ForeignKey 和 OneToOneField&#xff0c;用于 反向访问 关联模型的数据。 默认情况下&#xff0c;Django 会自动生成一个反向关系的名字&#xff0c;但如果多个外键指向同一个模型&#xff0c;就可能发生命名冲突。related…...

Deepseek-R1大模型微调实战技术深度解析

一、Deepseek-R1架构特性与微调适配性分析 1.1 核心架构创新对微调的影响 Deepseek-R1基于Deepseek-V3-Base架构,通过MoE(Mixture-of-Experts)与MLA(Multi-Head Latent Attention)的协同设计,实现了参数规模与计算效率的平衡。其6710亿参数总量中,每个token仅激活37B参…...

Google最新生图模型Gemini-2.0-Flash-Exp免费用

Google发布新生图模型 Google释放出最新生图模型&#xff0c;在发布说明中提到&#xff1a; 2025年3月12日 在 Gemini-2.0-Flash-Exp 中发布原生图像输出功能 Gemini 2.0 Flash Experimental 模型发布&#xff0c;支持原生图像输出功能。开发者能够使用 Gemini 进行图像输出和…...

leecode695.岛屿的最大面积

跟求岛屿数量的题目差不多&#xff0c;依旧是深度搜索或者广度搜索问题 class Solution { private:int maxAreaOfIsland(vector<vector<int>>& grid,vector<vector<bool>>& visited,int x,int y){if(x<0||x>grid.size()||y<0||y>…...

助力字体管理,规避设计卡顿的得力工具

在设计领域&#xff0c;字体看似平常&#xff0c;却常常在关键时刻“掉链子”&#xff0c;让设计师们头疼不已。面对海量字体库&#xff0c;找到心仪那款宛如大海捞针&#xff0c;字体安装过多还会造成软件卡顿&#xff0c;这些麻烦事儿&#xff0c;频繁与字体打交道的朋友肯定…...

零基础上手Python数据分析 (4):Python数据结构精讲 - 列表、元组、字典、集合

写在前面 回顾一下,在之前的博客中,我们学习了 Python 的基本数据类型(数值、字符串、布尔值)和核心语法(运算符、变量、流程控制、函数、模块)。 现在,我们已经掌握了 Python 编程的基础知识。 接下来,我们将进入数据分析的关键环节: 数据组织。 在数据分析中,数据…...

数统院复试来啦,西电数学与统计学院—考研录取情况

4西安电子科技大学—数学与统计学院—考研录取统计 01、数学与统计学院各个方向 02、24数学与统计学院近三年复试分数线对比 数统院24年院线相对于23年院线增加高达30分&#xff0c;确实增长浮动比较高&#xff0c;接近30分的水平&#xff0c;因此大家更需要好好去努力&#xf…...

Windows功能之FTP服务器搭建

一、创作背景 之前有用linux系统搭建过ftp服务器&#xff0c;最近想着用windows系统也顺便搭建一个&#xff0c;看网上有第三方服务软件一键部署&#xff0c;记得windows可以不借助第三方软件就可以搭建&#xff0c;就想顺便操作试试&#xff0c;结果老是连接不上&#xff0c;费…...

leetcode hot100普通动态规划/基础DP

1️⃣1️⃣ 普通动态规划&#xff08;基础 DP&#xff09; 70. 爬楼梯 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 题解: 动态规划Dynamic Programming ,在观察动态中找到如何规划解题的步骤…...

基于Python的天气预报数据可视化分析系统-Flask+html

开发语言&#xff1a;Python框架&#xff1a;flaskPython版本&#xff1a;python3.8数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat11开发软件&#xff1a;PyCharm 系统展示 系统登录 可视化界面 天气地图 天气分析 历史天气 用户管理 摘要 本文介绍了基于大数据…...

【鸿蒙开发】Hi3861学习笔记-Visual Studio Code安装(New)

00. 目录 文章目录 00. 目录01. Visual Studio Code概述02. Visual Studio Code下载03. Visual Studio Code安装04. Visual Studio Code插件05. 附录 01. Visual Studio Code概述 vscode是一种简化且高效的代码编辑器&#xff0c;同时支持诸如调试&#xff0c;任务执行和版本管…...