Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

Externalized Configuration

更多信息参考:7.2 Externalized Configuration

Spring Boot lets you externalize your configuration so that you can work with the same application code in different environments. You can use a variety of external configuration sources, include Java properties files, YAML files, environment variables, and command-line arguments.

Property values can be injected directly into your beans by

  • using the @Value annotation, accessed through Spring’s Environment abstraction,
  • or be bound to structured objects through @ConfigurationProperties.

Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Later property sources can override the values defined in earlier ones. Sources are considered in the following order:

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes.
  3. ⭐️ Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. ⭐️ OS environment variables (System.getenv()).
  6. ⭐️ Java System properties (System.getProperties()) (that is, arguments starting with -D, such as -Dspring.profiles.active=prod).
  7. ⭐️ JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. ⭐️ Command line arguments. (that is, arguments starting with --, such as --server.port=9000)
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

⭐️ Config data files are considered in the following order:

  1. Application properties packaged inside your jar (application.properties and YAML variants).
  2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  3. Application properties outside of your packaged jar (application.properties and YAML variants).
  4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

It is recommended to stick with one format for your entire application. If you have configuration files with both .properties and .yml format in the same location, .properties takes precedence.

External Application Properties

7.2.3. External Application Properties

Spring Boot will automatically find and load application.properties and application.yaml files from the following locations when your application starts:

  1. From the classpath
    1. The classpath root
    2. The classpath /config package
  2. From the current directory
    1. The current directory
    2. The config/ subdirectory in the current directory
    3. Immediate child directories of the config/ subdirectory

The list is ordered by precedence (with values from lower items overriding earlier ones). Documents from the loaded files are added as PropertySources to the Spring Environment.

spring.config.name 修改配置文件名称:

If you do not like application as the configuration file name, you can switch to another file name by specifying a spring.config.name environment property.

For example, to look for myproject.properties and myproject.yaml files you can run your application as follows:

1
$ java -jar myproject.jar --spring.config.name=myproject

spring.config.location 引用外部配置文件:

You can also refer to an explicit location by using the spring.config.location environment property. This property accepts a comma-separated list of one or more locations to check.

The following example shows how to specify two distinct files:

1
2
3
$ java -jar myproject.jar --spring.config.location=\
optional:classpath:/default.properties,\
optional:classpath:/override.properties

Use the prefix optional: if the locations are optional and you do not mind if they do not exist.

配置嵌入式服务器

Spring Boot 集成了 Tomcat、Jetty 和 Undertow,极大便利了项目部署。下面介绍一些常用配置:

1
2
server.port=8080 # Server HTTP port.
server.context-path= # Context path

Tomcat

URI 编码配置:

1
server.tomcat.uri-encoding=UTF-8 # Character encoding to use to decode the URI.

代理配置:

1
2
3
server.tomcat.remote-ip-header= # Name of the http header from which the remote ip is extracted. For instance `X-FORWARDED-FOR`
server.tomcat.protocol-header= # Header that holds the incoming protocol, usually named "X-Forwarded-Proto".
server.tomcat.port-header=X-Forwarded-Port # Name of the HTTP header used to override the original port value.

Socket 连接限制及等待超时时间:

1
2
server.tomcat.max-connections= # Maximum number of connections that the server will accept and process at any given time.
server.connection-timeout= # Time in milliseconds that connectors will wait for another HTTP request before closing the connection. When not set, the connector's container-specific default will be used. Use a value of -1 to indicate no (i.e. infinite) timeout.

业务线程池调优:

1
2
3
server.tomcat.max-threads=0 # Maximum amount of worker threads. Default 200.
server.tomcat.min-spare-threads=0 # Minimum amount of worker threads.
server.tomcat.accept-count= # Maximum queue length for incoming connection requests when all possible request processing threads are in use.

Undertow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 设置IO线程数, 它主要执行非阻塞的任务,它们会负责多个连接, 默认设置每个CPU核心一个线程
# 不要设置过大,如果过大,启动项目会报错:打开文件数过多
server.undertow.io-threads=16

# 阻塞任务线程池, 当执行类似servlet请求阻塞IO操作, undertow会从这个线程池中取得线程
# 它的值设置取决于系统线程执行任务的阻塞系数,默认值是IO线程数*8
server.undertow.worker-threads=256

# 以下的配置会影响buffer,这些buffer会用于服务器连接的IO操作,有点类似netty的池化内存管理
# 每块buffer的空间大小,越小的空间被利用越充分,不要设置太大,以免影响其他应用,合适即可
server.undertow.buffer-size=1024

# 每个区分配的buffer数量 , 所以pool的大小是buffer-size * buffers-per-region
server.undertow.buffers-per-region=1024

# 是否分配的直接内存(NIO直接分配的堆外内存)
server.undertow.direct-buffers=true

https://www.cnblogs.com/duanxz/p/9337022.html

属性注入

Common Application Properties

Appendix A: Common Application Properties

参考

《Spring Boot in Action》

Spring Boot Tomcat 配置

Roadmap

https://spring.io/projects/spring-boot#support

https://github.com/spring-projects/spring-boot/wiki

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Config-Data-Migration-Guide

参考:

Getting Started

有几种方式可以搭建基于 Spring Boot 的项目:

Spring Initializr

Spring Initializr 在线生成项目

Spring Initializr 从本质上来说就是一个Web应用程序,它能为你生成 Spring Boot 项目结构。虽然不能生成应用程序代码,但它能为你提供一个基本的项目结构,以及一个用于构建代码的 Maven 或 Gradle 构建说明文件。你只需要写应用程序的代码就好了。

Spring Initializr

Spring Boot CLI

Spring Boot CLI 命令行工具,下载地址点我,命令如下:

1
$ spring init -dweb,data-jpa,h2,thymeleaf --build gradle readingList

Spring Tools

Spring Tools,官方定制的 IDE 插件,支持:Eclipse、VS Code、Theia。

IntelliJ IDEA

  1. 收费版:直接使用 Spring Initializr 插件
  2. 社区版:离线安装 Spring Assistant 插件(在线安装方式被墙)

Spring Boot 组成

Spring Boot 的各个子项目组成及结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring-boot-dependencies (BOM)
spring-boot-parent (Parent POM)
spring-boot
spring-boot-autoconfigure
spring-boot-starters (Parent module)
spring-boot-starter (Core starter, including auto-configuration support, logging and YAML)
spring-boot-starter-parent (Parent pom providing dependency and plugin management for applications
built with Maven)
spring-boot-starter-web
...
spring-boot-test
spring-boot-test-autoconfigure
spring-boot-actuator
spring-boot-actuator-autoconfigure
spring-boot-devtools
spring-boot-cli
spring-boot-docs
spring-boot-tools (Parent module)
spring-boot-autoconfigure-processor
spring-boot-configuration-processor
spring-boot-maven-plugin
spring-boot-gradle-plugin
...

项目管理

  • spring-boot-dependencies
    • BOM (Bill of Materials),用于定义和统一管理 Sprint Boot 的各个依赖版本号。

    • 业务项目可以通过 dependencyManagement 引入该依赖,解决单继承问题

    • 业务项目可以覆盖版本号如下(但不建议):

      1
      2
      3
      <properties>
      <log4j2.version>2.16.0</log4j2.version>
      </properties>
  • spring-boot-parent
    • Spring Boot 各个依赖的父 POM,用于构建配置。
    • 继承自 spring-boot-dependencies

核心依赖

  • spring-boot Spring Boot 的核心工程。
  • spring-boot-autoconfigure 实现 Spring Boot 自动配置的关键,常用的包含:
    • 自动配置总开关 @EnableAutoConfiguration
    • 各种自动配置类 *AutoConfiguration
    • 各种外部化配置属性类 *Properties
    • 各种条件化注解类 @ConditionOn*
  • spring-boot-starters 起步依赖的父 POM
    • Spring Boot 提供的众多起步依赖,用于降低项目依赖的复杂度,清单详见:Starters,例如:
      • spring-boot-starter 核心起步依赖,包括自动配置支持、日志、YAML 依赖
      • spring-boot-starter-parent 业务项目的父 POM,继承自 spring-boot-dependencies
      • spring-boot-starter-web WEB 开发相关起步依赖
      • spring-boot-starter-test 测试相关起步依赖
    • 起步依赖本质上就是特殊的 Maven 依赖和 Gradle 依赖,利用了传递依赖解析,把常用库聚合在一起,组成了几个为特定功能而定制的依赖。
    • 比起减少依赖数量,起步依赖还引入了一些微妙的变化。向项目中添加了某个起步依赖,实际上指定了应用程序所需的一类功能
    • 起步依赖引入的库的版本兼容性都是经过测试的,可以放心使用。

工具或插件

  • spring-boot-testspring-boot-test-autoconfigure
    • 提供一系列测试支持,常用的如:@SpringBootTest、mock、web 支持。
  • spring-boot-actuatorspring-boot-actuator-autoconfigure
    • 包含许多额外的特性,以帮助你通过 HTTP 或 JMX 端点来监控和管理生产环境的应用程序。包括以下特性(详见用户手册):
      • Endpoints Actuator endpoints allow you to monitor and interact with your application. Spring Boot includes a number of built-in endpoints and you can also add your own. For example the health endpoint provides basic application health information. Run up a basic application and look at /actuator/health.
      • Metrics Spring Boot Actuator provides dimensional metrics by integrating with Micrometer.
      • Audit Spring Boot Actuator has a flexible audit framework that will publish events to an AuditEventRepository. Once Spring Security is in play it automatically publishes authentication events by default. This can be very useful for reporting, and also to implement a lock-out policy based on authentication failures.
  • spring-boot-devtools
    • 热部署、静态资源 livereload 等等。
  • spring-boot-tools 工具集的父 POM。为 Spring Boot 开发者提供的常用工具集。例如:
    • spring-boot-maven-plugin 插件
    • spring-boot-gradle-plugin 插件
  • spring-boot-cli
    • 命令行工具。

