嘘~ 正在从服务器偷取页面 . . .

SpringBoot 层次结构


1. Maven 项目

在maven创建时候的测试名就是 GroupId 就是运行的主类。

所有的目录结构都是约定好的标准结构,我们千万不要随意修改目录结构。

我们再来看最关键的一个项目描述文件pom.xml

其中,groupId类似于Java的包名,通常是公司或组织名称,artifactId类似于Java的类名,通常是项目名称,再加上version,一个Maven工程就是由groupIdartifactIdversion作为唯一标识。我们在引用其他第三方库的时候,也是通过这3个变量确定。例如,依赖commons-logging

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

Maven解决了依赖管理问题。例如,我们的项目依赖abc这个jar包,而abc又依赖xyz这个jar包。

当我们声明了abc的依赖时,Maven自动把abcxyz都加入了我们的项目依赖,不需要我们自己去研究abc是否需要依赖xyz

┌──────────────┐
│Sample Project│
└──────────────┘
        │
        ▼
┌──────────────┐
│     abc      │
└──────────────┘
        │
        ▼
┌──────────────┐
│     xyz      │
└──────────────┘

Maven定义了几种依赖关系,分别是compiletestruntimeprovided

scope 说明 示例
compile 编译时需要用到该jar包(默认) commons-logging
test 编译Test时需要用到该jar包 junit
runtime 编译时不需要,但运行时需要用到 mysql
provided 编译时需要用到,但运行时由JDK或某个服务器提供 servlet-api

其中,默认的compile是最常用的,Maven会把这种类型的依赖直接放入classpath。

test依赖表示仅在测试时使用,正常运行时并不需要。最常用的test依赖就是JUnit:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.2</version>
    <scope>test</scope>
</dependency>

runtime依赖表示编译时不需要,但运行时需要。最典型的runtime依赖是JDBC驱动,例如MySQL驱动:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
    <scope>runtime</scope>
</dependency>

provided依赖表示编译时需要,但运行时不需要。最典型的provided依赖是Servlet API,编译的时候需要,但是运行时,Servlet服务器内置了相关的jar,所以运行期不需要:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0</version>
    <scope>provided</scope>
</dependency> 

1.1 Maven 镜像

除了可以从Maven的中央仓库下载外,还可以从Maven的镜像仓库下载。如果访问Maven的中央仓库非常慢,我们可以选择一个速度较快的Maven的镜像仓库。Maven镜像仓库定期从中央仓库同步:

          slow     ┌───────────────────┐
    ┌─────────────>│Maven Central Repo.│
    │              └───────────────────┘
    │                        │
    │                        │sync
    │                        ▼
┌───────┐  fast    ┌───────────────────┐
│ User  │─────────>│Maven Mirror Repo. │
└───────┘          └───────────────────┘

中国区用户可以使用阿里云提供的Maven镜像仓库。使用Maven镜像仓库需要一个配置,在用户主目录下进入.m2目录(在这个 .m2 目录中我们可以看到 maven 从仓库中下载的依赖包),创建一个settings.xml配置文件,内容如下:

<settings>
    <mirrors>
        <mirror>
            <id>aliyun</id>
            <name>aliyun</name>
            <mirrorOf>central</mirrorOf>
            <!-- 国内推荐阿里云的Maven镜像 -->
            <url>https://maven.aliyun.com/repository/central</url>
        </mirror>
    </mirrors>
</settings>

1.2 Maven 指令

mvn clean:清理所有生成的class和jar;

mvn clean compile:先清理,再执行到compile

mvn clean test:先清理,再执行到test,因为执行test前必须执行compile,所以这里不必指定compile;

mvn clean package:先清理,再执行到package

1.3 Maven 多环境配置

对于多环境配置,Maven 提供了 profile 进行管理,只需修改对应 SpringBoot 的配置文件,就能够实现 dev、prod 环境的快速切换。

<profiles>
        <profile>
            <!--id 是每一个 profile 的唯一区分-->
            <id>dev</id>
            <!--设置该 profile 默认启动-->
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <!--properties 包含自定义的 xml 标签内容-->
            <properties>
                <build.profile.id>dev</build.profile.id>
            </properties>
        </profile>
        <profile>
            <id>cxy621</id>
            <properties>
                <build.profile.id>cxy621</build.profile.id>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <build.profile.id>prod</build.profile.id>
            </properties>
        </profile>
