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

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 中的文件配置

切换配置文件

如果出现配置不能正常加载的情况,可以通过重新加载 maven 解决。

对于 idea 的 maven 可视化可以自行进行配置,特别是在打包的过程中,可以使用 maven clean && maven package -P {environment_name} 进行指定。

1.3.1 新版本 maven 多环境管理

在使用SpringBoot 2.4 以上版本时,关于 spring.profiles.active 被废弃了。当然,虽然被弃用了,如果非要使用,还是可以正常使用的。SpringBoot 支持的最新配置方式为 spring.config.activate.on-profile。

SpringBoot 之所以进行改动,最主要的原因,一个是对 Kubernetes 的兼容支持,一个是修复 ConfigFileApplicationListener 类导致的文件处理问题。因此,在文件的加载方式上发生了两个重大变化:文档将按定义的顺序加载、profiles 激活开关不能被配置在特定环境中。

spring:
  profiles:
    active: @build.profile.id@

---
spring:
  config:
    activate:
      on-profile: dev

---
spring:
  config:
    activate:
      on-profile: pro

SpringBoot 2.4 中,配置参数是按在配置文件中定义的先后顺序进行加载的,后激活加载的参数会覆盖前面的。如果有一些配置参数之间是互相覆盖,那就要确保所需要的参数要放在文件最后。

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 进行前端地址中的变量的获取。

3.6 启动错误

3.6.1 BeanDefinitionStoreException

一般是 JDK 与 依赖项不匹配/依赖项版本过高,可以尝试升高 JDK 版本,至少到1.8。

老项目随意升级依赖项 SpringBoot 的依赖也会出现过高情况,需要降低。

3.6.2 configurationPropertiesBeans

在一些依赖中,SpringBoot 的依赖版本可能与包含中的不一致(比如 nacos 中的 SpringBoot 最高只支持到 2.4.2)。

对于多模块下的依赖版本,需要对应匹配。

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 容器中被“装配”出来,需要某种“注入”机制。

8.1 配置过程

在传统的模式中,都是由我们手动(new 进行实例化)去控制类的创建。

这样的缺点一是需要手动写大量类的实例化过程;二是耦合性很高,一个类中耦合了多个其他类。一旦出现问题或者类信息被修改,会导致不可预测的问题

public class TotalService {
    private EmailService emailService;
    
    public TotalService() {
        emailService = new EmailService();
    }
}

class EmailService {
    // ...
}

过高的耦合性

这个时候就借助依赖注入(Dependency Injection DI)的方式,将耦合的类脱离出来。让 EmailService 在外面注入而非在 TotalService 中实例化。

public class TotalService {
    private EmailService emailService;

    public TotalService(EmailService emailService) {
        this.emailService = emailService;
    }
}

class EmailService {
	// ...
}

class Main {
    public static void main(String[] args) {
        EmailService emailService = new EmailService();
        // 在业务层中,我们将配置好的类注入进 TotalService 中
        // 防止因为类修改等问题而让整个 Service 崩溃
        TotalService totalService = new TotalService(emailService);
    }
}

其他 Service 以装配的形式注入业务的 Service 中

在引入 SpringBoot 前,这是一个最简单的依赖注入的方法(构造器注入、Setter 注入)。在实际业务中,面对几千个类,反复使用 new 实例化可能会导致堆内存溢出,所以一般会采用 Singleton 保证类只创建一次。

在引入 SpringBoot 之后,引入了 Bean,并将依赖由框架管理其生命周期。

8.1.1 xml & Java 配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- services -->
    <bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>
    <!-- more bean definitions for services go here -->
</beans>
<!--通过 Spring 加载文件帮我们创建 bean,出现在许多早期 SSM 项目中-->

Java 配置的本质就是将 xml 转化为 Java 原生的配置,利用注解更快速实现配置。

@Configuration
public class BeansConfig {
    @Bean("userDao")
    public UserDaoImpl userDao() {
        return new UserDaoImpl();
    }

    @Bean("userService")
    public UserServiceImpl userService() {
        UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao());
        return userService;
    }
}