POM 配置

参考:16. Build Tool Plugins

继承 spring-boot-starter-parent

Maven 用户可以继承 spring-boot-starter-parent POM 项目以获得合理的默认配置:

1
2
3
4
5
6
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>

你只需要指定 parent 的 <version> 版本号。之后如果你需要引入其它起步依赖,你可以安全的省略起步依赖的 <version> 版本号,parent 会统一管理。

父项目还提供了以下功能:

  • Java 1.8 作为默认的编译器级别
  • UTF-8 源码编码
  • 提供统一的依赖版本管理(继承自 spring-boot-dependencies BOM),可以让你在自己的 pom 中引入依赖时省略版本号定义,保障依赖间的兼容性
  • An execution of the repackage goal with a repackage execution id.
  • 合理的 plugin configuration 配置
  • 合理的 resource filtering 配置(application.properties and application.yml including profile-specific files)

组合 spring-boot-dependencies

不是每个人都喜欢继承 spring-boot-starter-parent POM 项目。每个公司可能都拥有自己的标准父项目,或者你更愿意明确声明所有 Maven 配置。

即使如此,你仍然可以以组合方式引入 scope=importspring-boot-dependencies BOM 项目,来享受依赖管理的好处。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

这种组合方式能解决 Maven 单继承问题。

引入 spring-boot-maven-plugin

The Spring Boot Maven Plugin provides Spring Boot support in Apache Maven. It allows you to package executable jar or war archives, run Spring Boot applications, generate build information and start your Spring Boot application prior to running integration tests.

spring-boot-maven-plugin 插件内置几个 goal,如下:

spring-boot-maven-plugin

Goal Description
spring-boot:build-image Package an application into a OCI image using a buildpack.
spring-boot:build-info Generate a build-info.properties file based on the content of the current MavenProject.
spring-boot:help Display help information on spring-boot-maven-plugin. Call mvn spring-boot:help -Ddetail=true -Dgoal=<goal-name> to display parameter details.
spring-boot:repackage Repackage existing JAR and WAR archives so that they can be executed from the command line using java -jar. With layout=NONE can also be used simply to package a JAR with nested dependencies (and no main class, so not executable).
spring-boot:run Run an application in place.
spring-boot:start Start a spring application. Contrary to the run goal, this does not block and allows other goals to operate on the application. This goal is typically used in integration test scenario where the application is started before a test suite and stopped after.
spring-boot:stop Stop an application that has been started by the “start” goal. Typically invoked once a test suite has completed.

项目运行方式一

Goal spring-boot:run 用于快速编译并运行 Spring Boot 应用,常用于本地开发环境。命令:

1
$ mvn spring-boot:run

项目运行方式二

Goal spring-boot:repackage 用于重新打包现有的 jar/war 包,以便可以通过 java -jar 命令运行,常用于部署环境。命令:

1
2
3
4
# `clean` Phase 会清理 target 目录
# `package` Phase 先将项目打包成一个 jar/war 包
# `spring-boot:repackage` Goal 再重新打成 jar 包
$ mvn clean package spring-boot:repackage

⚠️ 如果未 mvn package 就直接 mvn spring-boot:repackage 会打包失败:

1
org.springframework.boot:spring-boot-maven-plugin:X.X.X.RELEASE:repackage failed: Source file must not be null 

Executing spring-boot:repackage Goal During Maven’s package Phase

We can configure the Spring Boot Maven Plugin in our pom.xml to repackage the artifact during the package phase of the Maven lifecycle. In other words, when we execute mvn package, the spring-boot:repackage will be automatically executed.

The configuration is pretty straightforward. We just add the repackage goal to an execution element:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

参考:

重新打包后的 jar 包内容如下:

repackaged jar file

重新打包后的 jar 包会内嵌一个 Servlet 容器,你可以像运行任何 Java 应用程序一样运行它:

1
$ java -jar target/myapplication-0.0.1-SNAPSHOT.jar

也可以指定 Java System properties 运行,例如:

1
$ java -jar -Dspring.profiles.active=prod target/myapplication-0.0.1-SNAPSHOT.jar

项目运行方式三

通过在 jar 中添加启动脚本,为 *nix 系统制作一个完全可执行的 jar。常用于生产环境。

参考:Optional parameter executable of spring-boot:repackage

Make a fully executable jar for *nix machines by prepending a launch script to the jar.

⚠️ Caution

该方式打包的可执行的 jar 无法使用 VIM 浏览并编辑内部文件

参考:14.2 Installing Spring Boot Applications

In addition to running Spring Boot applications by using java -jar, it is also possible to make fully executable applications for Unix systems. A fully executable jar can be executed like any other executable binary or it can be registered with init.d or systemd. This helps when installing and managing Spring Boot applications in common production environments.

⚠️ Caution

Fully executable jars work by embedding an extra script at the front of the file. It is recommended that you make your jar or war fully executable only if you intend to execute it directly, rather than running it with java -jar or deploying it to a servlet container.

To create a ‘fully executable’ jar with Maven, use the following plugin configuration:

1
2
3
4
5
6
7
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>

You can then run your application by typing ./my-application.jar (where my-application is the name of your artifact). The directory containing the jar is used as your application’s working directory.

Unix/Linux Services

更多信息参考:14.2.2. Unix/Linux Services

  • Installation as an init.d Service (System V)

  • Installation as a systemd Service

    systemd is the successor of the System V init system and is now being used by many modern Linux distributions.

  • Customizing the Startup Script

    • Customizing the Start Script When It Is Written

    • Customizing a Script When It Runs

      For items of the script that need to be customized after the jar has been written, you can use environment variables or a config file.

      The following environment properties are supported with the default script.

      With the exception of JARFILE and APP_NAME, the settings listed in the preceding section can be configured by using a .conf file. The file is expected to be next to the jar file and have the same name but suffixed with .conf rather than .jar. For example, a jar named /var/myapp/myapp.jar uses the configuration file named /var/myapp/myapp.conf, as shown in the following example:

      1
      2
      3
      4
      MODE=service
      JAVA_OPTS="-Xmx1024M -Dspring.profiles.active=prod"
      PID_FOLDER=./
      LOG_FOLDER=./
脚本命令

当设置 MODE=service./my-application.jar 可执行命令如下:

You can explicitly set it to service so that the stop|start|status|restart commands work or to run if you want to run the script in the foreground.

  • status 查看运行状态和 PID(Started、Running、Stoped、Not running)
  • stop 优雅停止应用
  • force-stop 强制停止应用
  • start 启动应用
  • restart 重启应用

参考

《Spring Boot in Action》

https://github.com/spring-projects/spring-boot

https://docs.spring.io/spring-boot/docs/current/

https://docs.spring.io/spring-boot/docs/current/reference/html/index.html

安全同学讲 Maven 重打包的故事 | 阿里技术

Spring Boot 的 16 条最佳实践

URI 构造

要轻松地操作 URL,可以使用 Spring 的 org.springframework.web.util.UriComponentsBuilder 类。相比手动拼接字符串更易维护,还可以处理 URL 编码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// `java.net.URI#toURL()` 可以转为 `java.net.URL`
private URI getUri(Object body) {
String rootUrl = "";
String path = "";
return UriComponentsBuilder
.fromHttpUrl(rootUrl)
.path(path)
// Append the given query parameter to the existing query parameters.
// .queryParam("key", "value1", "value2", "value3")
// Add the given query parameters.
.queryParams(GenericUtils.toMultiValueMap(body)) // 反射递归遍历对象的字段值,并转成 org.springframework.util.MultiValueMap
.build()
.encode()
.toUri();
}

GenericUtils 参考:https://github.com/qidawu/java-api-test/blob/master/src/main/java/reflect/GenericUtils.java

HTTP 请求

RestTemplate

构造出 java.net.URI 之后,可以使用 org.springframework.web.client.RestTemplate 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;

@Slf4j
public class HttpApiService {

@Autowired
private RestTemplate restTemplate;

public void request(HttpMethod httpMethod, Object body) {
try {
URI uri;
HttpHeaders requestHeaders = getHttpHeaders();
HttpEntity<Object> httpEntity;

// GET 请求,请求参数作为 query parameter 放入 URL
// 对方 Controller 方法入参需标注 @RequestParam,以接收 query parameter
if (httpMethod == HttpMethod.GET) {
uri = getUri(body);
// 请求参数不能放到 HttpEntity,因为 GET 请求的话不会带上 request body(因为底层 HttpURLConnection#setDoOutput(false))
httpEntity = new HttpEntity<>(requestHeaders);
}
// POST 请求,请求参数作为 request body 而不放入 URL
else {
uri = getUri(null);
// POST 表单(Content-Type: application/x-www-form-urlencoded)
// request body 类型必须为 MultiValueMap。MultiValueMap 参数最终会转为形如:key1=value1&key2=value2&...
// Writing [{key=[value]}] with org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter
//
// POST JSON(Content-Type: application/json)
// request body 类型为普通 POJO
// Writing [ReqDTO(key=value)] with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
// 对方 Controller 方法入参需标注 @RequestBody,以接收整个 request body
httpEntity = new HttpEntity<>(body, requestHeaders);
}

ResponseEntity<String> response = restTemplate.exchange(uri, httpMethod, httpEntity, String.class);
if (HttpStatus.OK.equals(response.getStatusCode())) {
log.info(response.getBody());
}
} catch (ResourceAccessException e) {
log.error("TCP 连接建立失败", e);
} catch (HttpClientErrorException | HttpServerErrorException e) {
log.error("HTTP 请求失败,状态码:{}", e.getRawStatusCode(), e);
}
}

private HttpHeaders getHttpHeaders() {
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.add("Accept", MediaType.APPLICATION_JSON_VALUE);
return requestHeaders;
}

}

