前言:
JSR 303,即 Bean Validation,是 Java EE 6 中的一项子规范,旨在为 Java Bean 提供一种标准化的数据验证机制。它通过注解的方式,允许开发者在 Java 类的字段或方法上直接定义验证规则,从而将验证逻辑从业务代码中分离出来,提升代码的可维护性和灵活性。
核心功能与特点
-
注解驱动 :JSR 303 提供了一系列内置的验证注解,如
@NotNull
、@Size
、@Min
、@Max
、@Email
等,开发者可以直接在字段上使用这些注解来定义验证规则125。 -
分组校验:支持根据不同的业务场景(如新增、更新)对字段进行分组校验。例如,新增时某些字段可以为空,而更新时则必须不为空135。
-
自定义约束:除了内置的注解,JSR 303 还允许开发者定义自定义的验证注解和逻辑,以满足特定的业务需求135。
-
统一异常处理:通过与 Spring 等框架的集成,JSR 303 可以方便地处理验证失败的情况,并将错误信息返回给前端356。
其主要用于表单数据的后端验证,确保数据的合法性。尽管前端通常也会进行验证,但后端验证是必不可少的,因为前端验证容易被绕过(如通过 Postman 等工具直接发送请求)。通过 JSR 303,开发者可以在控制器中方便地对表单提交的数据进行验证,并将验证结果返回给前端。
本章在上章基础上对原有自定义字符串校验注解进行适当修改,添加自定义文件验证注解,校验前端传输的文件大小、文件名、拓展名 ,优化原处理逻辑。并将其整合至微服务公共模块 由各子服务模块共享,并统一进行异常处理 。同时支持配置文件配置注解规范、支持枚举统一配置注解或各项分别配置。
本章使用的微服务项目博客链接如下,内有对应项目源码。
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如下:


sql
create table tb_order
(
id int auto_increment
primary 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
);
sql
create table tb_task
(
id int auto_increment
primary 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
);
sql
create table tb_task_log
(
id int auto_increment
primary 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
);
sql
create table tb_user
(
id int auto_increment
primary 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 account
unique (account),
constraint email
unique (email),
constraint username
unique (username)
);
测试数据库连接 属性->点击数据源->测试连接->输入用户名密码

测试连接

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

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

常规业务测试

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



二、JSR合并至common模块
1.common模块导入jsr所需依赖

common的pom内容如下:
XML
<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文件使用前缀。
java
package cn.bit.common.jsr.constant;
import lombok.Getter;
@Getter
public class DefaultValue {
//String
public 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}$";
//File
public static final Long DEFAULT_ANY_FILE_MAX_SIZE = 5L*1024*1024*1024; //5G
public static final Long DEFAULT_IMAGE_FILE_MAX_SIZE = 50L * 1024 * 1024; //50M
public static final Long DEFAULT_VIDEO_FILE_MAX_SIZE = 1024L * 1024 * 1024; //1G
public static final Long DEFAULT_AUDIO_FILE_MAX_SIZE = 100L * 1024 * 1024; //100M
public static final Long DEFAULT_TEXT_FILE_MAX_SIZE = 50L * 1024 * 1024; //50M
public static final Long DEFAULT_COMPRESSED_FILE_MAX_SIZE = 1024L * 1024 * 1024; //1G
public 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_.-]+$";
}
java
package cn.bit.common.jsr.constant;
public class Prefix {
//String Enum
public 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 Enum
public 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.枚举类
java
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; // 文件类型前缀
}
java
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.封装数据类
java
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.新增系统异常类

java
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.配置类,有对应前缀对应配置时使用配置,否则使用默认配置。
java
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<>();
@PostConstruct
public 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;
}
else
throw new SysException("regex analyze error: "+ stringEnum.name());
}
else
return 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;
}
else
throw new SysException("file limit analyze error: "+fileEnum.name());
}
else
return fileLimitMap.get(fileEnum.getPrefix());
}
}
8.自定义注解,默认优先使用枚举
java
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_.-]+$"; // 文件名的正则表达式规则
}
java
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.校验类
java
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> {
@NonNull
private JSRConfig jsrConfig;
private long maxSize;
private String[] allowedExtensions;
private int maxFileNameLength;
private Pattern fileNamePattern;
@Override
public 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());
}
}
@Override
public 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;
}
}
java
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> {
@NonNull
private JSRConfig jsrConfig;
private String regex;
@Override
public 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);
}
else
regex = constraintAnnotation.regex();
}
@Override
public 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类
java
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能够被其他服务获取。

XML
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.bit.common.jsr.config.JSRConfig,\
cn.bit.common.handler.GlobalExceptionHandler
13.修改order-service的controller作为测试接口
java
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 {
@Autowired
private 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("订单不存在");
}
else
return R.ok(orderInfoVO);
}
}
14.项目根目录创建.html文件夹用于存放后续测试前端页面,并创建jsr.html内容如下:
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使用三章。还请多多支持!