8.2.2 注解配置

针对于常用的注解,都有继承 @Component,SpringBoot 会自动帮我们创建管理有此注解的类。

之前的 @Controller、@Service、@Repository 同理,都会被 SpringBoot 扫描。

8.2 Bean

SpringBoot 通过 Bean 加载管理,在顶层的结构设计主要围绕着 BeanFactory 和 xxxRegistry 进行。

  • BeanFactory: 工厂模式定义了IOC容器的基本功能规范
  • BeanRegistry: 向IOC容器手工注册 BeanDefinition 对象的方法

Bean 调用继承链

8.2.1 Bean 结构

BeanFactory
  • ListableBeanFactory:定义了访问容器中 Bean 基本信息的若干方法,如查看 Bean 的个数、获取某一类型 Bean 的配置名、查看容器中是否包括某一 Bean 等方法;

  • HierarchicalBeanFactory:父子级联 IoC 容器的接口,子容器可以通过接口方法访问父容器; 通过 HierarchicalBeanFactory 接口, Spring 的 IoC 容器可以建立父子层级关联的容器体系,子容器可以访问父容器中的 Bean,但父容器不能访问子容器的 Bean。Spring 使用父子容器实现了很多功能,比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean;

  • ConfigurableBeanFactory:是一个重要的接口,增强了 IoC 容器的可定制性,它定义了设置类装载器、属性编辑器、容器初始化后置处理器等方法;

  • ConfigurableListableBeanFactory: ListableBeanFactory 和 ConfigurableBeanFactory 的融合;

  • AutowireCapableBeanFactory:定义了将容器中的 Bean 按某种规则(如按名字匹配、按类型匹配等)进行自动装配的方法。

public interface BeanFactory {    
      
    // 用于取消引用实例并将其与 FactoryBean 创建的 bean 区分开来。
    // 例如,如果命名的 bean 是 FactoryBean,则获取将返回 Factory,而不是 Factory 返回的实例。
    String FACTORY_BEAN_PREFIX = "&"; 
        
    // 根据 bean 的名字和 Class 类型等来得到 bean 实例    
    Object getBean(String name) throws BeansException;    
    Object getBean(String name, Class requiredType) throws BeansException;    
    Object getBean(String name, Object... args) throws BeansException;
    <T> T getBean(Class<T> requiredType) throws BeansException;
    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

    //返回指定 bean 的 Provider
    <T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
    <T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

    //检查工厂中是否包含给定 name 的 bean,或者外部注册的 bean
    boolean containsBean(String name);

    //检查所给定 name 的 bean 是否为单例/原型
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

    //判断所给 name 的类型与 type 是否匹配
    boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
    boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

    //获取给定 name 的 bean 的类型
    @Nullable
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;

    //返回给定 name 的 bean 的别名
    String[] getAliases(String name);
     
}
BeanDefinition

BeanDefinition:定义了各种 Bean 对象及其相互的关系;

Spring IOC 容器管理了我们定义的各种 Bean 对象及其相互的关系,Bean 对象在 Spring 实现中是以 BeanDefinition 来描述的,其继承体系如下:

BeanDefinition 逐步继承

BeanDefinitionReader:这是 BeanDefinition 的解析器;

Bean 的解析过程非常复杂,功能被分的很细,因为这里需要被扩展的地方很多,必须保证有足够的灵活性,以应对可能的变化。Bean 的解析主要就是对 Spring 配置文件的解析。这个解析过程主要通过下图中的类完成:

逐个解析

BeanDefinitionHolder:这是 BeanDefinition 的包装类,用来存储 BeanDefinition,name 以及 aliases 等。

保存 Bean 对应信息

ApplicationContext

IoC 容器的接口类是 ApplicationContext,很显然它必然继承 BeanFactory 对 Bean 规范(最基本的 IoC 容器的实现)进行定义。而 ApplicationContext 表示的是应用的上下文,除了对 Bean 的管理外,还至少应该包含了以下方面:

  • 访问资源:对不同方式的 Bean 配置(资源)进行加载。(实现 ResourcePatternResolver 接口);
  • 国际化: 支持信息源,可以实现国际化(实现MessageSource接口);
  • 应用事件: 支持应用事件(实现ApplicationEventPublisher接口)。