GET 请求、POST 表单时,对方 Controller 方法入参需标注 @RequestParam,以接收 query parameter。但要注意,官方文档提醒如下:

Supported for annotated handler methods in Spring MVC and Spring WebFlux as follows:

  • In Spring MVC, “request parameters” map to query parameters, form data, and parts in multipart requests. This is because the Servlet API combines query parameters and form data into a single map called “parameters”, and that includes automatic parsing of the request body.
  • In Spring WebFlux, “request parameters” map to query parameters only. To work with all 3, query, form data, and multipart data, you can use data binding to a command object annotated with ModelAttribute.

CURL 形式

GET 请求

1
2
curl --location --request GET 'http://rootUrl/path?key=value' \
--header 'Content-Type: application/x-www-form-urlencoded'

POST 表单

1
2
3
curl --location --request POST 'http://rootUrl/path' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'key=value'

POST JSON

1
2
3
4
5
curl --location --request POST 'http://rootUrl/path' \
--header 'Content-Type: application/json' \
--data-raw '{
"key": "value"
}'

核心类解析

RestTemplate UML

涉及的核心类如下:

org.springframework.*

org.springframework.web.client.RestTemplate

Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others.

The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.

org.springframework.http.converter.HttpMessageConverter

Strategy interface for converting from and to HTTP requests and responses.

参考:

java.net.*

java.net.URL

Class URL represents a Uniform Resource Locator, a pointer to a “resource” on the World Wide Web. A resource can be something as simple as a file or a directory, or it can be a reference to a more complicated object, such as a query to a database or to a search engine. More information on the types of URLs and their formats can be found at: Types of URL

java.net.URLConnection

The abstract class URLConnection is the superclass of all classes that represent a communications link between the application and a URL. Instances of this class can be used both to read from and to write to the resource referenced by the URL.

URLConnection 的继承结构如下:

URLConnection

java.net.HttpURLConnection

A URLConnection with support for HTTP-specific features. See the spec for details.

java.net.Socket

This class implements client sockets (also called just “sockets”). A socket is an endpoint for communication between two machines.

The actual work of the socket is performed by an instance of the SocketImpl class. An application, by changing the socket factory that creates the socket implementation, can configure itself to create sockets appropriate to the local firewall.

java.net.SocketImpl

The abstract class SocketImpl is a common superclass of all classes that actually implement sockets. It is used to create both client and server sockets.

当通过 Socket 类的默认无参构造方法 new Socket() 创建 socket 对象时,其底层实现如下图。从下图可见,将会创建抽象类 SocketImpl 的默认实现类 SocksSocketImpl

Default implementation of SocketImpl

SocksSocketImpl 的继承结构如下。

Socket 类提供了 getOutputStream()getInputStream() 方法,其底层实现获取 AbstractPlainSocketImpl 的两个私有成员变量,如下:

  • java.net.SocketInputSteram,核心方法:

    SocketInputStream#socketRead0

  • java.net.SocketOutputStream,核心方法:

    SocketOutputStream#socketWrite0

    这两个类的继承结构如下:

java.net.SocketInputSteram & java.net.SocketOutputStream

java.io.*

java.io.FileDescriptor

Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.

Applications should not create their own file descriptors.

常见异常

下面介绍网络编程时,常见的 Socket、HTTP 异常。详见:https://docs.oracle.com/javase/8/docs/api/java/net/package-summary.html

IOException

In case the TCP handshakes are not complete, the connection remains unsuccessful. Consequently, the program throws an IOException indicating an error occurred while establishing a new connection.

BindException

java.net.BindException

Signals that an error occurred while attempting to bind a socket to a local address and port.

问题:

1
2
3
4
5
6
7
java.net.BindException: Address already in use (Bind failed)
at java.net.PlainSocketImpl.socketBind(Native Method)
at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:513)
at java.net.ServerSocket.bind(ServerSocket.java:375)
at java.net.ServerSocket.<init>(ServerSocket.java:237)
at java.net.ServerSocket.<init>(ServerSocket.java:181)
...

原因:server-side 未成功 bind() 到指定端口号(如端口被其它服务占用)。

ConnectException

java.net.ConnectException

Signals that an error occurred while attempting to connect a socket to a remote address and port.

Connection refused

问题:

1
2
3
4
5
6
7
8
9
10
11
java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
at java.net.Socket.connect(Socket.java:555)
at java.net.Socket.<init>(Socket.java:451)
at java.net.Socket.<init>(Socket.java:228)
...

ResourceAccessException

原因:client-side connect() 建立 TCP 连接失败

  • client-side connect() 错了服务端口号;
  • server-side 服务未启动、未在 listen() 监听、或 listen()backlog 队列数无法满足 client-side 并发连接请求数;
  • 无法完成 TCP 三次握手:

WireShark 抓包

Connection timed out

问题:

1
2
3
4
5
6
7
8
9
10
11
java.net.ConnectException: Connection timed out (Connection timed out)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at java.net.Socket.<init>(Socket.java:434)
at java.net.Socket.<init>(Socket.java:211)
...

原因:client-side connect() 超时:

1
2
3
Socket socket = new Socket(); 
SocketAddress socketAddress = new InetSocketAddress(host, port);
socket.connect(socketAddress, 5 * 1000); // timeout for connect()

Socket#connect 方法

连接超时原因:client-side 发出 sync 包之后,server-side 未在指定时间内回复 ack 导致的。没有回复 ack 的原因可能是网络丢包、防火墙阻止服务端返回 synack 包等。

  • 防火墙原因

    Sometimes, firewalls block certain ports due to security reasons. As a result, a “connection timed out” error can occur when a client is trying to establish a connection to a server. Therefore, we should check the firewall settings to see if it’s blocking a port before binding it to a service.

SocketTimeoutException

java.net.SocketTimeoutException

Signals that a timeout has occurred on a socket accept() or read().

Accept timed out

问题:

1
2
3
4
5
6
java.net.SocketTimeoutException: Accept timed out
at java.net.PlainSocketImpl.socketAccept(Native Method)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:535)
at java.net.ServerSocket.implAccept(ServerSocket.java:545)
at java.net.ServerSocket.accept(ServerSocket.java:513)
...

原因:server-side accept() 超时:

1
2
ServerSocket serverSocket = new ServerSocket(port, backlog);
serverSocket.setSoTimeout(5 * 1000); // timeout for accept()

setSoTimeout 方法

Read timed out

问题:

1
2
3
4
5
6
7
java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at java.net.SocketInputStream.read(SocketInputStream.java:127)
...

原因:server-side / client-side read() 超时:

1
2
3
4
5
6
7
// server-side
Socket socket = serverSocket.accept();
socket.setSoTimeout(5 * 1000); // timeout for read()

// client-side
Socket socket = new Socket(host, port);
socket.setSoTimeout(5 * 1000); // timeout for read()

setSoTimeout 方法

SocketException

java.net.SocketException

Thrown to indicate that there is an error creating or accessing a Socket.

Connection reset by peer

1
2
3
4
5
6
7
8
java.net.SocketException: Connection reset by peer (connect failed)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:476)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:218)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:200)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:394)
at java.net.Socket.connect(Socket.java:606)
...

Bad file descriptor

1
2
3
4
5
java.net.SocketException: Bad file descriptor (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
...

Broken pipe

1
2
3
4
5
java.net.SocketException: Broken pipe (Write failed)
at java.net.SocketOutputStream.socketWrite0(Native Method)
at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
at java.net.SocketOutputStream.write(SocketOutputStream.java:155)
...

HttpClientErrorException

org.springframework.web.client.HttpClientErrorException

Exception thrown when an HTTP 4xx is received.

HttpClientErrorException

HttpServerErrorException

org.springframework.web.client.HttpServerErrorException

Exception thrown when an HTTP 5xx is received.

HttpServerErrorException

参考

https://docs.oracle.com/javase/8/docs/technotes/guides/net/index.html

https://www.baeldung.com/a-guide-to-java-sockets

https://www.baeldung.com/category/java/tag/exception/

AOP 概念

前面我们重点关注了如何使用依赖注入(DI)管理和配置我们的应用对象,从而实现应用对象之间的解耦,而 AOP 主要实现“横切关注点(cross-cutting concern)”与它们所影响的对象之间的解耦。

在软件开发中,散布于应用中多处的功能被称为“横切关注点(cross-cutting concern)”。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

AOP 补充了 OOP 编程,通过提供另一种思考软件结构的方法。OOP 编程中的模块单元是“类(Class)”,而 AOP 编程中的模块单元是“切面(Aspect)”。

切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)。这样做有两个好处:

  • 首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;
  • 其次,服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被转移到切面中了。

AOP 的一些场景如下:

  • 日志
  • 事务,如 Spring Transactional
  • 安全,如 Spring Security
  • 缓存,如 Spring Cache

AOP 的知识点总结:

