Qida's Blog

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

Spring 4.0 引入的条件化注解

假设你希望一个或多个 bean 只有在应用的类路径下包含特定的库时才创建。或者我们希望某个 bean 只有当另外某个特定的 bean 也声明了之后才会创建。我们还可能要求只有某个特定的环境变量设置之后,才会创建某个 bean。
在 Spring 4 之前,很难实现这种级别的条件化配置,但是 Spring 4 引入了一个新的 @Conditional 注解,它可以用到带有 @Bean 注解的方法上。如果给定的条件计算结果为 true,就会创建这个 bean,否则的话,这个 bean 会被忽略。

@Conditional 注解的源码如下:

1
2
3
4
5
6
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}

Condition 接口的源码如下:

1
2
3
public interface Condition {
boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

其中,通过 Condition 接口的入参 ConditionContext,我们可以做到如下几点:

  • 借助 getRegistry() 返回的 BeanDefinitionRegistry 检查 bean 定义;
  • 借助 getBeanFactory() 返回的 ConfigurableListableBeanFactory 检查 bean 是否存在,甚至探查 bean 的属性;
  • 借助 getEnvironment() 返回的 Environment 检查环境变量是否存在以及它的值是什么;
  • 读取并探查 getResourceLoader() 返回的 ResourceLoader 所加载的资源;
  • 借助 getClassLoader() 返回的 ClassLoader 加载并检查类是否存在。

环境与 profile

在开发软件的时候,有一个很大的挑战就是将应用程序从一个环境迁移到另外一个环境。开发阶段中,某些环境相关做法可能并不适合迁移到生产环境中,甚至即便迁移过去也无法正常工作。跨环境部署时会发生变化的几个典型例子:

  • 数据库配置
  • 加密算法
  • 与外部系统的集成

解决办法:

  • 在单独的 Java Config(或 XML)中配置每个 bean,然后在构建时根据不同的环境分别打包,典型方法是采用 Maven profile。这种方式的问题在于要为每种环境重新构建应用。
  • 运行时指定不同的环境变量,典型方法是采用 Spring profile bean。这种方式的好处在于无需为每种环境重新构建应用。

Spring profile bean 的使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@Profile("dev") // 类级别(Spring 3.1 引入)
public class DevProfileConfig() {

}

@Configuration
public class GlobalConfig() {

@Bean
@Profile("dev") // 方法级别(Spring 3.2 引入)
public DataSource embeddedDataSource() {
...
}

@Bean
@Profile("prod") // 方法级别(Spring 3.2 引入)
public DataSource jndiDataSource() {
...
}

}

激活方式,使用属性:spring.profiles.activespring.profiles.default。有多种方式来设置这两个属性:

  • 作为 DispatcherServlet 的初始化参数;
  • 作为 Web 应用的上下文参数;
  • 作为 JNDI 条目;
  • 作为环境变量;
  • 作为 JVM 的系统属性;
  • 在集成测试类上,使用 @ActiveProfiles 注解设置。

注意,Spring profile bean 注解 @Profile 底层其实也是基于 @ConditionalCondition 实现:

1
2
3
4
5
6
7
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}

Spring Boot 的条件化注解

Spring Boot 没有引入任何形式的代码生成,而是利用了 Spring 4 的条件化 bean 配置特性,以及 Maven 和 Gradle 提供的传递依赖解析,以此实现 Spring 应用上下文里的自动配置。Spring Boot 实现的条件化注解如下:

Spring Boot 实现的条件化注解

Spring Boot 的 @Conditional 注解实现及相关支持类,详见文档:Package org.springframework.boot.autoconfigure.condition

依赖配置:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

通过运行 DEBUG=true mvn spring-boot:run,可以看到 DEBUG 级别的日志输出,从而观察自动配置的详情(AUTO-CONFIGURATION REPORT):

  • Positive matches
  • Negative matches
  • Exclusions
  • Unconditional classes
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
============================
CONDITIONS EVALUATION REPORT
============================

Positive matches:
-----------------

GenericCacheConfiguration matched:
- Cache org.springframework.boot.autoconfigure.cache.GenericCacheConfiguration automatic cache type (CacheCondition)

JmxAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.jmx.export.MBeanExporter' (OnClassCondition)
- @ConditionalOnProperty (spring.jmx.enabled=true) matched (OnPropertyCondition)

PropertyPlaceholderAutoConfiguration#propertySourcesPlaceholderConfigurer matched:
- @ConditionalOnMissingBean (types: org.springframework.context.support.PropertySourcesPlaceholderConfigurer; SearchStrategy: current) did not find any beans (OnBeanCondition)


Negative matches:
-----------------

ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

AopAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class 'org.aspectj.lang.annotation.Aspect' (OnClassCondition)

Exclusions:
-----------

None

Unconditional classes:
----------------------

org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration

org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration

参考

Spring in Action, 4th

SpringBoot源码分析之条件注解的底层实现

工作中由于前端项目打包、各类工具使用(如本博客就是基于 Node.js)经常要用到 npm,下面总结一下。

npm 是随 Node.js 自动安装的,由三个独立的部分组成:

  • 网址:开发者查找包(package)、设置参数以及管理 npm 使用体验的主要途径。
  • 注册表(Registry):一个巨大的数据库,保存了每个包(package)的信息。
  • 命令行工具(CLI):命令行或终端运行。开发者通过 CLI 与 npm 打交道。

安装

OS X 推荐使用 HomeBrew 安装:

1
brew install node --with-npm

其它:

https://nodejs.org/zh-cn/download/

npm 命令

NPM CLI Commands