ApplicationContext 结构

  • HierarchicalBeanFactory 和 ListableBeanFactory:ApplicationContext 继承了 HierarchicalBeanFactory 和 ListableBeanFactory 接口,在此基础上,还通过多个其他的接口扩展了 BeanFactory 的功能;

  • ApplicationEventPublisher:让容器拥有发布应用上下文事件的功能,包括容器启动事件、关闭事件等。实现了 ApplicationListener 事件监听接口的 Bean 可以接收到容器事件,并对事件进行响应处理。在 ApplicationContext 抽象实现类 AbstractApplicationContext 中,我们可以发现存在一个 ApplicationEventMulticaster,它负责保存所有监听器,以便在容器产生上下文事件时通知这些事件监听者;

  • MessageSource:为应用提供 i18n 国际化消息访问的功能;

  • ResourcePatternResolver:所有 ApplicationContext 实现类都实现了类似于 PathMatchingResourcePatternResolver 的功能,可以通过带前缀的 Ant 风格的资源文件路径装载 Spring 的配置文件;

  • LifeCycle:该接口是 Spring 2.0 加入的,该接口提供了 start() 和 stop() 两个方法,主要用于控制异步处理过程。在具体使用时,该接口同时被 ApplicationContext 实现及具体 Bean 实现, ApplicationContext 会将 start/stop 的信息传递给容器中所有实现了该接口的 Bean,以达到管理和控制 JMX、任务调度等目的。

不同 Bean 的配置方式(比如 xml,groovy,annotation 等)有着不同的资源加载方式,衍生出众多 ApplicationContext 的实现类。

多种 ApplicationContext 实现方式

第一,从类结构设计上看, 围绕着是否需要Refresh容器衍生出两个抽象类

  • GenericApplicationContext: 是初始化的时候就创建容器,往后的每次 refresh 都不会更改;
  • AbstractRefreshableApplicationContext: AbstractRefreshableApplicationContext 及子类的每次 refresh 都是先清除已有(如果不存在就创建)的容器,然后再重新创建;AbstractRefreshableApplicationContext 及子类无法做到 GenericApplicationContext 混合搭配从不同源头获取 bean 的定义信息

第二, 从加载的源来看(比如 xml,groovy,annotation 等),衍生出众多类型的 ApplicationContext,典型比如:

  • FileSystemXmlApplicationContext:从文件系统下的一个或多个 xml 配置文件中加载上下文定义,也就是说系统盘符中加载 xml 配置文件;
  • ClassPathXmlApplicationContext:从类路径下的一个或多个 xml 配置文件中加载上下文定义,适用于 xml 配置的方式;
  • AnnotationConfigApplicationContext:从一个或多个基于 Java 的配置类中加载上下文定义,适用于 Java 注解的方式;
  • ConfigurableApplicationContext:扩展于 ApplicationContext,它新增加了两个主要的方法: refresh() 和 close(),让 ApplicationContext 具有启动、刷新和关闭应用上下文的能力。在应用上下文关闭的情况下调用 refresh() 即可启动应用上下文,在已经启动的状态下,调用 refresh() 则清除缓存并重新装载配置信息,而调用 close() 则可关闭应用上下文。这些接口方法为容器的控制管理带来了便利,但作为开发者,我们并不需要过多关心这些方法。

8.2.2 初始化流程

以 xml 配置为例,讲解 Bean 通过加载,解析,生成 BeanDefination 并注册到 IoC 容器中的过程。

// create and configure beans
// 在 main() 方法中实例化 ClasspathXmlApplicationContext 即可创建一个 IoC 容器
ApplicationContext context = 
    new ClassPathXmlApplicationContext("aspects.xml", 
                                       "daos.xml", 
                                       "services.xml");

public ClassPathXmlApplicationContext(
    String... configLocations) throws BeansException {
    this(configLocations, true, (ApplicationContext)null);
}