AOP 总览

AOP 术语

与大多数技术一样,AOP 已经形成了自己的术语。下图展示了这些概念是如何关联在一起的:

An aspect's functionality (advice) is woven into a program's execution at one or more join points.

切面(Aspect)

什么是切面?通俗来说就是“何时何地发生何事”,其组成如下:

Aspect = Advice (what & when) + Pointcut (where)

通知(Advice)

通知(Advice)定义了何时(when)发生何事(what)

Spring AOP 的切面(Aspect)可以搭配下面五种通知(Advice)注解使用:

通知 描述
@Before The advice functionality takes place before the advised method is invoked.
@After The advice functionality takes place after the advised method completes, regardless of the outcome.
@AfterReturning The advice functionality takes place after the advised method successfully completes.
@AfterThrowing The advice functionality takes place after the advised method throws an exception.
@Around The advice wraps the advised method, providing some functionality before and after the advised method is invoked.

切点(Pointcut)

切点(Pointcut)定义了切面在何处(where)执行。

Spring AOP 的切点(Pointcut)使用 AspectJ 的“切点表达式语言(Pointcut Expression Language)”进行定义。但要注意的是,Spring 仅支持其中一个子集:

切面指示器(Aspectj Designator)

切点表达式的语法如下:

切点表达式(Pointcut Expression)

连接点(Join point)

连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面(aspect)在指定的连接点(join point)被织入(weaving)到目标对象中。在目标对象的生命周期里有多个点可以进行织入:

生命周期 描述
编译期 切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ Compiler 就是以这种方式织入切面的。
类加载期 切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ 5 的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面。
运行期 切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。Spring AOP 构建在动态代理基础之上,因此,Spring 对 AOP 的支持局限于方法拦截。如果你的 AOP 需求超过了简单的方法调用(如构造器或属性拦截),那么你需要考虑使用 AspectJ 来实现切面。

Spring AOP 与 AspectJ AOP 对比

这里总结下 Spring AOP 和 AspectJ AOP 两种织入方式的优缺点:

Spring AOP 优点

  • 使用方式比 AspectJ 简单,无需特殊的 LTW 或 AspectJ Compiler,仅在运行时通知对象

    通过在代理类中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标 bean。当代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。

    Spring 的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用目标方法

    直到应用需要被代理的 bean 时,Spring 才创建代理对象。如果使用的是 ApplicationContext 的话,在 ApplicationContextBeanFactory加载所有 bean 的时候,Spring 才会创建被代理的对象。因为 Spring 运行时才创建代理对象,所以我们不需要特殊的编译器来织入 Spring AOP 的切面。

  • Advice 使用 Java 编写,使用成本低

    Spring 所创建的通知(Advice)都是用标准的 Java 类编写的。这样的话,我们就可以使用与普通 Java 开发一样的集成开发环境(IDE)来开发切面。而且,定义通知所应用的切点通常会使用注解或在 Spring 配置文件里采用 XML 来编写,这两种语法对于Java开发者来说都是相当熟悉的。

    AspectJ 与之相反。虽然 AspectJ 现在支持基于注解的切面,但 AspectJ 最初是以 Java 语言扩展的方式实现的。这种方式有优点也有缺点。通过特有的 AOP 语言,我们可以获得更强大和细粒度的控制,以及更丰富的 AOP 工具集,但是我们需要额外学习新的工具和语法。

Spring AOP 缺点

  • 只支持方法级别的 join points,局限于 public 方法拦截。

    正如前面所探讨过的,通过使用各种 AOP 方案可以支持多种连接点模型。因为 Spring 基于动态代理,所以 Spring 只支持方法连接点。这与一些其他的 AOP 框架是不同的,例如 AspectJ 和 JBoss,除了方法切点,它们还提供了字段和构造器接入点。Spring 缺少对字段连接点的支持,无法让我们创建细粒度的通知,例如拦截对象字段的修改。而且它不支持构造器连接点,我们就无法在 bean 创建时应用通知。

    但是方法拦截可以满足绝大部分的需求。如果需要方法拦截之外的连接点拦截功能,那么我们可以利用 Aspect 来补充 Spring AOP 的功能。

  • 受限于 JDK Proxy 以及 CGLib Proxy(Spring 风格)的特点,不支持方法自调用(self-invocation),即同一个类中的方法调用无法应用切面。

  • 无法将切面应用到非 Spring 工厂创建的 bean。

  • 有一定的运行时开销。

AspectJ AOP 优点

  • 支持所有类型的 join points(构造器、字段、方法),可以做细粒度的控制。
  • 支持任意访问修饰符(如 protected、private)、支持方法自调用(self-invocation)。

AspectJ AOP 缺点

  • 使用上要小心,确保切面只织入到需要被织入的地方。
  • 需要额外的 LTW 或 AspectJ Compiler。

Spring 对 AOP 的支持

Spring AOP 的设计理念和大多数其它 AOP 框架不同。目标并不是为了提供一个最完整的 AOP 实现,而是为了提供一个 AOP 实现与 Spring IoC 的紧密集成,以帮助解决企业级应用的常见问题。

Spring AOP 的两种实现方式

字节码操作库有很多,常用的例如:

aop_lib

JDK Proxy

参考:《Java 反射篇(四)JDK 动态代理总结

CGLib

基于 ASM 库。

参考:https://github.com/cglib/cglib/wiki

cglib

两种实现方式对比

Spring AOP 支持两种模式的动态代理,JDK Proxy 或者 CGLib:

Spring AOP process

两种模式的优势如下:

  • JDK Proxy

    • 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 CGLib 更加可靠。
    • 平滑进行 JDK 版本升级,而第三方字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
    • 代码实现简单,主要利用 JDK 反射机制。
  • CGLib Proxy

    • 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 CGLib 动态代理就没有这种限制。CGLib 动态代理采取的是创建目标类的子类的方式,因为是子类化,我们可以达到近似使用被调用者本身的效果。
    • 只操作我们关心的类,而不必为其它相关类增加工作量。
    • 性能更好,相对于低版本的 JDK Proxy。

核心源码解析

AopProxy 实现结构

org.springframework.aop.framework.DefaultAopProxyFactory 工厂类负责判断创建哪个 AopProxy 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

/**
* Determine whether the supplied {@link AdvisedSupport} has only the
* {@link org.springframework.aop.SpringProxy} interface specified
* (or no proxy interfaces specified at all).
*/
private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
Class<?>[] ifcs = config.getProxiedInterfaces();
return (ifcs.length == 0 || (ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0])));
}

}

org.springframework.aop.framework.CglibAopProxy