</profiles>

对于 id 和总配置文件,我们使用-进行区别。

#application.yml
spring:
	profiles:
    	active: "@build.profile.id@
    	#对应 maven 中配置的自定义 xml

#application-cxy621.yml 自己的文件配置
server:
  port: 9000
#这样既可以直接使用 id 为 cxy621 中的文件配置

切换配置文件

2. yaml

在创建好项目之后,在 resource 会有一个自带的配置文件 application.properties ,在这里可以更改 Spring-Boot 默认的配置(比如默认开启端口 8080)。

现在更推荐使用 yml 的配置文件,因为 properties 是键值对的形式,而 yml 使用的是严格的缩进,写法上更加简单。yaml —— “Yet Another Markup Language”,这种语言以数据为中心,而不是以标记语言为重点。

2.1 yaml 数值注入

${*}作为字符串替换,允许我们使用 yaml 外配置文件中的数据给 bean 注入属性值。

#application.yaml 配置示例
jwt:
  secret: 19ADSKJDGKJ12983JKDFHAKAS  #JWT密钥
  expiration: 864000000              #JWT过期时间(1000天)
@Value("${jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private int expiration;

//这样我们就注入成功了

除了简单的属性注入,还可以直接注入类。

@Component
@ConfigurationProperties(prefix = "person")
//将配置文件种的每一个属性,映射到这个组件中,告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
@Validated//数据校验
public class Person {
   


    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String, Object> maps;
    private List<Object> lists;
    private Dog dog;

/......../
}
person:
  name: 聂文钊
  age: 3
  happy: false
  birth: 2000/01/01
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - girl
    - music
  dog:
    name: 旺旺
    age: 1

2.2 filtering

在资源文件中可以使用${...}来表示变量。变量的定义可以为系统属性,项目中属性,筛选的资源以及命令。

如果在 pom 文件中继承了spring-boot-starter-parentpom 文件,那么maven-resources-plugins的 Filtering 默认的过滤符号就从${*}改为@...@(i.e. @maven.token@ instead of ${maven.token})来防止与 spring 中的占位符冲突。

所以在上面1.3的示例中,就是使用@...@设置 profile。

3. SpringBoot

Spring 是一个开源的轻量级框架,目的是为了简化企业级应用程序开发。Spring 框架除了帮我们管理对象及其依赖关系,还提供像通用日志记录、性能统计、安全控制、异常处理等面向切面的能力,还能帮我管理最头疼的数据库事务,本身提供了一套简单的 JDBC 访问实现,提供与第三方数据访问框架集成(如 Hibernate、JPA),与各种 Java EE 技术整合(如Java Mail、任务调度等等),提供一套自己的 web 层框架 Spring MVC、而且还能非常简单的与第三方 web 框架集成。

Spring 让我们不再需要自己通过工厂和生成器来创建及管理对象之间的依赖关系。但是 Spring 的弊端也很快就暴露出来了,虽然 Spring 已经帮我进行了依赖的管理,但我们有时候需要写一堆依赖配置文件。因为我们一旦需要什么功能,我们就需要去添加这个功能所包含的依赖。这个时候,Spring-Boot 出现就完美解决了这个问题。

Spring-Boot 是基于 Spring 优化而诞生出的一个框架。Spring-Boot 解决了 Spring 所需依赖过多的问题,该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。在导入外部依赖包的时候,一个包就整合了多个依赖,在初始化的时候 idea 也提供了可视化的方式直接添加所需要的 jar 包(Spring-Boot 已经整合了像是 tomcat 这类的 web 启动)。

3.1 层次

Spring-Boot 中有几个常见的包的写法,分别对应不同的功能,这些并非 Spring-Boot 中规定,只是一种不成文的规定。(大家都这么写了之后,你也就会模仿这个模式写框架了)在这个规定下,其实对于经常使用注释也是如此。比如@RestController@Component等本质上都是一样的,作用都是将其作为 Spring Boot 的组件。

model 层:
model 层即数据库实体层,也被称为 entity 层,pojo 层,domain 层。model 层主要是数据库中的字段在框架中的展示。Spring-Boot 在数据库操作中,会自动把这些和数据库进行匹配(同时自动转换大小写、驼峰命名和下划线命名)。一般数据库一张表对应一个实体类,类属性同表字段一一对应;

mapper 层:
mapper 层即数据持久层,也被称为 dao 层。mapper 层的作用为访问数据库,向数据库发送 SQL 语句,完成数据的增删改查任务。通过在mapper 层的接口,和下面要说的 service 进行交互;

service 层:
service 层即业务逻辑层。service 层的作用为完成功能设计,在整个 Spring-Boot 项目中,service 层实现绝大多数的逻辑,调用 mapper 层接口,接收 mapper 层返回的数据,完成项目的基本功能设计。一般都是写一个 service 接口,之后在 service 层中创建一个 impl 包,用于对 service 接口的实现的实体类;

controller 层:
controller 层即控制层。controller 层的功能为请求和响应控制,是在后端实现跟前端交互的途径。controller 层负责接受前端请求,调用 service 层,接收 service 层返回的数据,最后返回具体的页面和数据到客户端(如果是一个人写 Java 全栈,那么 controller 层直接返回的地址,让前端知道自己跳转到哪里);

当然,这是最简单的包结构,一般的项目都会更加的负责

3.2 推荐依赖框架

通过运行 Spring-Boot 中的 application.java 文件,你的程序就算是正式启动了。默认测试主界面为 localhost:8080

这个时候,如果你在用 Java 写前端页面,写过的人都知道,页面是需要不断调试的。但是 Spring-Boot 本身不能像 vscode 一样进行实时刷新页面进行不断调整,你必须重启主程序,这就导致效率非常低下。这个时候可以使用到 Spring web 最需要的依赖——热部署。通过添加 dev-tool 这个依赖,你就可以使用刷新进行实时调节了。直接使用 dev-tool 并没有什么作用,这里建议使用 idea 中 JRebel 插件,配置可以参考这个网站

还有一个是在写实体类中的简化操作的依赖——lombok。在使用实体类时,我们需要创建有参&无参构造方法、Getter&Setter,才能作为一个规范的实体类的写法。虽然我们 alt + insert 这样的快捷键,但是在 ATSAST 的实际开发中,有些表中有多达20个的字段,如果这样写,代码将会十分臃肿。

lombok 中则定义了几个十分好用的注释:

@Data:注解在类上;提供类所有属性的 get 和 set 方法,此外还提供了equals、canEqual、hashCode、toString 方法 ;

@AllArgsConstructor:注解在类上;为类提供一个全参的构造方法,加了这个注解后,类中不提供默认构造方法了;

@NoArgsConstructor:注解在类上;为类提供一个无参的构造方法。

这样就可以省去几十行的代码,但是这样的插件也有弊端。就如知乎上某些人说的那样,这种通过插件取缔快捷键的方法可能对整个项目效率提升并不大,因为这样导致别人也需要安装这个插件。最后这个插件的耦合性就可能越来越大,到时候如果想要舍弃这个插件,所付出的代价和时间可能不会很小

3.3 各个层细节问题

在这之前,需要了解 Spring-Boot 中的一个非常重要的注释。@Autowired 注释,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,get方法

在 controller & service 层中,我们需要使用这个注释来分别对 service & mapper 层的对象进行自动装配

3.3.1 controller

在 controller 层中,我们需要使用 Spring-Boot 的注释标注自己是控制器。这里有两个注释,一个是 @Controller ,还有一个是 @RestController,下面我们说说这两者的区别

  1. 一般来说 @RestController 用于前后端分离,@Controller 不用于前后端分离, 因为使用 @RestController 的话,导致配置的视图解析器 InternalResourceViewResolver不起作用,从而使 Controller 层的方法无法返回jsp页面(无法转发),返回的内容只能是 return 里的内容。那是不是用 @Controller 就不能返回到指定页面呢?当然不是,需要 @ResponseBody 结合使用,在需要的方法上加上即可

  2. 很多人说 @RestController 相当于 @ResponseBody + @Controller 合在一起的作用,不可否认,但还是有一点小区别,@RestController 可以用实体接收,而 @ResponseBody + @Controller 不能再用实体接收。如果去看源码就可以发现 @RestController 整合了很多其他的注释

@ResponseBody 是让 controller 中的返回值自动打包成一个 json 字符串的形式。而对于 @RequestBody 则是让 SpringBoot 意识到需要将接收的 json 字符串反序列化封装为实体类。

3.3.2 mapper

在 mapper 层实现和数据库的交互,应该加上 @mapper 注释表名自己是 Spring-Boot 中的 mapper 组件。不过可以在 application 中加上 @MapperScan 将在运行主类时自动扫描指定位置的文件,这个时候就需要使用 @Repository 表明这个文件是 Spring-Boot 中的组件,这个在不同层的注释不一样

这样之后,就可以在 service 层使用 @Autowired 进行自动装配了

3.3.3 service

service 在大部分的大型项目中,对应的实体类肯定不止一个,但是 ATSAST 算是练手的中小型项目,所以没有那没多的接口,在以后的功能性扩展中可能会发展得更加宏大。在实体类中,需要使用 @Service 表明这个文件在 Spring-Boot 中的组件身份

写完这个逻辑接口之后,就可以直接去 controller 层实现前后端交互

3.4 不推荐变量注入

在 service 层或者 controller 直接对变量使用 @Autowired 进行自动装配,idea 会提示我们不推荐字段注入。

不推荐使用

//字段注入
@Controller
public class UserController {

    @Autowired
    private UserService userService;

}

//构造器注入,也是最为推荐的一种方式
@Controller
public class UserController {

    private final UserService userService;

    public UserController(UserService userService){
        this.userService = userService;
    }

}

//通过一个 Setter 方法实现注入
@Controller
public class UserController {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService){
        this.userService = userService;
    }
}