public ClassPathXmlApplicationContext(
    String[] configLocations, 
    boolean refresh, 
    @Nullable ApplicationContext parent) throws BeansException {
    // 设置Bean资源加载器
    super(parent);

    // 设置配置路径
    this.setConfigLocations(configLocations);

    // 初始化容器
    if (refresh) {
        this.refresh();
    }
}

调用父类容器 AbstractApplicationContext 的构造方法(super(parent)方法)为容器设置好 Bean 资源加载器。

public AbstractApplicationContext(@Nullable ApplicationContext parent) {
    // 默认构造函数初始化容器id, name, 状态 以及 资源解析器
    this();

    // 将父容器的Environment合并到当前容器
    this.setParent(parent);
}
// 通过 AbstractApplicationContext 的 setParent(parent) 方法将父容器的 Environment 合并到当前容器
public void setParent(@Nullable ApplicationContext parent) {
    this.parent = parent;
    if (parent != null) {
        Environment parentEnvironment = parent.getEnvironment();
        if (parentEnvironment instanceof ConfigurableEnvironment) {
            this.getEnvironment().merge((ConfigurableEnvironment)parentEnvironment);
        }
    }
}

public AbstractApplicationContext() {
    this.logger = LogFactory.getLog(this.getClass());
    this.id = ObjectUtils.identityToString(this);
    this.displayName = ObjectUtils.identityToString(this);
    this.beanFactoryPostProcessors = new ArrayList();
    this.active = new AtomicBoolean();
    this.closed = new AtomicBoolean();
    this.startupShutdownMonitor = new Object();
    this.applicationStartup = ApplicationStartup.DEFAULT;
    this.applicationListeners = new LinkedHashSet();
    this.resourcePatternResolver = this.getResourcePatternResolver();
}
// Spring 资源加载器
protected ResourcePatternResolver getResourcePatternResolver() {
    return new PathMatchingResourcePatternResolver(this);
}

设置容器的资源加载器之后,FileSystemXmlApplicationContet 执行 setConfigLocations 方法通过调用其父类 AbstractRefreshableConfigApplicationContext 的方法进行对 Bean 定义资源文件的定位。

public void setConfigLocations(@Nullable String... locations) {
    if (locations != null) {
        Assert.noNullElements(locations, "Config locations must not be null");
        this.configLocations = new String[locations.length];

        for(int i = 0; i < locations.length; ++i) {
            // 解析配置路径
            this.configLocations[i] = this.resolvePath(locations[i]).trim();
        }
    } else {
        this.configLocations = null;
    }
}

protected String resolvePath(String path) {
    // 从上一步 Environment 中解析
    return this.getEnvironment().resolveRequiredPlaceholders(path);
}

8.2.3 Bean 实例化

容器中存放的是 Bean 的定义即 BeanDefinition 放到 BeanDefinitionMap 中,本质上是一个 ConcurrentHashMap<String, Object>

Bean 加载流程

getBean()

BeanDefinition 接口中包含了这个类的 Class 信息以及是否是单例等。

方便在之后的获取中确认配置信息

getBean() 方法的的具体思路:

  • 从 BeanDefinitionMap 通过 BeanName 获得 BeanDefinition;

  • 从 BeanDefinition 中获得 BeanClassName;

  • 通过反射初始化 BeanClassName 的实例 instance;

    • 构造函数从 BeanDefinition 的 getConstructorArgumentValues() 方法获取;

    • 属性值从 BeanDefinition 的 getPropertyValues() 方法获取。

  • 返回 BeanName 的实例 instance。

BeanDefinition 还有单例的信息,如果是无参构造函数的实例还可以放在一个缓存中,这样下次获取这个单例的实例时只需要从缓存中获取,如果获取不到再通过上述步骤获取。

public Object getBean(String name) throws BeansException {
    return doGetBean(name, null, null, false);
}
public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
    return doGetBean(name, requiredType, null, false);
}
public Object getBean(String name, Object... args) throws BeansException {
    return doGetBean(name, null, args, false);
}
public <T> T getBean(String name, @Nullable Class<T> requiredType, 
                     @Nullable Object... args) throws BeansException {
    return doGetBean(name, requiredType, args, false);
}