org.springframework.aop.framework.JdkDynamicAopProxy,两个关键方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {
/**
* 创建基于接口的 JDK 动态代理
**/
@Override
public Object getProxy(ClassLoader classLoader) {
if (logger.isDebugEnabled()) {
logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

/**
* Implementation of {@code InvocationHandler.invoke}.
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
// Get the interception chain for this method.
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

...
// We need to create a method invocation...
invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// Proceed to the joinpoint through the interceptor chain.
retVal = invocation.proceed();

...
}
}

官方文档的一些关键摘录:

AOP Proxies

Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. By default, CGLIB is used if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes, business classes normally implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface or where you need to pass a proxied object to a method as a concrete type.

It is important to grasp the fact that Spring AOP is proxy-based. See Understanding AOP Proxies for a thorough examination of exactly what this implementation detail actually means.

Proxying Mechanisms

Spring AOP uses either JDK dynamic proxies or CGLIB to create the proxy for a given target object. (JDK dynamic proxies are preferred whenever you have a choice).

If the target object to be proxied implements at least one interface, a JDK dynamic proxy is used. All of the interfaces implemented by the target type are proxied. If the target object does not implement any interfaces, a CGLIB proxy is created.

If you want to force the use of CGLIB proxying (for example, to proxy every method defined for the target object, not only those implemented by its interfaces), you can do so. However, you should consider the following issues:

  • final methods cannot be advised, as they cannot be overridden.
  • As of Spring 3.2, it is no longer necessary to add CGLIB to your project classpath, as CGLIB classes are repackaged under org.springframework and included directly in the spring-core JAR. This means that CGLIB-based proxy support “just works”, in the same way that JDK dynamic proxies always have.
  • As of Spring 4.0, the constructor of your proxied object is NOT called twice any more, since the CGLIB proxy instance is created through Objenesis. Only if your JVM does not allow for constructor bypassing, you might see double invocations and corresponding debug log entries from Spring’s AOP support.

To force the use of CGLIB proxies, set the value of the proxy-target-class attribute of the <aop:config> element to true, as follows:

1
2
3
<aop:config proxy-target-class="true">
<!-- other beans defined here... -->
</aop:config>

To force CGLIB proxying when you use the @AspectJ auto-proxy support, set the proxy-target-class attribute of the <aop:aspectj-autoproxy> element to true, as follows:

1
<aop:aspectj-autoproxy proxy-target-class="true"/>

Spring AOP 切面声明的两种方式

Spring 2.0 之后提供了以下两种方式,为编写自定义切面引入了一种更简单和更强大的方式:

schema-based approach

schema-based approach,手工声明切面方式。下面是一个完整的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- this is the object that will be proxied by Spring's AOP infrastructure -->
<bean id="personService" class="x.y.service.DefaultPersonService"/>

<!-- this is the actual advice itself -->
<bean id="myAspect" class="x.y.MyAspect"/>

<!-- cglib 代理方式配置:proxy-target-class="true" -->
<aop:config>
<aop:aspect ref="myAspect">
<aop:pointcut id="theExecutionOfSomePersonServiceMethod" expression="execution(* x.y.service.PersonService.getPerson(String,int)) and args(name, age)"/>
<aop:around pointcut-ref="theExecutionOfSomePersonServiceMethod" method="process"/>
</aop:aspect>
</aop:config>

@AspectJ annotation style

@AspectJ annotation style

  • @AspectJ 注解风格作为 AspectJ 5 发行版的一部分被引入。Spring 利用了这个注解,使用了 AspectJ 提供的类库进行切点解析和匹配。然而,AOP 运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ 编译器和织入器。
  • The @AspectJ support can be enabled with XML- or Java-style configuration. In either case, you also need to ensure that AspectJ’s aspectjweaver.jar library is on the classpath of your application (version 1.8 or later).

配置如下:

  • Java Config 方式:

    1
    2
    3
    4
    5
    6
    7
    // 声明 Java Config
    @Configuration
    // 开启组件扫描,将 MethodCacheInterceptor 作为 bean 注册到 Spring 容器
    @ComponentScan
    // 开启自动代理
    @EnableAspectJAutoProxy
    public class ConertConfig {}

    注解 @EnableAspectJAutoProxy 用于开启 AspectJ 自动代理,为使用 @Aspect 注解的 bean 创建一个代理。其中 proxyTargetClass 属性用于控制代理方式:

    • true 表示开启 CGLIB 风格的子类继承代理(CGLIB-style ‘subclass’ proxy)
    • 默认为 false 表示开启基于接口的 JDK 动态代理(interface-based JDK proxy)
  • XML 配置方式:

    1
    2
    <context:component-scan base-package="your.package" />
    <aop:aspectj-autoproxy /> <!-- 代理方式配置:proxy-target-class="true" -->

例子

由于项目中散落着各种使用缓存的代码,这些缓存代码与业务逻辑代码交织耦合在一起既编写重复又难以维护,因此打算将这部分缓存代码抽取出来形成一个注解以便使用。

这样的需求最适合通过 AOP 来解决了,来看看如何在 Spring 框架下通过 AOP 和注解实现方法缓存:

自定义注解

首先,自定义一个方法注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package your.package;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 方法级缓存
* 标注了这个注解的方法返回值将会被缓存
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodCache {

/**
* 缓存过期时间,单位是秒
*/
int expire();

}

编写切面

使用注解来创建切面,是 AspectJ 5 所引入的关键特性。在 AspectJ 5 之前,编写 AspectJ 切面需要学习一种 Java 语言的扩展,很不友好。在此我们使用注解来实现我们的切面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package your.package;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.kingdee.finance.cache.service.centralize.CentralizeCacheService;

/**
* 方法级缓存拦截器
*/
@Aspect
@Component
@Slf4j
public class MethodCacheInterceptor {

private static final String CACHE_NAME = "Your unique cache name";

@Autowired
private CentralizeCacheService centralizeCacheService;

/**
* 搭配 AspectJ 指示器“@annotation()”可以使本切面成为某个注解的代理实现
*/
@Around("@annotation(your.package.MethodCache)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String cacheKey = getCacheKey(joinPoint);
Serializable serializable = centralizeCacheService.get(CACHE_NAME, cacheKey);
if (serializable != null) {
log.info("cache hit,key [{}]", cacheKey);
return serializable;
} else {
log.info("cache miss,key [{}]", cacheKey);
Object result = joinPoint.proceed(joinPoint.getArgs());
if (result == null) {
log.error("fail to get data from source,key [{}]", cacheKey);
} else {
MethodCache methodCache = getAnnotation(joinPoint, MethodCache.class);
centralizeCacheService.put(CACHE_NAME, methodCache.expire(), cacheKey, (Serializable) result);
}
return result;
}
}

/**
* 根据类名、方法名和参数值获取唯一的缓存键
* @return 格式为 "包名.类名.方法名.参数类型.参数值",类似 "your.package.SomeService.getById(int).123"
*/
private String getCacheKey(ProceedingJoinPoint joinPoint) {
return String.format("%s.%s",
joinPoint.getSignature().toString().split("\\s")[1], StringUtils.join(joinPoint.getArgs(), ","));
}

private <T extends Annotation> T getAnnotation(ProceedingJoinPoint jp, Class<T> clazz) {
MethodSignature sign = (MethodSignature) jp.getSignature();
Method method = sign.getMethod();
return method.getAnnotation(clazz);
}

}

要注意的是,目前该实现存在两个限制:

  1. 方法入参必须为基本数据类型或者字符串类型,使用其它引用类型的参数会导致缓存键构造有误;
  2. 方法返回值必须实现 Serializable 接口;

开启动态代理

最后,开启 Spring 的组件扫描、自动代理功能:

1
2
3
4
5
6
// 声明 Java Config
@Configuration
// 开启组件扫描,将 MethodCacheInterceptor 作为 bean 注册到 Spring 容器
@ComponentScan
@EnableAspectJAutoProxy
public class ConertConfig {}

投入使用

例如,使用本注解为一个“按 ID 查询列表”的方法加上五分钟的缓存:

1
2
3
4
@MethodCache(expire = 300)
public List<String> listById(String id) {
// return a string list.
}

总结

使用 AOP 技术,你可以在一个地方定义所有的通用逻辑,并通过声明式(declaratively)的方式进行使用,而不必修改各个业务类的实现。这种代码解耦技术使得我们的业务代码更纯粹、仅包含所需的业务逻辑。相比继承(inheritance)和委托(delegation),AOP 实现相同的功能,代码会更整洁。

参考

Spring in Action, 4th

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop

AspectJ

http://openjdk.java.net/jeps/199

https://github.com/cglib/cglib/wiki

@EnableAspectJAutoProxy

https://www.cnblogs.com/xrq730/p/6661692.html

find

find 是最常见和最强大的查找命令,直接查找磁盘,缺点耗时长。命令格式如下:

find [path...] [expression]

The expression is made up of :

  • options (which affect overall operation rather than the processing of a specific file, and always return true)
  • tests (which return a true or false value)
  • actions (which have side effects and return a true or false value)

all separated by operators.

选项

常用选项:

  • -maxdepth 1 只查找当前目录

条件

常用条件:

  • -name 名称查找(例如:find ./ -name 'struts*'

  • -type 类型查找

    • d 目录类型
    • f 常规文件类型
    • l 软链类型
    • ……
  • -user 设定所属用户的名称

  • -group 设定所属用户组的名称

  • -perm 设定权限

  • -regex 使用正则表达式进行匹配

  • -size 表示文件大小

  • -empty 空文件或空目录

  • -atime / -amin File was last accessed n*24 hours/n minutes ago.

  • -ctime / -cmin File’s status was last changed n*24hours/n minutes ago.

  • -mtime / -mmin File’s data was last modified n*24hours/n minutes ago.

动作

常用动作:

  • -print 输出结果(默认动作)
  • -ls 输出详情
  • -delete 执行删除
  • -exec 执行指定命令

操作符

操作符用于提高表达式的优先级,下列操作符的优先级以倒序排列:

  • ( expr ) 强制最高优先级
  • ! expr 求反操作
  • expr1 expr2 (or expr1 -a expr2) 求与操作
  • expr1 -o expr2 求或操作

例子

按文件名查找

查找当前目录树中,名字以 fileA_fileB_ 开头的所有文件:

1
$ find . -name 'fileA_*' -o -name 'fileB_*'

查找当前目录树中的 foo.cpp 文件,查找过程中排查掉 .svn 子目录树:

1
$ find . -name 'foo.cpp' '!' -path '.svn'

查找当前目录树中,以 my 开头的常规文件,并输出文件详情:

1
$ find . -name 'my*' -type f -ls

按大小查找

查找大小在 100k~500k 的文件:

1
$ find . -size +100k -a -size -500k

查找空文件:

1
$ find . -size 0k

查找非空文件:

1
$ find . ! -size 0k

删除文件或目录

删除空文件或空目录:

1
2
$ find . -type f -empty -delete
$ find . -type d -empty -delete

根据 inode 号删除乱码文件::

1
$ find . -inum <inode-number> -exec rm -i {} \;

1
$ rm `find ./ -inum <inode-number>`

find -exec

依赖注入

按照传统的做法,每个对象负责管理与自己相互协作的对象(即它所依赖的对象)的引用,这将会导致高度耦合和难以测试的代码:

1
2
3
4
5
6
7
8
9
            +---+
+---new--->Bar|
| +---+
+-+-+
|Foo|
+-+-+
| +---+
+---new--->Baz|
+---+

通过依赖注入(Dependency Injection)这种设计模式,对象的依赖关系将由系统中负责协调各对象的第三方组件在创建对象的时候进行设定。对象无需自行创建或管理它们的依赖关系,依赖关系将被自动注入到需要它们的对象当中去,即做到“控制反转(IoC)”:

1
2
3
4
5
6
7
8
9
             +---+
+--inject--+Bar|
| +---+
+-v-+
|Foo|
+-^-+
| +---+
+--inject--+Baz|
+---+

如果一个对象只通过接口(而不是具体实现或初始化过程)来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行替换。这就是依赖注入所带来的最大收益——松耦合。

对依赖进行替换的一个最常用方法就是在测试的时候使用 mock 实现。

如何实现?

在面向对象的编程中,有几种实现控制反转的基本技术:

  • 使用工厂模式(factory pattern)
  • 使用服务定位模式(service locator pattern)
  • 使用以下任何给定类型的依赖注入(DI)
    • 构造方法注入(a constructor injection)
    • setter 方法注入(a setter injection)
    • 接口注入(an interface injection)

自动装配

自动装配作为依赖注入的实现方式之一,是为了简化依赖注入的配置而生的。

常用的注解:

  • @Autowired
  • @Resource
  • @Inject

Spring 通过它的配置,能够了解这些组成部分是如何装配起来的。这样的话,就可以在不改变所依赖的类的情况下,修改依赖关系。

Collection Injection

例子一

1
2
3
4
5
6
7
// Spring 会将 service 对象作为集合注入到 list
@Autowired
private List<DemoService> demoServices;

// Spring 会将 service 的名字作为 key,service 对象作为 value 注入到 Map
@Autowired
private Map<String, DemoService> demoServiceMap;

例子二

下例通过 Collection Injection 实现自定义策略模式。

首先,创建策略注解。注意,此处还使用了 @Service,表示标注了 @PayMethod 注解的类都由 Spring Bean Factory 来创建对象并管理 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface PayMethod {

PayMethodEnum code();

@RequiredArgsConstructor
@Getter
enum PayMethodEnum {
...
}

}

然后,创建策略类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 策略接口
*/
public interface XxxHandler {
...
}

/**
* 策略实现
*/
@PayMethod(code = PayMethod.PayMethodEnum.CARD)
public class CardPayHandler implements XxxHandler {
...
}

最后,创建工厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.apache.commons.lang3.NotImplementedException;

@Service
public class XxxHandlerFactoryBean {

private Map<PayMethod.PayMethodEnum, XxxHandler> map;

/**
* 通过构造方法注入策略实现
*/
@Autowired
public XxxHandlerFactory(List<XxxHandler> handlers) {
this.map = handlers.stream()
.filter(handler -> getAnnotation(handler) != null)
.collect(Collectors.toMap(handler -> getAnnotation(handler).code(), Function.identity()));
}

private PayMethod getAnnotation(XxxHandler handler) {
return handler.getClass().getAnnotation(PayMethod.class);
}

public XxxHandler getHandler(String code) {
PayMethod.PayMethodEnum payMethodEnum = PayMethod.PayMethodEnum.valueOfCode(code);
XxxHandler handler = map.get(payMethodEnum);
if (handler == null) {
throw new NotImplementedException("Not Implemented");
}
return handler;
}

}

循环依赖问题

Spring 循环依赖那些事儿(含 Spring 详细流程图)| 阿里技术

参考

https://en.wikipedia.org/wiki/Dependency_injection

Spring in Action, 4th

Dependency Injection, Design patterns using Spring and Guice

IoC模式(依赖、依赖倒置、依赖注入、控制反转)

Spring 探索丨既生 @Resource,何生 @Autowired?| 阿里技术

@Autowired, @Resource, @Inject 这几个关于Spring 依赖注入的问题你清楚吗?

注解 @Autowired 是如何实现的?

使用 @Autowired 为什么会被 IDEA 警告,应该怎么修改最佳?

Bean 的生命周期

Spring Bean Factory 负责管理 bean 的生命周期,可以分为三个阶段:

Spring Bean Life Cycle

一、初始化阶段:

  • Instantiation:Spring 启动,查找并加载需要被 Spring 管理的bean,进行 Bean 的实例化,调用构造方法。
  • Populate Properties:属性注入,包括引用的 Bean 和值,调用 setter 方法。
  • 调用该 Bean 实现的各种生命周期回调接口。

二、就绪阶段:

  • 此时,Bean 已经准备就绪,可以被应用程序使用了。它们将一直驻留在应用上下文中,直到应用上下文被销毁。

三、销毁阶段:

  • 调用该 Bean 实现的各种生命周期回调接口。

ApplicationContextInitializer

ApplicationContextInitializer

Callback interface for initializing a Spring ConfigurableApplicationContext prior to being refreshed.

Typically used within web applications that require some programmatic initialization of the application context. For example, registering property sources or activating profiles against the context’s environment.

初始化和销毁方法

Spring 框架提供了以下几种方式指定 bean 生命周期的初始化和销毁回调方法:

初始化 销毁
实现 Spring Boot 的接口 ApplicationRunner
实现 Spring Framework 的接口 InitializingBean DisposableBean
在 Spring @Bean 注解中指定属性 @Bean(initMethod="xxx") @Bean(destroyMethod="xxx")
在 Spring bean 配置文件指定属性 <bean init-method="xxx" /> <bean destroy-method="xxx" />
使用 JavaEE 规范 javax.annotation 包中提供的注解 @PostConstruct @PreDestroy

在 Spring 容器启动后执行一些初始化逻辑是一个很常见的场景,注意使用不同的方式,顺序不同:

参考:https://zhuanlan.zhihu.com/p/44786291

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class OssUtil {

@Value("${oss.endpoint}")
private String endpoint;

@Value("${oss.access-key-id}")
private String accessKeyId;

@Value("${oss.access-key-secret}")
private String accessKeySecret;

@Value("${oss.bucket-name}")
private String bucketName;

private OSS ossClient;

@PostConstruct
private void construct() {
ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}

@PreDestroy
private void destory() {
ossClient.shutdown();
}

}

Aware 接口

在日常的开发中,我们经常需要用到 Spring 容器本身的功能资源,可以通过 Spring 提供的一系列 Aware (org.springframework.beans.factory) 子接口来实现具体的功能。Aware 是一个具有标识作用的超级接口,实现该接口的 bean 具有被 Spring 容器通知的能力,而被通知的方式就是通过回调,以依赖注入的方式为 bean 设置相应属性,这是一个典型的依赖注入的使用场景。Aware 接口的继承关系如下:

Aware 接口

这些 *Aware 子接口在 Spring Bean 的生命周期中被回调的顺序如下:

  1. BeanNameAware (org.springframework.beans.factory)
  2. BeanClassLoaderAware (org.springframework.beans.factory)
  3. BeanFactoryAware (org.springframework.beans.factory)
  4. EnvironmentAware (org.springframework.context)
  5. EmbeddedValueResolverAware (org.springframework.context)
  6. ResourceLoaderAware (org.springframework.context)
  7. ApplicationEventPublisherAware (org.springframework.context)
  8. MessageSourceAware (org.springframework.context)
  9. ApplicationContextAware (org.springframework.context)

例子

通过 ApplicationContextAware 注入 ApplicationContext,用以主动获取 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Service
public class XxxHandlerFactoryBean implements ApplicationContextAware {

private final Map<String, Class<?>> map = new HashMap<>();

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 通过注解获取 Beans
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(PayMethod.class);
beans.forEach((beanName, bean) -> {
Class<?> beanClass = bean.getClass();
PayMethod payMethod = beanClass.getAnnotation(PayMethod.class);
map.put(payMethod.code(), beanClass);
});
}

public Class<?> getBeanClass(String code) {
Class<?> aClass = map.get(code);
if (aClass == null) {
throw new NotImplementedException("Not Implemented");
}
return aClass;
}

}

Bean 的作用域

使用 @Scope 注解定义 bean 的作用域,它可以与 @Component@Bean 一起使用:

  • 单例(Singleton):在整个应用中,只创建bean的一个实例。

  • 原型(Prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的bean实例。注意,此时目标 bean 要使用代理模式,否则无法达到效果:

    1
    2
    3
    4
    5
    // 目标 bean 为类
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

    // 目标 bean 为接口
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.INTERFACES)
  • 会话(Session):在Web应用中,为每个会话创建一个bean实例。

  • 请求(Rquest):在Web应用中,为每个请求创建一个bean实例。

参考

org.springframework.beans.factory.Aware

The IoC Container - Bean Scopes

简介

org.springframework.beans.factory.FactoryBean 用于在 IoC 容器中创建其它 Bean,该接口定义如下:

1
2
3
T getObject()  // Return an instance (possibly shared or independent) of the object managed by this factory.
Class<?> getObjectType() // Return the type of object that this FactoryBean creates, or null if not known in advance.
boolean isSingleton() // Is the object managed by this factory a singleton? That is, will getObject() always return the same object (a reference that can be cached)?

有哪些现存的 FactoryBean?例如:

  • 当需要从 JNDI 查找对象(例如 DataSource)时,可以使用 JndiObjectFactoryBean
  • 当使用 Spring AOP 为 bean 创建代理时,可以使用 ProxyFactoryBean
  • 当需要在 IoC 容器中创建 Hibernate 的 SessionFactory 时,可以使用 LocalSessionFactoryBean
  • 当需要在 IoC 容器中创建 MyBatis 的 SqlSessionFactory 时,可以使用 SqlSessionFactoryBean

使用方式

这里我们举一个例子:

如果要为某个接口生成 JDK 动态代理,且将该代理对象放入 Spring IoC 容器,以便后续依赖注入使用,可以自定义实现 FactoryBean 实现如下效果,如图:

HttpApiService_example

实现代码如下,首先创建接口及其代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Xxx 接口
*/
public interface HttpApiService {
HttpRespDTO<XxxRespDTO> api1(XxxReqDTO reqDTO);
}

/**
* Xxx 接口的动态代理实现
*/
public class HttpApiServiceProxy implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
......
}
}