劣弊关系对比图

对于构造器注入来说,因为有着严格的构造顺序,所以使得灵活性和性能逊于其他两种方法。但在一般的情况下依然是最优的选择。

不推荐使用字段注入,是因为字段注入是通过反射原理进行注入的。使用反射,我们可以进行很多操作,但是反射在一定程度上破坏了封装性,使得私有变量可以被修改(在 JDK 11 中,这种不安全的情况会被提示出来)。

3.5 前端传值

在前端传值的过程中,可能会有多种形式,一般会有三种

  1. query 传值,这种一般传比较少的值,大部分情况都是向后端发送一个 get 请求,主要作用是查询或修改数据库中特定的值;
  2. json 传值,一般传一个字符串或者数组,一般使用场景是向后端发送一个 post 请求,储存数据库。Spring-Boot 内置了一个依赖 jackson,只要前端传得参数名称一致,就可以自动匹配。对于对象也可以自动装配,不过需要使用 @RequestBody。如果不同需要使用@RequestParam 指定对应前端的参数名称;
  3. 地址传值,这种比较少,指前端直接在地址用 {} 包裹一个参数进行传参。这个时候,方法中需要使用 @PathVariable 进行前端地址中的变量的获取。

4. 统一返回

在 SpringBoot 的 web 依赖包中,整合了返回值转 json 的依赖。