getBean() 的重载通过调用 doGetBean() 来实现。

// 参数 typeCheckOnly:bean 实例是否包含一个类型检查
protected <T> T doGetBean(
    String name, @Nullable Class<T> requiredType, 
    @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {

    // 解析 bean 的真正 name,如果 bean 是工厂类,name 前缀会加 &,需要去掉
    String beanName = transformedBeanName(name);
    Object beanInstance;

    // Eagerly check singleton cache for manually registered singletons.
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        // 无参单例从缓存中获取
        beanInstance = getObjectForBeanInstance(sharedInstance, name, 
                                                beanName, null);
    } else {
        // 如果 Bean 实例还在创建中,则直接抛出异常
        if (isPrototypeCurrentlyInCreation(beanName)) {
            throw new BeanCurrentlyInCreationException(beanName);
        }
        // 如果 Beandefinition 存在于父的 Bean 工厂中,委派给父 Bean 工厂获取
        BeanFactory parentBeanFactory = getParentBeanFactory();
        if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
            // Not found -> check parent.
            String nameToLookup = originalBeanName(name);
            if (parentBeanFactory instanceof AbstractBeanFactory) {
                return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                    nameToLookup, requiredType, args, typeCheckOnly);
            } else if (args != null) {
                // Delegation to parent with explicit args.
                return (T) parentBeanFactory.getBean(nameToLookup, args);
            } else if (requiredType != null) {
                // No args -> delegate to standard getBean method.
                return parentBeanFactory.getBean(nameToLookup, requiredType);
            } else {
                return (T) parentBeanFactory.getBean(nameToLookup);
            }
        }
        
        if (!typeCheckOnly) {
            // 将当前 Bean 实例放入 alreadyCreated 集合里,标识这个 bean 准备创建了
            markBeanAsCreated(beanName);
        }
        
        StartupStep beanCreation = this.applicationStartup
            .start("spring.beans.instantiate")
            .tag("beanName", name);
        try {
            if (requiredType != null) {
                beanCreation.tag("beanType", requiredType::toString);
            }
            RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
            checkMergedBeanDefinition(mbd, beanName, args);

            // 确保它的依赖也被初始化了.
            String[] dependsOn = mbd.getDependsOn();
            if (dependsOn != null) {
                for (String dep : dependsOn) {
                    if (isDependent(beanName, dep)) {
                        throw new BeanCreationException(
                            mbd.getResourceDescription(), beanName, 
                            "Circular depends-on relationship between '" 
                            + beanName + "' and '" + dep + "'");
                    }
                    registerDependentBean(dep, beanName);
                    try {
                        getBean(dep); // 初始化它依赖的Bean
                    }
                    catch (NoSuchBeanDefinitionException ex) {
                        throw new BeanCreationException(
                            mbd.getResourceDescription(), 
                            beanName, 
                            "'" + beanName + "' depends on missing bean '" 
                            + dep + "'", ex);
                    }
                }
            }

            // 创建 Bean 实例:单例
            if (mbd.isSingleton()) {
                sharedInstance = getSingleton(beanName, () -> {
                    try {
                        // 真正创建 bean 的方法
                        return createBean(beanName, mbd, args);
                    }
                    catch (BeansException ex) {
                        // Explicitly remove instance from singleton cache: It might have been put there
                        // eagerly by the creation process, to allow for circular reference resolution.
                        // Also remove any beans that received a temporary reference to the bean.
                        destroySingleton(beanName);
                        throw ex;
                    }
                });
                beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
            }
            // 创建 Bean 实例:原型
            else if (mbd.isPrototype()) {
                // It's a prototype -> create a new instance.
                Object prototypeInstance = null;
                try {
                    beforePrototypeCreation(beanName);
                    prototypeInstance = createBean(beanName, mbd, args);
                }
                finally {
                    afterPrototypeCreation(beanName);
                }
                beanInstance = getObjectForBeanInstance(prototypeInstance, 
                                                        name, beanName, mbd);
            }
            // 创建 Bean 实例:根据 Bean 的 scope 创建
            else {
                String scopeName = mbd.getScope();
                if (!StringUtils.hasLength(scopeName)) {
                    throw new IllegalStateException(
                        "No scope name defined for bean ´" + beanName + "'"
                    );
                }
                Scope scope = this.scopes.get(scopeName);
                if (scope == null) {
                    throw new IllegalStateException(
                        "No Scope registered for scope name '" + scopeName + "'"
                    );
                }
                try {
                    Object scopedInstance = scope.get(beanName, () -> {
                        beforePrototypeCreation(beanName);
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        finally {
                            afterPrototypeCreation(beanName);
                        }
                    });
                    beanInstance = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                }
                catch (IllegalStateException ex) {
                    throw new ScopeNotActiveException(beanName, scopeName, ex);
                }
            }
        }
        catch (BeansException ex) {
            beanCreation.tag("exception", ex.getClass().toString());
            beanCreation.tag("message", String.valueOf(ex.getMessage()));
            cleanupAfterBeanCreationFailure(beanName);
            throw ex;
        }
        finally {
            beanCreation.end();
        }
    }

    return adaptBeanInstance(name, beanInstance, requiredType);
}