一、手工注册 1.0

如果只需创建一个 FactoryBean,可以将其作为一个 Java Config 加上 @Configuration 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Xxx 接口工厂,用于创建代理实现
*/
@Configuration
public class HttpApiServiceFactoryBean implements FactoryBean<HttpApiService> {

private static final Class<?> API_INTERFACE = HttpApiService.class;

@Override
public HttpApiService getObject() {
return (HttpApiService) Proxy.newProxyInstance(
API_INTERFACE.getClassLoader(), new Class[]{ API_INTERFACE }, new HttpApiServiceProxy());
}

@Override
public Class<?> getObjectType() {
return API_INTERFACE;
}

@Override
public boolean isSingleton() {
return true;
}
}

二、手工注册 2.0

如果需要创建多个 FactoryBean,可以使用泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Setter
public class HttpApiServiceFactoryBean<T> implements FactoryBean<T> {

private Class<T> apiService;

@SuppressWarnings("unchecked")
@Override
public T getObject() {
return (T) Proxy.newProxyInstance(
apiService.getClassLoader(), new Class[]{ apiService }, new HttpApiServiceProxy());
}

@Override
public Class<?> getObjectType() {
return apiService;
}

@Override
public boolean isSingleton() {
return true;
}

}

Java Config 如下,手工创建多个 FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class HttpApiServiceConfig {
@Bean
public HttpApiServiceFactoryBean<UserHttpApiService> userHttpApiService() {
return new HttpApiServiceFactoryBean(UserHttpApiService.class);
}

@Bean
public HttpApiServiceFactoryBean<RoleHttpApiService> roleHttpApiService() {
return new HttpApiServiceFactoryBean(RoleHttpApiService.class);
}
}