npm install -g <package> 全局安装:

  • Windows:

    • 安装目录 C:\Users\Admin\AppData\Roaming\npm\node_modules\<package>
    • 生成可执行脚本:C:\Users\Admin\AppData\Roaming\npm\<package>
    • 由于目录 C:\Users\Admin\AppData\Roaming\npm 已加入 PATH 环境变量,因为可以直接执行对应命令。
  • OS X:

    • 安装目录 /usr/local/lib/node_modules/<package>
    • 生成软链,例如:/usr/local/bin/npm -> /usr/local/lib/node_modules/npm/bin/npm-cli.js

npm list -g -depth=0 验证是否安装到位。

npm 配置

npm 配置主要有两份:

  • npmrc:npm 从命令行、环境变量、npmrc 文件中读取配置。
  • package.json:包(package)配置文件。

重点了解下 package.json,重点属性有 nameversiondependenciesmain

属性 描述
name 包名
version 包的语义版本号:X.Y.Z:如果有大变动,向下不兼容,需要更新主版本号X;如果是新增了功能,但是向下兼容,需要更新次版本号 Y;如果只是修复bug,需要更新补丁版本号 Z
description 包的描述
homepage 包的官网 url
author 包的作者姓名
contributors 包的其他贡献者姓名
dependencies 依赖包列表。如果依赖包没有安装,npm 会自动将依赖包安装在 node_module 目录下
repository 包代码存放的地方的类型,可以是 git 或 svn,git 可在 Github 上
main main 字段是一个模块ID,它是一个指向你程序的主要项目。就是说,如果你包的名字叫 express,然后用户安装它,然后require("express")
keywords 关键字

淘宝 npm 镜像

为了解决慢的问题,推荐使用淘宝 npm 镜像。三种使用方式:

  • 使用淘宝定制的 cnpm 命令行工具代替默认的 npm,安装方法:

    1
    $ npm install -g cnpm --registry=https://registry.npm.taobao.org
  • 永久配置 npm 命令行工具:

    1
    2
    3
    $ npm config set registry https://registry.npm.taobao.org
    --配置后验证是否成功
    $ npm config get registry
  • npm 安装包时,临时使用淘宝 npm 镜像:

    1
    $ npm install vue-cli --registry=https://registry.npm.taobao.org

参考

https://www.npmjs.com/

https://www.npmjs.com.cn/

https://npm.taobao.org/

http://www.runoob.com/nodejs/nodejs-tutorial.html

Spring Boot 应用中的”自动配置”是通过 @EnableAutoConfiguration 注解进行开启的。@EnableAutoConfiguration 可以帮助 Spring Boot 应用将所有符合条件的 @Configuration 配置类的 bean 都加载到 Spring IoC 容器中。本文解析了实现这个效果的原理。

自动配置的原理

Spring Boot 应用的自动配置流程如下:

Spring SPI

Spring Factories 机制

首先,注解的实现类使用了 spring-core 中的加载类 SpringFactoriesLoader 加载指定配置,详见文档

SpringFactoriesLoader loads and instantiates factories of a given type from “META-INF/spring.factories” files which may be present in multiple JAR files in the classpath. The spring.factories file must be in Properties format, where the key is the fully qualified name of the interface or abstract class, and the value is a comma-separated list of implementation class names.

该类会通过类加载器从 classpath 中搜索所有 META-INF/spring.factories 配置文件,然后获取 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 部分。

例如 spring-boot-autoconfigure.jar/META-INF/spring.factories

1
2
3
4
5
6
7
8
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
......

加载结果如图:

SpringFactoriesLoader 解析结果

继续加载 key 为 org.springframework.boot.autoconfigure.AutoConfigurationImportFilter 部分:

1
2
3
4
5
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

执行条件注解

针对这 118 个候选的 Auto Configure,执行 Import Filters 对应的条件注解:

  • OnClassCondition

    1
    2
    3
    4
    5
    6
    7
    8
    // 底层实现,加载成功则返回 true;加载失败则抛出 java.lang.ClassNotFoundException,捕获异常后返回 false
    private static Class<?> forName(String className, ClassLoader classLoader)
    throws ClassNotFoundException {
    if (classLoader != null) {
    return classLoader.loadClass(className);
    }
    return Class.forName(className);
    }
  • OnBeanCondition

    判断指定的 bean 类或名称是否已存在于 BeanFactory

过滤结果如下:

TRACE 日志如下:

1
Filtered 30 auto configuration class in 1000 ms

总结

在日常工作中,我们可能需要实现一些 Spring Boot Starter 给被人使用,这个时候我们就可以使用这个 Factories 机制,将自己的 Starter 注册到 org.springframework.boot.autoconfigure.EnableAutoConfiguration 命名空间下。这样用户只需要在服务中引入我们的 jar 包即可完成自动加载及配置。Factories 机制可以让 Starter 的使用只需要很少甚至不需要进行配置。

本质上,Spring Factories 机制与 Java SPI 机制原理都差不多:都是通过指定的规则,在规则定义的文件中配置接口的各种实现,通过 key-value 的方式读取,并以反射的方式实例化指定的类型。

spring.factories 已废弃

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.7-Release-Notes

Loading auto-configurations from spring.factories is deprecated.

If you have created your own auto-configurations, you should move the registration from spring.factories to a new file named META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Each line contains the fully qualified name of the auto-configuration. See the included auto-configurations for an example.

For backwards compatibility, entries in spring.factories will still be honored.

《提升维护效率,利用开源项目 mica-auto 自动生成这两个文件》

https://mp.weixin.qq.com/s/ASBRANcdMI2VXflyvD6wiA

参考

Auto-configuration Classes

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/SpringFactoriesLoader.html

https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java

Spring Boot spring.factories vs @Enable annotations

https://www.cnblogs.com/zheting/p/6707035.html

http://www.cnblogs.com/whx7762/p/7832985.html

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