8.2.4 处理循环依赖

三级缓存

Spring 只是解决了单例模式下属性依赖的循环问题;Spring 为了解决单例的循环依赖问题,使用了三级缓存。

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects 
    = new ConcurrentHashMap<String, Object>(256);
 
/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects 
    = new HashMap<String, Object>(16);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories 
    = new HashMap<String, ObjectFactory<?>>(16);

第一层缓存(singletonObjects):单例对象缓存池,已经实例化并且属性赋值,这里的对象是成熟对象

第二层缓存(earlySingletonObjects):单例对象缓存池,已经实例化但尚未属性赋值,这里的对象是半成品对象

第三层缓存(singletonFactories):单例工厂的缓存。

单例循环依赖
// 创建两个类,进行循环依赖
// 在属性注入中,并不会检测循环依赖问题
@Service
public class UserService {

	@Autowired
	private OrderService orderService;

	public void query(){
		System.out.println(orderService);
	}
}

@Service
public class OrderService {

	@Autowired
	private UserService userService;

	public void query(){
		System.out.println(userService);
	}
}

创建/调用 Bean 步骤如下:

  1. Spring 启动时,会在 refresh() 方法中创建所有的单例 Bean,并完成自动装配。假如先创建 OrderService,那么就会先调用 getBean(orderService),getBean() 方法会调用到 doGetBean();

  2. 在 doGetBean() 方法中会先调用 getSingleton(orderService) 方法,判断它的返回值是否为空(此时第一次调用,返回值一定为空),如果不为空,则返回 bean,如果为空,则继续执行下面的逻辑。在后面又会调用到 getSingleton() 的一个重载方法:getSingleton(orderService, lambda 表达式),在这个方法中会调用 createBean() 方法。

  3. createBean() 方法会调用到 doCreateBean() 方法,在 doCreateBean() 方法中,会先实例化 Bean(此时的 Bean 是一个半成品),然后将 Bean 通过 addSingletonFactory() 方法放入到 singletonFactories 属性中,接着调用 populateBean() 方法为Bean填充属性,完成自动装配。例如:先创建 orderService ,然后将其放到 singletonFactories 中,接着通过 populateBean() 方法为 orderService 装配 userService 属性;

  4. 为 orderService 装配 userService 属性时,由于 userService 此时还没有被创建,所以又会调用到 getBean(userService)同上面的逻辑一样,先调用 getSingleton(userService) 判断是否为空,此时仍然为空,因为是第一次获取 userService,所以接着会调用到 createBean()、doCreateBean()。

  5. 进入 doCreateBean() 时,同样也是先实例化 userService,然后将半成品的 userService 放入到singletonFactories 中,接着通过 populateBean() 方法来为 userService 装配 orderService 属性。

  6. 在为 userService 装配 orderService 属性时,在 doGetBean(orderService) 中仍然是先调用getSingleton(orderService) 方法,在第3步中,orderService 被放入到了 singletonFactories 中,此时返回值不为空,doGetBean() 方法将 Bean 返回,不会再调用到 createBean() 。getSingleton() 方法的源码如下:

    public Object getSingleton(String beanName) {
        return getSingleton(beanName, true);
    }
    // allowEarlyReference :是否容许从 singletonFactories 中经过 getObject 拿到对象
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        // Spring 首先从 singletonObjects(一级缓存)中尝试获取
        Object singletonObject = this.singletonObjects.get(beanName);
        // 若是获取不到而且对象在建立中,则尝试从 earlySingletonObjects (二级缓存)中获取
        if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
            // isSingletonCurrentlyInCreation():判断当前单例 bean 是否正在建立中
            // 也就是没有初始化完成(好比 A 的构造器依赖了 B 对象因此得先去建立 B 对象,
            // 或则在 A 的 populateBean 过程当中依赖了 B 对象,得先去建立 B 对象,
            // 这时的 A 就是处于建立中的状态。)
            synchronized (this.singletonObjects) {
                singletonObject = this.earlySingletonObjects.get(beanName);
                if (singletonObject == null && allowEarlyReference) {
                    ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
                        // 若是仍是获取不到而且容许从 singletonFactories 经过 getObject 获取
                        // 则经过 singletonFactory.getObject() (三级缓存)获取
                        singletonObject = singletonFactory.getObject();
                        // 若是获取到了则将 singletonObject 放入到e arlySingletonObjects
                        // 也就是将三级缓存提高到二级缓存中
                        this.earlySingletonObjects.put(beanName, singletonObject);
                        this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
        return (singletonObject != NULL_OBJECT ? singletonObject : null);
    }
  7. 在 getSingleton() 中,先从 singletonObjects 中获取 bean,如果获取不到,就从 earlySingletonObjects 中获取,如果还获取不到,就从 singletonFactories 中获取(三级缓存)。

  8. 当将 orderService 对象返回后,代码就会回到 userService 对象的 populateBean() 方法中,此时就能将其赋值给userService对象中的 orderService 属性了。(注意此时的 orderService 对象仍然是一个半成品,这个对象中的属性还没有被填充,但是没有关系,因为 userService 对象持有的是 orderService 的引用,所以当后面 orderService 的属性变化了,对userService 是没有影响的)

  9. 接着进行 userService 对象的其他属性填充,当 userService 完全初始化完成后,先将 userService 对象放入到 singletonObjects 中,再将 userService 对象返回。然后代码就会回到 orderService 对象的 populateBean() 方法处了,此时就可以将 userService 对象赋值给 orderService 对象的 userService 属性了,接着继续完成 orderService 的初始化操作。最后将 orderService 返回,然后将其放入到 singletonObjects 中。这样就完成了对象之前循环依赖。

处理依赖循环的核心在于三层缓存。在 Bean 建立过程当中,有两处比较重要的匿名内部类实现了该接口。一处是 Spring 利用其建立 bean 的时候,另外一处就是:

addSingletonFactory(beanName, new ObjectFactory<Object>() {
    @Override   
    public Object getObject() throws BeansException {
        return getEarlyBeanReference(beanName, mbd, bean);
    }
});

A 首先完成了初始化的第一步,而且将本身提早曝光到 singletonFactories 中,此时进行初始化的第二步,发现本身依赖对象 B,此时就尝试去 get(B),发现 B 尚未被 create,因此走 create 流程,B 在初始化第一步的时候发现本身依赖了对象 A,因而尝试 get(A),尝试一级缓存 singletonObjects(确定没有,由于 A 还没初始化彻底),尝试二级缓存 earlySingletonObjects(也没有),尝试三级缓存 singletonFactories,因为 A 经过 ObjectFactory 将本身提早曝光了,因此B可以经过 ObjectFactory.getObject 拿到 A 对象(半成品),B 拿到 A 对象后顺利完成了初始化阶段一、二、三,彻底初始化以后将本身放入到一级缓存 singletonObjects 中。此时返回 A 中,A 此时能拿到 B 的对象顺利完成本身的初始化阶段二、三,最终 A 也完成了初始化,进去了一级缓存 singletonObjects 中,并且更加幸运的是,因为 B 拿到了 A 的对象引用,因此 B 如今 hold 住的 A 对象完成了初始化。

非单例循环依赖
  • 构造器注入形成的循环依赖:BeanB 需要在 BeanA 的构造函数中完成初始化,BeanA 也需要在 BeanB 的构造函数中完成初始化。这种情况的结果就是两个 Bean 都不能完成初始化,循环依赖难以解决。在调用构造方法之前还未将其放入三级缓存之中。通过使用 @Lazy 注解解决;
  • 不能解决 prototype 作用域循环依赖:Spring不会缓存 ‘prototype’ 作用域的 Bea;
  • 不能解决多例的循环依赖:多实例 Bean 每次调用一次 getBean 都会执行一次构造方法并且给属性赋值,没有三级缓存。可以通过把 Bean 改成单例的解决。

8.2.5 生命周期

Spring 容器可以管理 singleton 作用域 Bean 的生命周期,在此作用域下,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁。

而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。每次客户端请求 prototype 作用域的 Bean 时,Spring 容器都会创建一个新的实例,并且不会管那些被配置成 prototype 作用域的 Bean 的生命周期。

Spring 生命周期

  • 如果 BeanFactoryPostProcessor 和 Bean 关联, 则调用 postProcessBeanFactory 方法;(即首先尝试从 Bean 工厂中获取 Bean)

  • 如果 InstantiationAwareBeanPostProcessor 和 Bean 关联,则调用 postProcessBeforeInstantiation 方法;

  • 根据配置情况调用 Bean 构造方法实例化 Bean

  • 利用依赖注入完成 Bean 中所有属性值的配置注入

  • 如果 InstantiationAwareBeanPostProcessor 和 Bean 关联,则调用 postProcessAfterInstantiation 方法和 postProcessProperties;

  • 调用 xxxAware 接口 (上图只是给了几个例子)

    • 第一类 Aware 接口

      • 如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值;
      • 如果 Bean 实现了 BeanClassLoaderAware 接口,则 Spring 调用 setBeanClassLoader() 方法传入classLoader的引用;
      • 如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。
    • 第二类 Aware 接口

      • 如果 Bean 实现了 EnvironmentAware 接口,则 Spring 调用 setEnvironment() 方法传入当前 Environment 实例的引用;
      • 如果 Bean 实现了 EmbeddedValueResolverAware 接口,则 Spring 调用 setEmbeddedValueResolver() 方法传入当前 StringValueResolver 实例的引用;
      • 如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。
  • 如果 BeanPostProcessor 和 Bean 关联,Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的;

  • 如果 Bean 实现了 InitializingBean 接口,Spring 调用 afterPropertiesSet();(或者有执行 @PostConstruct 注解的方法)

  • 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法;

  • 如果 BeanPostProcessor 和 Bean 关联, Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了;

  • 如果在 <bean> 中指定了该 Bean 的作用范围为 scope=”singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 <bean> 中指定了该 Bean 的作用范围为 scope=”prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean;

  • 如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;(或者有执行 @PreDestroy 注解的方法)

  • 如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

对于管理的方法有如下分类:

  • Bean自身的方法:这个包括了 Bean 本身调用的方法和通过配置文件中 <bean> 的 init-method 和 destroy-method 指定的方法;

  • Bean级生命周期接口方法:这个包括了 BeanNameAware、BeanFactoryAware、ApplicationContextAware。当然也包括InitializingBean和DiposableBean这些接口的方法(可以被 @PostConstruct 和 @PreDestroy 注解替代);

  • 容器级生命周期接口方法:这个包括了 InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”;

  • 工厂后处理器接口方法:这个包括了 AspectJWeavingEnabler, ConfigurationClassPostProcessor, CustomAutowireConfigurer 等等非常有用的工厂后处理器接口的方法。工厂后处理器也是容器级的。在应用上下文装配配置文件之后立即调用。

参考文章

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

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