这种方式参考了 Mybatis-Spring 注册映射器

三、自动发现

如果想进一步省略 Java Config,做到自动扫描并创建 FactoryBean,可以创建自动配置类。例如,为添加了 @HttpApi 注解的接口创建相应的 FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* HttpApi 动态代理自动配置类
**/
@Configuration
@Import({AutoConfiguredHttpApiScannerRegistrar.class})
public class HttpApiServiceAutoConfig {
}

/**
* 自定义 ImportBeanDefinitionRegistrar
**/
@Slf4j
class AutoConfiguredHttpApiScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware {

// 扫描 Application 类所在的 classpath。也可以指定其它路径(如配合 @EnableHttpApi 注解使用)
private static final String BASE_PKG = Application.class.getPackage().getName();
private ResourceLoader resourceLoader;

@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
try {
log.debug("Searching for HttpApiService annotated with @HttpApi from {}", BASE_PKG);

// 配置自定义的 ClassPathHttpApiScanner
ClassPathHttpApiScanner scanner = new ClassPathHttpApiScanner(registry);
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
// 扫描指定路径
scanner.doScan(BASE_PKG);
} catch (IllegalStateException ex) {
log.debug("Could not determine auto-configuration package, automatic @HttpApi scanning disabled.", ex);
}
}

@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}

/**
* 自定义 ClassPathHttpApiScanner
**/
class ClassPathHttpApiScanner extends ClassPathBeanDefinitionScanner {

public ClassPathHttpApiScanner(BeanDefinitionRegistry registry) {
super(registry, false);
}

@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 添加过滤条件,这里是只要添加了 @HttpApi 注解的类或接口,就会被扫描到
addIncludeFilter(new AnnotationTypeFilter(HttpApi.class));

// 调用 Spring 的扫描
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

if (beanDefinitions.isEmpty()) {
logger.warn( "No @HttpApi bean was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
}
// 处理扫到的 BeanDefinition
else {
processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}

private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
GenericBeanDefinition definition;
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (GenericBeanDefinition) holder.getBeanDefinition();

String beanClassName = definition.getBeanClassName();

// 修改 BeanDefinition 的 BeanClass 为 FactoryBean
definition.setBeanClass(HttpApiServiceFactoryBean.class);

// 为构造方法指定所需参数
// definition.getPropertyValues().add("restTemplate", new RuntimeBeanReference("restTemplate"));
definition.getPropertyValues().add("apiService", getClass(beanClassName));
}
}

private Class<?> getClass(String beanClassName){
try {
return Class.forName(beanClassName);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}

/**
* 覆盖原有策略,限定只需要接口类型
*/
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}

}

这种方式参考了 Mybatis-Spring 发现映射器

需要用到 Spring 的几个类,待补充:

  • ImportBeanDefinitionRegistrar
  • ClassPathBeanDefinitionScanner
    • BeanDefinitionHolder
    • GenericBeanDefinition
    • AnnotatedBeanDefinition

BeanDefinition

参考

Spring BeanFactory和FactoryBean的区别

org.springframework.beans.factory.FactoryBean

ThreadPoolExecutorFactoryBean

LocalSessionFactoryBean

SqlSessionFactoryBean

MyBatis-Spring 注入映射器

spring-bean

Spring 组件引入的两种推荐方式:

  • 非 Spring Boot 项目,显示引入 Java Config:@Enable* + @Import
  • Spring Boot 项目,隐式引入 Java Config:@EnableAutoConfiguration + META-INF/spring.factories

Spring bean 的声明及装配的几种配置方式:

  • 基于 XML Config 的显式配置,不推荐
  • 基于 Java Config 的显式配置,推荐用于声明第三方编写的组件
  • 自动化配置,即组件扫描(隐式的 bean 发现机制) + 自动装配,推荐用于自己编写的组件

用户可以选择其中一种方式使用,也可以混搭使用。使用时的最佳实践如下:

  • 建议尽可能地使用自动化配置的机制。显式配置越少越好,以避免显式配置所带来的维护成本。
  • 当你必须要显式配置 bean 的时候(比如,有些源码不是由你来维护的,而当你需要为这些代码配置 bean 的时候),推荐使用类型安全并且比 XML Config 更加强大的 Java Config。
  • 最后,只有当你想要使用便利的 XML 命名空间,并且在 JavaConfig 中没有同样的实现时,才应该使用 XML Config。

自动化配置

Spring 从两个角度来实现 bean 的自动化配置:

  • 组件扫描(component scanning):Spring 会自动发现应用上下文中要创建的 bean。
  • 自动装配(autowiring):Spring 自动满足 bean 之间的依赖。

组件扫描(隐式的 bean 发现机制)和自动装配组合在一起能够发挥出强大的威力,它们能够将你的显式配置降低到最少。

组件声明

注解:

  • @Named

  • @Component

    Indicates that an annotated class is a “component”. Such classes are considered as candidates for auto-detection when using annotation-based configuration and classpath scanning.
    Other class-level annotations may be considered as identifying a component as well, typically a special kind of component: e.g. the @Repository annotation or AspectJ’s @Aspect annotation.

  • @Controller@RestController@Service@Repository

例子:

1
2
3
4
5
6
7
8
9
10
// 接口
public interface CompactDisc { void play(); }

// @Component 注解表明该类会作为组件类,并告知 Spring 要为这个类创建 bean。因此没有必要在 XML 或 Java Config 中显式配置该 bean。
@Component
public class SgtPeppers implements CompactDisc {
public void play() {
System.out.println("Hello world!");
}
}

组件扫描

注解:

  • @Configuration

  • @ComponentScan

    Configures component scanning directives for use with @Configuration classes. Provides support parallel with Spring XML’s <context:component-scan> element.
    Either basePackageClasses() or basePackages() (or its alias value()) may be specified to define specific packages to scan. If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.
    Note that the <context:component-scan> element has an annotation-config attribute; however, this annotation does not. This is because in almost all cases when using @ComponentScan, default annotation config processing (e.g. processing @Autowired and friends) is assumed. Furthermore, when using AnnotationConfigApplicationContext, annotation config processors are always registered, meaning that any attempt to disable them at the @ComponentScan level would be ignored.
    See @Configuration‘s Javadoc for usage examples.

例子:

1
2
3
4
5
6
7
8
// 显式声明 Java Config 配置类
@Configuration
// 组件扫描默认是不启用的。我们还需要显式配置一下 Spring,从而命令它去寻找带有 @Component 注解的类,并为其创建 bean。
// @ComponentScan 默认会扫描与配置类相同的包及其子包。有一个原因会促使我们明确地设置基础包,那就是我们想要将配置类放在单独的包中,使其与其他的应用代码区分开来。
@ComponentScan
public class CDPlayerConfig {
// 没有显式地声明任何 bean,但由于开启了组件扫描,会在 Spring 容器中自动创建一个 SgtPeppers 类的 bean。
}

自动装配

注解:

  • @Autowired
  • @Resource
  • @Inject

例子:为了测试组件扫描的功能,我们创建一个简单的 JUnit 单元测试。它会创建 Spring 上下文,并判断 bean 是否真的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringJUnit4ClassRunner.class) // 用于自动创建 Spring 的应用上下文
@ContextConfiguration(classes=CDPlayerConfig.class) // 指定要加载的配置
public class CDPlayerTest {

// @Autowired 是 Spring 特有的注解,也可以使用 Java 依赖注入规范的 @Inject
@Autowired
private CompactDisc cd;

@Test
public void cdShouldNotBeNull() {
assertNotNull(cd); // 测试通过
cd.play(); // 输出 Hello world!
}

}

如何处理自动装配的歧义性问题?有两种方案:

  • 使用 @Primary 注解将可选 bean 中的某一个设为首选的 bean。@Primary 能够与 @Component 组合用在组件扫描的 bean 上,也可以与 @Bean 组合用在 Java 配置的 bean 声明中。
  • 使用限定符注解 @Qualifier 来帮助 Spring 将可选的 bean 的范围缩小到只有一个 bean。

更多详见:《Spring Bean 自动装配总结

基于 Java Config 的显式配置