引入源码

但是对于前后端交互来说,我们需要一个固定的返回格式和对应的错误内容。

4.1 定义返回结构

使用泛型定义返回的结构(虽然 Java 的泛型实现归根到底还是 Object)。

import com.sast.jwt.common.enums.CustomError;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class HttpResponse<T> {

    private boolean success;
    private String errMsg;
    private Integer errCode;
    private T data;

    public static <T> HttpResponse<T> success(T data) {
        return new HttpResponse<>(true, null, null, data);
    }

    public static <Void> HttpResponse<Void> failure(String errMsg, Integer errCode) {
        return new HttpResponse<>(false, errMsg, errCode, null);
    }

    public static <Void> HttpResponse<Void> failure(String message) {
        return new HttpResponse<>(false, message, null, null);
    }

    public static <Void> HttpResponse<Void> failure(CustomError customError) {
        return new HttpResponse<>(false, customError.getErrMsg(), customError.getCode(), null);
    }

    public static <Void> HttpResponse<Void> failure() {
        return new HttpResponse<>(false, "Unknown Error", null, null);
    }

}

4.2 包装返回

@Restcontroller@Controller@ResponseBody的结合体。会将后台返回的 Java 对象转换为 Json 字符串传递给前台。而 Spring 默认使用的是 jackson 来做 json 序列化,相对应的 converter 是 MappingJackson2HttpMessageConverter。

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sast.jwt.common.response.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.io.IOException;
import java.lang.reflect.Type;

