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

news2025/3/16 8:46:11

前言:

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_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
);
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
);
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
);
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内容如下:

<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 {
    //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_.-]+$";


}
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.枚举类

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<>();
    @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.自定义注解,默认优先使用枚举

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> {

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

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 {
    @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内容如下:
 

<!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使用三章。还请多多支持!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2315922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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

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

【训练细节解读】文本智能混合分块(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…

在微信小程序或前端开发中,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…

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

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

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

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

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

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

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;频繁与字体打交道的朋友肯定…

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

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;任务执行和版本管…

Redis基本命令手册——五大类型

目录 一&#xff1a;基本操作 二&#xff1a;字符串&#xff08;String&#xff09; 三&#xff1a;哈希&#xff08;Hash) 四&#xff1a;列表&#xff08;List&#xff09; 五&#xff1a;集合&#xff08;Set&#xff09; 六&#xff1a;有序集合&#xff08;Zset&…

历年华中科技大学计算机考研复试上机真题

历年华中科技大学计算机考研复试上机真题 2022华中科技大学计算机考研复试上机真题 2021华中科技大学计算机考研复试上机真题 2019华中科技大学计算机考研复试上机真题 在线评测&#xff1a;https://pgcode.cn 八进制 题目描述 输入一个整数&#xff0c;将其转换成八进制数…