尽管在很多场景下通过组件扫描和自动装配实现 Spring 的自动化配置是更为推荐的方式,但有时候自动化配置的方案行不通,因此需要显式配置 Spring。比如说,你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加 @Component@Autowired 注解的,因此就不能使用自动化装配的方案了。

在这种情况下,就必须要采用显式装配的方式。在进行显式配置的时候,有两种可选方案:Java 和 XML。Java Config 的优缺点如下:

  • 优点:类型安全,对重构友好且不易出错。因为它就是 Java 代码,就像应用程序中的其它 Java 代码一样。
  • 缺点:如果修改了 Java Config 类中的配置,就必须重新编译应用程序。

同时,Java Config 与其它的 Java 代码又有所区别,在概念上,它与应用程序中的业务逻辑和领域代码是不同的。尽管它与其它的组件一样都使用相同的语言进行表述,但 Java Config 是配置代码。这意味着它不应该包含任何业务逻辑,Java Config 也不应该侵入到业务逻辑代码之中。尽管不是必须的,但通常会将 Java Config 放到单独的包中,使它与其他的应用程序逻辑分离开来,这样对于它的意图就不会产生困惑了。

@Bean

下面是一个 Java Config 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 显式声明 Java Config 配置类
@Configuration
public class CDPlayerConfig {

// 显式地声明 bean。@Bean 注解会告诉 Spring 这个方法将会返回一个对象,该对象要注册为 Spring 应用上下文中的 bean。方法体中包含了最终产生 bean 实例的逻辑。
@Bean
public CompactDisc getCompactDisc() { return new SgtPeppers(); }

// 装配方式一:Spring 将会拦截所有对 getCompactDisc() 的调用,并确保直接返回该方法所创建的 bean,而不是每次都对其进行实际的调用。且默认情况下,Spring 中的 bean 都是单例的,因此多次调用只会返回同一个实例。
@Bean
public CDPlayer getCDPlayer1() {
return new CDPlayer(getCompactDisc());
}

// 装配方式二:上述通过调用方法来引用 bean 的方式有点令人困惑。而下面这种方式的好处是:
// 1.不要求将 CompactDisc 声明到同一个配置类之中。
// 2.不关注 Bean 的配置方式,你可以将配置分散到多个配置类、XML 文件以及自动扫描和装配 bean 之中,只要功能完整健全即可。
@Bean
public CDPlayer getCDPlayer2(CompactDisc cd) {
return new CDPlayer(cd);
}

}

注意,这个 Java Config 需要放在 @ComponentScan 能够扫描到的路径之下,否则配置中所声明的 bean 将无法被 Spring 容器所注册。

@Import

有时候我们需要引入一些外部的 Java Config 配置,这些配置往往是在其它 package 下。此时可以通过 @Import 注解导入这些外部 Java Config。

@Enable*

@Import 注解导入 Java Config 的方式有时不够直观,主流的做法是为其包装一层 @Enable* 注解,其字面意思是“开启某个功能”,非常直观。Spring 框架中提供了大量这类注解:

Spring Framework:

  • spring-context
    • @EnableAsync 开启对 @Async 注解的支持
    • @EnableScheduling 开启对 @Scheduled 注解的支持
    • @EnableCaching 开启对 @Cacheable 注解的支持
    • @EnableAspectJAutoProxy 开启对 @Aspect 注解的支持
    • @EnableLoadTimeWeaving
    • @EnableMBeanExport
  • spring-tx
    • @EnableTransactionManagement 开启对 @Transactional 注解的支持
  • spring-webmvc
    • @EnableWebMvc 开启对 @Controller 注解的支持
  • spring-webflux
    • @EnableWebFlux
  • spring-websocket
    • @EnableWebSocket
    • @EnableWebSocketMessageBroker
  • spring-jms
    • @EnableJms

其它组件:

  • spring-security
    • @EnableWebSecurity
  • spring-data-jpa
    • @EnableJpaRepositories
  • spring-boot-autoconfigure
    • @EnableAutoConfiguration

通过简单的 @Enable* 即可开启一项功能的支持,从而避免大量配置,大大降低使用难度。通过观察这些 @Enable* 注解的源码,可以发现所有的注解都有一个 @Import 注解,@Import 是用来导入配置类的,这也就意味着这些自动开启的实现其实就是导入了一些自动配置的 Bean。这些导入的配置主要分为以下三类:

  1. 直接导入配置类
  2. 依据条件选择配置类
  3. 动态注册 Bean

@Conditional

假设你希望实现条件化的 bean,例如:

  • 某个 bean 只有在应用的类路径下包含特定的库时才创建;

  • 某个 bean 只有当另外某个特定的 bean 也声明了之后才会创建;

  • 某个特定的环境变量设置之后,才会创建某个 bean。

在 Spring 4 之前,很难实现这种级别的条件化配置,但是 Spring 4 引入了一个新的 @Conditional 注解,它可以用到带有 @Bean注解的方法上。如果给定的条件计算结果为 true,就会创建这个 bean,否则的话,这个 bean 会被忽略。

详情参考另一篇博文:《Spring Bean 条件化配置总结

基于 XML 的显式配置

XML 配置的缺点是比较复杂,且无法从编译期的类型检查中受益。除非是老项目维护,否则在新项目中已不再建议使用,此处不作过多介绍。

混合配置

在典型的 Spring 应用中,我们可能会同时使用自动化和显式配置。这些配置方案不是互斥的,可以将 Java Config 的组件扫描和自动装配和/或 XML 配置混合在一起:

1
2
3
4
5
6
7
8
9
// 创建一个全局的根配置,并组合各种配置
@Configuration
// 通常会在根配置中启用组件扫描
@ComponentScan
// 导入 Java Config
@Import({FirstConfig.class, SecondConfig.class})
// 导入 XML 配置
@ImportResource("classpath:applicationContext.xml")
public class GlobalConfig() {}

参考

Spring in Action, 4th

使用 Java 配置进行 Spring bean 管理

Package org.springframework.context.annotation

Annotation support for the Application Context, including JSR-250 “common” annotations, component-scanning, and Java-based metadata for creating Spring-managed objects.

Spring4.x高级话题(六):@Enable*注解的工作原理

How those Spring @Enable* Annotations work

详细讲解Spring中的@Bean注解

org.springframework.beansorg.springframework.context 包为 Spring 框架 IoC 容器的提供基础。有两种形式的 Spring 容器:

  • Bean Factory
  • Application Context
    • spring-context 核心模块
      • FileSystemXmlapplicationcontext
      • ClassPathXmlApplicationContext
      • AnnotationConfigApplicationContext
    • spring-web 模块
      • XmlWebApplicationContext
      • AnnotationConfigWebApplicationContext

继承结构如下:

BeanFactory

Application Context

Spring 通过应用上下文(Application Context)装载 bean 的定义并将它们装配起来。Spring 应用上下文全权负责对象的创建、装配、配置它们并管理它们的整个生命周期。Spring 自带了多种应用上下文的实现,它们之间主要的区别仅仅在于如何加载配置:

XML

  • FileSystemXmlapplicationcontext 从文件系统下的一个或多个 XML 配置文件中加载上下文定义:

    1
    ApplicationContext ctx = new FileSystemXmlApplicationContext("c:/applicationContext.xml");
  • ClassPathXmlApplicationContext 从类路径下的一个或多个 XML 配置文件中加载上下文定义:

    1
    ApplicationContext ctx = new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
  • XmlWebApplicationContext 从 Web 应用下的一个或多个 XML 配置文件中加载上下文定义,是 Web 应用程序使用的默认上下文类,因此不必在 web.xml 文件中显式指定这个上下文类。以下代码描述了 web.xml 中指向将由 ContextLoaderListener 监听器类载入的外部 XML 上下文文件的元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <web-app>
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
    <listener-class>
    org.springframework.web.context.ContextLoaderListener
    </listener-class>
    </listener>
    <servlet>
    <servlet-name>sampleServlet</servlet-name>
    <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    </servlet>

    ...
    </web-app>

Annotation

  • AnnotationConfigApplicationContext 从一个或多个基于 Java 的配置类中加载 Spring 应用上下文:

    1
    2
    3
    4
    5
    // 使用构造函数来注册配置类 DubboApplication
    ApplicationContext ctx = new AnnotationConfigApplicationContext(org.apache.dubbo.config.DubboApplication.class);

    // 此外,还可以使用 register 方法来注册配置类 OtherApplication
    ctx.register(OtherApplication.class)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    package org.apache.dubbo.config;

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;

    @Configuration
    public class DubboApplication {

    // 注册配置类将自动注册 @Bean 注解的方法名称返回的 bean
    @Bean
    public ApplicationConfig applicationConfig() {
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("dubbo-annotation-provider");
    return applicationConfig;
    }
    }
  • AnnotationConfigWebApplicationContext 从一个或多个基于 Java 的配置类中加载 Spring Web 应用上下文,需要显示配置该类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <web-app>
    <context-param>
    <param-name>contextClass</param-name>
    <param-value>
    org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
    </context-param>
    <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
    demo.AppContext
    </param-value>
    </context-param>
    <listener>
    <listener-class>
    org.springframework.web.context.ContextLoaderListener
    </listener-class>
    </listener>
    <servlet>
    <servlet-name>sampleServlet</servlet-name>
    <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
    <param-name>contextClass</param-name>
    <param-value>
    org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
    </init-param>
    </servlet>

    ...
    </web-app>

Bean Factory

org.springframework.beans.factory.BeanFactory

参考

Spring in Action, 4th

org.springframework.beans.factory.BeanFactory