从零搭建微服务项目Pro(第2-2章——JSR303自定义文件校验+整合至微服务公共模块)
前言:
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,开发者可以在控制器中方便地对表单提交的数据进行验证,并将验证结果返回给前端。
本章在上章基础上对原有自定义字符串校验注解进行适当修改,添加自定义文件验证注解,校验前端传输的文件大小、文件名、拓展名,优化原处理逻辑。并将其整合至微服务公共模块由各子服务模块共享,并统一进行异常处理。同时支持配置文件配置注解规范、支持枚举统一配置注解或各项分别配置。
本章使用的微服务项目博客链接如下,内有对应项目源码。
从零搭建微服务项目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自定义文件校验+整合至微服务公共模块)
前言: JSR 303,即 Bean Validation,是 Java EE 6 中的一项子规范,旨在为 Java Bean 提供一种标准化的数据验证机制。它通过注解的方式,允许开发者在 Java 类的字段或方法上直接定义验证规则,从而将验证逻辑…...
Python 实现大文件的高并发下载
项目背景 基于一个 scrapy-redis 搭建的分布式系统,所有item都通过重写 pipeline 存储到 redis 的 list 中。这里我通过代码演示如何基于线程池 协程实现对 item 的中文件下载。 Item 结构 目的是为了下载 item 中 attachments 保存的附件内容。 {"crawl_tim…...
【论文笔记】Contrastive Learning for Compact Single Image Dehazing(AECR-Net)
文章目录 问题创新网络主要贡献Autoencoder-like Dehazing NetworkAdaptive Mixup for Feature PreservingDynamic Feature Enhancement1. 可变形卷积的使用2. 扩展感受野3. 减少网格伪影4. 融合空间结构信息 Contrastive Regularization1. 核心思想2. 正样本对和负样本对的构建…...
Java vs Go:SaaS 系统架构选型解析与最佳实践
在构建 SaaS(Software as a Service)系统时,选用合适的技术栈至关重要。Java 和 Go 是当今最受欢迎的后端开发语言之一,各自有其优势和适用场景。那么,SaaS 系统开发应该选择 Java 还是 Go?本文将从多个维度…...
OpenEuler-22.03-LTS上利用Ansible轻松部署MySQL 5.7
一、需求 使用ansible自动化部署mysql二进制部署mysql部署mysql并创建JDBC用户 二、环境信息 本文涉及的代码,配置文件地址: 链接:百度网盘 请输入提取码 提取码:1g6y 软件名称版本备注Ansible2.9.27All modules — Ansible Doc…...
vscode编译器的一些使用问题
目录 解决pip不可用问题 检查VSCode的终端配置 解决pip不可用问题 eg: C:\Users\student>pip pip 不是内部或外部命令,也不是可运行的程序或批处理文件。 先找到系统环境变量 高级->环境变量 系统属性->Path 变量名随意,自己后续知道…...
解决Windows版Redis无法远程连接的问题
🌟 解决Windows版Redis无法远程连接的问题 在Windows系统下使用Redis时,很多用户会遇到无法远程连接的问题。尤其是在配置了Redis并尝试通过工具如RedisDesktopManager连接时,可能会报错“Cannot connect to ‘redisconnection’”。今天&am…...
MFC中使用Create或CreateDialog创建对话框失败,GetLastError错误码为1813(找不到映像文件中指定的资源类型)
文章目录 创建对话框失败示例、原因分析及解决方案示例代码错误原因解决方案 AFX_MANAGE_STATE(AfxGetStaticModuleState())作用一、功能1. 模块状态切换2. 自动状态恢复 二、为什么要用该函数?三、必须使用该宏的典型场景1. MFC 扩展 DLL(Extension DLL…...
std::invoke详解
基础介绍 c17版本引入了std::invoke特性,这是一个通用的调用包装器,可以统一调用: 普通函数成员函数函数对象Lambda表达式指向成员的指针 它的主要作用是提供一个统一的方式来调用各种可调用对象。 std::invoke依赖的头文件:#…...
【Rust】枚举和模式匹配——Rust语言基础14
文章目录 1. 枚举类型1.2. Option 枚举 2. match 控制流结构2.1. match 对绑定值的匹配2.2. Option<T> 的匹配2.3. 通配模式以及 _ 占位符 3. if let 控制流4. 小测试 1. 枚举类型 枚举(enumerations),也被称作 enums。枚举允许你通过…...
视频理解之Actionclip(论文宏观解读)
配合解读代码解读 1.研究背景 1. 视频行为识别的重要性 视频行为识别是视频理解领域的核心任务之一,旨在通过分析视频内容来识别和分类其中的人物行为或活动。这一任务在多个领域具有重要的应用价值,例如智能监控、人机交互、自动驾驶、医疗健康等。随…...
【论文精读】Deformable DETR:用于端到端目标检测可变形 Transformer
论文:DEFORMABLE DETR: DEFORMABLE TRANSFORMERS FOR END-TO-END OBJECT DETECTION 代码:Deformable-DETR 摘要 DETR 最近被提出用于消除目标检测中许多手工设计组件的需求,同时展示了良好的性能。然而,它存在收敛速度慢和特征空…...
Odoo18 Http鉴权+调用后端接口
最近在调研Odoo18,包括它的前后端原理、源码等。发现官方的开发文档并不十分实用,比如标题这种简单的实用需求,竟然浪费了一点时间,特此记录。 官方文档:External API — Odoo 18.0 documentation 前提:首…...
doris:SQL 方言兼容
提示 从 2.1 版本开始,Doris 可以支持多种 SQL 方言,如 Presto、Trino、Hive、PostgreSQL、Spark、Clickhouse 等等。通过这个功能,用户可以直接使用对应的 SQL 方言查询 Doris 中的数据,方便用户将原先的业务平滑的迁移到 Doris…...
Linux红帽:RHCSA认证知识讲解(六)创建、管理和删除本地用戶和组
Linux红帽:RHCSA认证知识讲解(六)创建、管理和删除本地用戶和组 前言一、用户和组概念用户类型对比表格主要组和补充组对比表格: 二、本地用户账户增删改查三、本地组账户 前言 上篇博客我们详细了解了从红帽和 DNF 软件仓库下载…...
【Repos系列】yum install nginx 是怎么从仓库中下载并安装的?
yum install nginx 是 YUM 包管理工具从配置的软件仓库中下载并安装软件包的核心操作。以下是其完整工作流程的详细步骤(结合缓存机制和依赖处理): 1. 隐式元数据同步(若缓存过期) 检查缓存有效性:…...
《JavaScript高级程序设计(第5版)》学习大纲
《JavaScript高级程序设计(第5版)》学习大纲 《JavaScript高级程序设计(第5版)》是JavaScript领域的经典“红宝书”,它从基础语法讲到高级特性,还包含浏览器环境和前端工程化等内容。本书2024年12月出版&a…...
基于微信小程序开发的宠物领养平台——代码解读
项目前端 一、项目的技术架构概况 一句话概括:该项目是基于微信小程序开发的宠物领养平台,采用原生小程序框架进行用户界面的构建,使用 wx.request 进行 API 请求,并通过 getApp() 和本地存储来管理全局状态和用户信息。 一&am…...
KICK第五课:Mac 系统下安装 Xcode 或 Clang
Mac 系统下安装 Xcode 或 Clang 详细指南 一、安装前的准备 确认系统版本 macOS 10.9 及以上版本支持 Xcode 和 Clang。若版本过低,需先升级系统。 了解工具区别 Xcode:苹果官方 IDE,包含完整开发环境、模拟器、调试工具等,适合…...
PHP语法基础
PHP语法基础 一,变量 在PHP中,变量是存储数据的容器,其灵活性和动态类型系统是PHP的核心特性之一。以下是PHP变量的详细解析,涵盖声明、作用域、类型转换及最佳实践: 1. 变量基础 声明与命名规则 无需显式声明类型&…...
嵌入式硬件: GPIO与二极管基础知识详解
1. 前言 在嵌入式系统和硬件开发中,GPIO(通用输入输出)是至关重要的控制方式,而二极管作为基础电子元件,广泛应用于信号整流、保护电路等。本文将从基础原理出发,深入解析GPIO的输入输出模式,包…...
母婴商城系统Springboot设计与实现
项目概述 《母婴商城系统Springboot》是一款基于Springboot框架开发的母婴类电商平台,旨在为母婴产品提供高效、便捷的在线购物体验。该系统功能全面,涵盖用户管理、商品分类、商品信息、商品资讯等核心模块,适合母婴电商企业或个人开发者快…...
Redis相关面试题
以下是150道Redis相关面试题: Redis基础概念 1. Redis是什么? Redis是一个开源的、基于内存的高性能键值存储数据库,常用于缓存、消息队列等场景。 2. Redis的特点有哪些? • 高性能,读写速度快。 • 支持多种数据…...
ArcGIS助力水文分析:数据处理、地图制作与流域特征提取
在水文水环境保护中,对于信息的采集、处理和分析是关键步骤。水文水环境及其相关数据均具有空间分布特征,传统的方法难以发挥作用。地理信息系统(GIS)强大的空间数据管理和分析功能,在空间信息处理上有独到的优势&…...
docker桌面版启动redis,解决无法连接
docker run -d --name redis -p 6379:6379 -v E:\2\redis\redis.conf:/usr/local/etc/redis/redis.conf redis redis-server /usr/local/etc/redis/redis.conf 在本地创建一个目录,里面有个redis.conf文件,内容如下,启动时绑定这个配置文件目…...
Vue 项目中 CDN 引入的利弊及解决方案
在Vue项目中,引入到工程中的所有js、css文件,编译时都会被打包进vendor.js,浏览器在加载该文件之后才能开始显示首屏。若是引入的库众多,那么vendor.js文件体积将会相当的大,影响首屏的体验。通过调试发送时间主要消耗…...
【QT】】qcustomplot的使用
1.下载并添加qcustomplot.c和qcustomplot.h文件 拖动一个Widget,提升为qcustomplot 成功后是这样的, 改第三行:greaterThan(QT_MAJOR_VERSION, 4): QT widgets printsupport 编译,不报错,出现带坐标轴的界面&#…...
第三周日志-web(2)
原本计划的web不是这个,但是b站上一个大佬讲web做到了连我都能听懂,不趁热打铁学一学记一记就怕忘记了 指路:Shiro反序列化漏洞(一)-shiro550流程分析_哔哩哔哩_bilibili khttps://www.zhihu.com/question/486555909 学模板先看看结构和功…...
KICK第四讲Linux 系统下安装 GCC 编译器全指南
Linux 系统下安装 GCC 编译器全指南 GCC(GNU Compiler Collection)是 Linux 系统下最常用的编译器之一,支持 C/C、Java 等多种编程语言。本文将介绍不同 Linux 发行版下的安装方法,帮助开发者快速配置开发环境。 一、使用包管理…...
SpringCloud 学习笔记1(Spring概述、工程搭建、注册中心、负载均衡、 SpringCloud LoadBalancer)
文章目录 SpringCloudSpringCloud 概述集群和分布式集群和分布式的区别和联系 微服务什么是微服务?分布式架构和微服务架构的区别微服务的优缺点?拆分微服务原则 什么是 SpringCloud ?核心功能与组件 工程搭建父项目的 pom 文件 注册中心Rest…...
go 安装swagger
1、依赖安装: # 安装 swag 命令行工具 go install github.com/swaggo/swag/cmd/swaglatest# 安装 gin-swagger 和 swagger 文件的依赖 go get -u github.com/swaggo/gin-swagger go get -u github.com/swaggo/files 2、测试 cmd中输入: swag -v 如果…...
Java中关于Optional的 orElse 操作,以及 orElse 与 orElseGet 的区别
文章目录 1. 大概说明2. 详细分析2.1 .orElse 操作2.2 .orElse 的作用:避免空指针异常2.3 为什么要用?2.4 orElseGet如何使用2.5 orElse和orElseGet的区别 1. 大概说明 这篇文章的目的是为了说明: orElse 如何使用orElseGet 如何使用两者的…...
Sqlmap注入工具简单解释
安装 1. 安装 Python SQLMap 是基于 Python 开发的,所以要先安装 Python 环境。建议安装 Python 3.9 或更高版本,可从 Python 官方网站 下载对应操作系统的安装包,然后按照安装向导完成安装。 2. 获取 SQLMap 可以从 SQLMap 的官方 GitHu…...
petalinxu 在zynq的FPGA下的ST7735S的驱动配置
spi的接线: 【TFT模块排针8】 【开发板spi,gpio】【antminers9】 VCC ----------- 3.3V ----------- 3.3V GND ----------- GND ----------- GND BLK(背光)-------GPIO----------- BANK34_L4N_RXD2(w13; j2.12; gpio[2]) RST(复位ÿ…...
数据篇| App爬虫入门(一)
App 的爬取相比 Web 端爬取更加容易,反爬虫能力没有那么强,而且数据大多是以 JSON 形式传输的,解析更加简单。在 Web 端,我们可以通过浏览器的开发者工具监听到各个网络请求和响应过程,在 App 端如果想要查看这些内容就需要借助抓包软件。常见抓包软件有: 工具名称…...
【6】拓扑排序学习笔记
前言 有向无环图和拓扑排序直接关联到中后期的图论建模思想,是很重要的基础知识。这个如果不彻底弄懂,以后图论会很困难。 有向无环图 正如其名,一个边有向,没有环的图,也叫DAG。 DAG图实际运用:描述含…...
OpenCV实现图像特征提取与匹配
一、特征检测与描述子提取 选择特征检测器 常用算法包括: ORB:一种高效的替代SIFT和SURF的算法,主要用于移动机器人和增强现实等领域。适合实时应用,结合FAST关键点与BRIEF描述子。SIFT(尺度不变特征变…...
MyBatis 的核心配置文件是干什么的? 它的结构是怎样的? 哪些是必须配置的,哪些是可选的?
MyBatis 的核心配置文件(通常命名为 mybatis-config.xml)是 MyBatis 应用程序的入口点,它定义了 MyBatis 的全局配置信息 。 核心配置文件的作用: 配置 MyBatis 的运行时行为: 通过 <settings> 标签设置全局参数ÿ…...
VLAN,DHCP实验访问物理机
目标 三层交换机完成DHCP自动分配IP地址不同vlan间完成通信DNS服务器能够解析www.baidu.com,使PC机能够访问连接真实物理机,PC机与物理机能够互相访问 步骤 一、创建VLAN,配置好PC机和交换机的IP地址 LSW1 [LSW1]vlan batch 10 20 Info: T…...
六十天前端强化训练之第十七天React Hooks 入门:useState 深度解析
欢迎来到编程星辰海的博客讲解 看完可以给一个免费的三连吗,谢谢大佬! 目录 一、知识讲解 1. Hooks 是什么? 2. useState 的作用 3. 基本语法解析 4. 工作原理 5. 参数详解 a) 初始值设置方式 b) 更新函数特性 6. 注意事项 7. 类组…...
解决 HTTP 请求中的编码问题:从乱码到正确传输
文章目录 解决 HTTP 请求中的编码问题:从乱码到正确传输1. **问题背景**2. **乱码问题的原因**2.1 **客户端编码问题**2.2 **请求头缺失**2.3 **服务器编码问题** 3. **解决方案**3.1 **明确指定请求体编码**3.2 **确保请求头正确**3.3 **动态获取响应编码** 4. **调…...
跨国企业网络案例分析:SD-WAN智能组网
跨国企业面临的网络问题日益增加,如全球供应链协同、跨国研发协作及实时生产数据传输等场景,对网络质量提出更高要求。本文将深度解析某跨国工业集团如何通过SD-WAN实现网络架构智能化转型。 该集团以上海全球总部为核心,构建了覆盖亚欧美三大…...
视频孪生与三维视频融合:重构工业现场的“数字视网膜“
在浙江某精密制造企业的总控中心,30米长的曲面屏上实时跳动着工厂的每个生产细节:机械臂的运动轨迹与数字模型完全同步,质检工位的操作误差被自动标记,AGV小车的行进路径在三维空间中以光带形式可视化呈现。这种虚实交融的场景并非…...
STM32Cubemx-H7-7-OLED屏幕
如何把江科大的OLED标准库文件换成hal库的文件 前言 本文讲解如在hHAL库中使用OLED,其实江科大做的文件好已经很好了 只讲操作,不讲废话,默认大家都有32基本操作 创建工程 首先创建工程 把那两个引脚设置成开漏 获取标准库文件 打开江科大O…...
FPGA为何要尽量减少组合逻辑的使用
在FPGA设计中,组合逻辑的使用确实需要谨慎,尤其是要尽量减少它的复杂性。这并不是因为组合逻辑本身不好,而是因为它在实际应用中容易引发一系列问题,而这些问题往往与FPGA的设计哲学和硬件特性相冲突。让我从几个关键点来和你聊聊…...
缓存使用的具体场景有哪些?缓存的一致性问题如何解决?缓存使用常见问题有哪些?
缓存使用场景、一致性及常见问题解析 一、缓存的核心使用场景 1. 高频读、低频写场景 典型场景:商品详情页、新闻资讯、用户基本信息。特点:数据更新频率低,但访问量极高。策略: Cache-Aside(旁路缓存)&a…...
基于 RWA 模型与 AI - Agent 协同的企业级 aPAAS 架构设计
一、引言 在企业数字化转型不断深化的当下,现实世界资产(RWA)模型与人工智能智能体(AI - Agent)的协同融合,为企业级应用平台即服务(aPAAS)架构的创新发展带来了新契机。这种架构旨在…...
基于“动手学强化学习”的知识点(一):第 14 章 SAC 算法(gym版本 >= 0.26)
第 14 章 SAC 算法(gym版本 > 0.26) 摘要SAC 算法(连续)SAC 算法(离散) 摘要 本系列知识点讲解基于动手学强化学习中的内容进行详细的疑难点分析!具体内容请阅读动手学强化学习&…...
【QT:信号和槽】
QT信号涉及的三要素:信号源、信号类型、信号的处理方式。 QT的信号槽机制: 给按钮的点击操作关联一个处理函数,用户点击按钮时触发,对应的处理函数就会执行 QT中使用connect函数将信号和槽关联起来,信号触发…...
Oracle中的INHERIT PRIVILEGES权限
Oracle中的INHERIT PRIVILEGES权限 存储过程和用户函数的AUTHID属性调用者权限vs定义者权限一个简单的示例INHERIT PRIVILEGES权限的含义INHERIT PRIVILEGES权限的安全隐患注意到Oracle 19c数据库中有如下权限信息: SQL> select grantor,grantee,table_name,privilege fro…...