@Component
@Slf4j
@ControllerAdvice
public class CustomJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter
        implements ResponseBodyAdvice<Object> {

    private static final String CLASS_NAME = "com.sast.jwt.common.response.HttpResponse";

    //若 controller返回值为 String,其调用的 converter 是 StringConverter,如果还是用这个统一返回则会发生类型转换错误
    //所以使用 Spring 自带的 MappingJackson2HttpMessageConverter 解决问题
    @Override
    protected void writeInternal(Object object, Type type,
                                 HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        if (!(object instanceof HttpResponse)) {
            // 当返回对象并非是 HttpResponse 时, 包装成 HttpResponse
            object = HttpResponse.success(object);
        }
        super.writeInternal(object, type, outputMessage);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        // 如果返回类型已经是 HttpResponse 则不转换
        return !CLASS_NAME.equals(returnType.getParameterName());
    }

    /**
     * 无入侵式的统一转化变量
     *
     * @param body                  方法返回值,即需要写入到响应流中
     * @param returnType            对应方法的返回值
     * @param selectedContentType   当前content-type
     * @param selectedConverterType 当前转化器
     * @param request               当前请求
     * @param response              当前响应
     * @return 处理后真正写入响应流的对象
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType,
                                  MediaType selectedContentType, Class selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        //在 writeInternal 已经将非 HttpResponse 包装成 HttpResponse
        if (body == null) {
            log.error("body is null", new NullPointerException());
            return HttpResponse.failure("内容为空", 4004);
        } else {
            return body;
        }
    }
}

5. 包装异常

5.1 自定义异常

使用枚举类创建自定义异常,可以在枚举内部写入:

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
@Getter
public enum CustomError {
    NO_USER(4000, "没有找到用户"),
    FILE_ERROR(4001, "文件类型错误"),
    FILE_NOT_FOUND(4002, "未找到文件"),
    AUTHENTICATION_ERROR(4003, "没有权限"),
    EXIST_USER(4005, "已经有用户注册"),
    INTERNAL_ERROR(5001, "内部出错"),
    UNKNOWN_ERROR(5002, "未知错误"),

    NAME_ERROR(6000, "用户名错误"),
    PASSWORD_ERROR(6001, "密码错误"),
    NAME_PASSWORD_ERROR(6002, "用户名或者密码错误"),

    //token 系列错误
    TOKEN_ERROR(7001, "令牌内容错误"),
    TOKEN_OUT_TIME(7002, "令牌时间过期");

    private int code;
    private String errMsg;

    public void setCode(int code) {
        this.code = code;
    }

    public void setErrMsg(String errMsg) {
        this.errMsg = errMsg;
    }
}

5.2 全局异常

我们需要捕获全局的异常,再使用通过统一封装返回给前端。

写一个异常类,继承 RuntimeException,将自定义的异常包装:

import form.covid.maxtune.common.enums.CustomError;
import lombok.Getter;

@Getter
public class LocalRuntimeException extends RuntimeException {
    private CustomError error;

    public LocalRuntimeException(CustomError error) {
        this.error = error;
    }

    public LocalRuntimeException(String message) {
        super(message);
    }

    public LocalRuntimeException(String message, CustomError error) {
        super(message);
        this.error = error;
    }

    public LocalRuntimeException(String message, Throwable cause, CustomError error) {
        super(message, cause);
        this.error = error;
    }
}

5.3 捕获全局异常

import form.covid.maxtune.common.enums.CustomError;
import form.covid.maxtune.common.response.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;
import java.util.stream.Collectors;

@RestControllerAdvice
@Slf4j
public class AllExceptionHandler {
    @ExceptionHandler(LocalRuntimeException.class)
    public HttpResponse<Void> localRunTimeException(LocalRuntimeException e) {
        log.error("异常", e);
        if (e.getError() != null) {
            return HttpResponse.failure(e.getError());
        } else {
            return HttpResponse.failure(e.getMessage());
        }
    }

    // 处理 json 请求体调用接口对象参数校验失败抛出的异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public HttpResponse<Void> handlerValidationException(MethodArgumentNotValidException e) {
        log.error("参数校验异常", e);
        //流处理,获取错误信息
        String message = e.getBindingResult().getAllErrors().stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining("\n"));
        return HttpResponse.failure(message);
    }

    // 处理 form data 方式调用接口时,参数校验异常
    @ExceptionHandler(BindException.class)
    public HttpResponse<Void> bindExceptionHandler(BindException e) {
        log.error("参数校验异常", e);
        String message = e.getBindingResult().getAllErrors().stream()
                .map(ObjectError::getDefaultMessage)
                .collect(Collectors.joining("\n"));
        return HttpResponse.failure(message);
    }

    // 处理单个 constraint violation 异常
    @ExceptionHandler(ConstraintViolationException.class)
    public HttpResponse<Void> ParamsException(ConstraintViolationException e) {
        log.error("参数校验异常", e);
        Set<ConstraintViolation<?>> set = e.getConstraintViolations();
        StringBuffer errorMsg = new StringBuffer();
        set.forEach(ex -> errorMsg.append(ex.getMessage()));
        return HttpResponse.failure(errorMsg.toString(), CustomError.PARAM_ERROR.getCode());
    }

    // 处理所有未被捕获的异常
    @ExceptionHandler(Exception.class)
    public HttpResponse<Void> handleException(Exception e) {
        log.error("未知异常", e);
        return HttpResponse.failure(CustomError.UNKNOWN_ERROR);
    }
}

5.4 参数校验

对于前端发送参数的校验工作,我们可以通过validation省去麻烦的 if/else 参数校验工作,通过特定的注释进行校验。

springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。

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

5.4.1 示例代码

创建 DTO 实体类:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TestDTO {
    @NotNull(message = "用户名不能为空")
    @NotBlank(message = "用户名不能为空")
    private String name;

    @NotNull(message = "学号不能为空")
    @NotBlank(message = "学号不能为空")
    private String schoolNumber;
}

创建测试 controller:

import form.covid.maxtune.pojo.dto.TestDTO;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

@RestController
@RequestMapping("test")
@Validated
public class TestController {

    @PostMapping("index")
    public String test() {
        return "test";
    }

    @PostMapping("body")
    public TestDTO testBody(@RequestBody @Validated TestDTO testDTO) {
        return testDTO;
    }

    @PostMapping("one")
    public String callMyName(@RequestParam("name")
                             @NotBlank(message = "用户名不能为空")
                             @NotNull(message = "用户名不能为空") String name) {
        return "Hello " + name;
    }
}

这里的返回格式也可以经过统一的封装进行返回。

5.4.2 约束的适用范围

@Null:

  • 说明:被注释的元素必须为 null
  • 适用范围:Object

@NotNull:

  • 说明:被注释的元素必须不为 null
  • 适用范围:Object

@AssertTrue:

  • 说明:被注释的元素必须为 true
  • 适用范围:booleanBoolean

@AssertFalse:

  • 说明:被注释的元素必须为 false
  • 适用范围:booleanBoolean

@Min(value):

  • 说明:被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • 适用范围:BigDecimalBigIntegerbyteByteshortShortintIntegerlongLong

@Max(value):

  • 说明:被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • 适用范围:BigDecimalBigIntegerbyteByteshortShortintIntegerlongLong

@DecimalMin(value):

  • 说明:被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • 适用范围:BigDecimalBigIntegerCharSequencebyteByteshortShortintIntegerlongLong

@DecimalMax(value):

  • 说明:被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • 适用范围:BigDecimalBigIntegerCharSequencebyteByteshortShortintIntegerlongLong

@Size(max, min):

  • 说明:被注释的元素的大小必须在指定的范围内
  • 适用范围:CharSequenceCollectionMapArray

@Digits (integer, fraction):

  • 说明:被注释的元素必须是一个数字,其值必须在可接受的范围内
  • 适用范围:BigDecimalBigIntegerCharSequencebyte Byteshort Shortint Integerlong Long

@Past:

  • 说明:被注释的元素必须是一个过去的日期
  • 适用范围:DateCalendarInstantLocalDateLocalDateTimeLocalTimeMonthDayOffsetDateTimeOffsetTimeYearYearMonthZonedDateTimeHijrahDateJapaneseDateMinguoDateThaiBuddhistDate

@Future:

  • 说明:被注释的元素必须是一个将来的日期
  • 适用范围:DateCalendarInstantLocalDateLocalDateTimeLocalTimeMonthDayOffsetDateTimeOffsetTimeYearYearMonthZonedDateTimeHijrahDateJapaneseDateMinguoDateThaiBuddhistDate

@Pattern(value):

说明:被注释的元素必须符合指定的正则表达式

  • 适用范围:CharSequencenull

@Email:

  • 说明:被注释的元素必须是电子邮箱地址
  • 适用范围:CharSequence

@Length:

  • 说明:被注释的字符串的大小必须在指定的范围内
  • 适用范围:

@NotEmpty:

  • 说明:被注释的字符串的必须非空
  • 适用范围:

@Range:

  • 说明:被注释的元素必须在合适的范围内
  • 适用范围:

6. Angular 规范

针对 git commit 中的消息问题,采取 Angular 进行统一规范(虽然说只有在前端开发中看到有使用)。

每次提交,Commit message 都包括三个部分:Header,Body 和 Footer。

<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

6.1 Head

Header部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。

  • type:

type 用于说明 commit 的类别,只允许使用下面7个标识。

feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:构建过程或辅助工具的变动
  • scope:

scope用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

  • subject:

subject是 commit 目的的简短描述。

6.2 Body

Body 部分是对本次 commit 的详细描述,可以分成多行。

Footer 部分只用于两种情况:

  1. 不兼容变动;
  2. 关闭 Issue。

7. VO、DTO、DO、PO

  • VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来;

  • DTO(Data Transfer Object):数据传输对象,这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB 的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象;

  • DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体;

  • PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。

  1. 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为 VO;
  2. 展示层把 VO 转换为服务层对应方法所要求的 DTO,传送给服务层。

总结:

简单来说,前端传给后端的 json 数据统一用 DTO 封装;后端传给前端的序列化数据使用 VO 封装。

8. IOC

在一般的 Java 程序中,如果一个系统有大量的组件,其生命周期和相互之间的依赖关系如果由组件自身来维护,不但大大增加了系统的复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来了极大的困难。

如果没有使用 IOC,对于持久层和业务层之间的连接,可以能需要创建好几个对象。

核心问题是:

  1. 谁负责创建组件?
  2. 谁负责根据依赖关系组装组件?
  3. 销毁时,如何按依赖顺序正确销毁?

在 IOC 模式下,控制权发生了反转,即从应用程序转移到了 IOC 容器,所有组件不再由应用程序自己创建和配置,而是由 IOC 容器负责,这样,应用程序只需要直接使用已经创建好并且配置好的组件。为了能让组件在 IOC 容器中被“装配”出来,需要某种“注入”机制。

参考文章

  1. yaml 配置注入
  2. 参数校验,统一异常,统一结果
  3. SpringBoot Validation 优雅的全局参数校验
  4. Properties & configuration
  5. 为什么 IDEA 不推荐你使用 @Autowired ?
  6. 通过反射实现对某个对象属性的注入
  7. Spring 中是如何处理循环依赖的
  8. SprinBoot 中修改默认 Json 转换器的部分特性
  9. 为什么说Java的泛型是“假泛型”?
  10. Java 假泛型和真泛型语言如 C++ 、C# 比有什么弱点?
  11. @ControllerAdvice
  12. SpringBoot 优雅的参数校验
  13. Angular 规范
  14. 浅析 VO、DTO、DO、PO 的概念、区别和用处
  15. IOC 原理

文章作者: 陈鑫扬
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 陈鑫扬 !
评论
  目录