Qida's Blog

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

简介

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

工作中经常需要为新开展的业务创建新工程,如果每次都重新搭建、或者拷贝老项目,这些重复工作会影响开发效率,也不利于维护(例如为新工程统一引入新组件、或升级配置文件等)。Maven 提供了 archetype 骨架插件,用于抽取这些重复的配置和代码,以模板的方式创建新项目。

Maven Archetype Plugin(骨架插件)能够让用户从现有的模板(即骨架)中创建 Maven 项目,也能够从现有的项目中创建骨架。其流程如下:

Maven Archetype Plugin

从上图可见,该插件提供了如下目标(即命令):

  • archetype:generate creates a Maven project from an archetype: asks the user to choose an archetype from the archetype catalog, and retrieves it from the remote repository. Once retrieved, it is processed to create a working Maven project.
  • archetype:create-from-project creates an archetype from an existing project.(注意如果需要包含 yml 配置文件,需要加上参数 -Darchetype.filteredExtentions=yml
  • archetype:crawl search a repository for archetypes and updates a catalog.

下面具体演示如何使用。

创建 archetype 工程样例

方式一

利用 Maven 内置的 maven-archetype-archetype 构件创建一个骨架工程样例:

1
2
3
4
mvn archetype:generate
-DgroupId=[your project's group id]
-DartifactId=[your project's artifact id]
-DarchetypeArtifactId=maven-archetype-archetype

创建成功后,其目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
archetype
|-- pom.xml // archetype pom
`-- src
`-- main
`-- resources
|-- META-INF
| `-- maven
| `--archetype.xml // archetype descriptor
`-- archetype-resources // prototype files
|-- pom.xml // prototype pom
`-- src
|-- main
| `-- java
| `-- App.java
`-- test
`-- java
`-- AppTest.java

骨架由以下四个部分组成,各文件作用如下:

组成部分 组成部分 路径 描述
archetype pom 骨架的 POM 根目录下的 pom.xml
archetype descriptor 骨架描述符文件 src/main/resources/META-INF/maven/archetype.xml 这个文件列出了包含在 archetype 中的所有文件并将这些文件分类,因此 archetype 生成机制才能正确的处理。
prototype pom 新工程的原型 POM src/main/resources/archetype-resources/pom.xml archetype 插件会直接复制这个 pom.xml,然后替换其中的占位符 ${artifactId}${groupId}${version}
prototype files 新工程的原型文件 src/main/resources/archetype-resources/ archetype 插件会直接复制这些文件

方式二

利用公司现有模板项目创建骨架:

1
mvn archetype:create-from-project -Darchetype.filteredExtentions=yml,xml,java,jsp

创建成功后,其目录结构如下:

1
2
3
4
5
6
7
8
9
youproject
|-- pom.xml // 项目源文件
`-- src // 项目源文件
`-- main
`-- test
`-- target // 创建结果
`-- generated-sources
`-- archetype
// 目录结构同方式一。后续安装 archetype 到本地仓库时,需要 cd 到本目录,执行 mvn install;如果是发布到远程仓库,则 mvn deploy

-Darchetype.filteredExtentions 用于指定要过滤的文件后缀名,被过滤的文件将会替换文件里面用到的占位符。在生成的 archetype.xml 文件时,命令将会扫描模板项目中所有的文件类型,为上述指定的文件类型添加 filtered="true" 属性:

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
<fileSet filtered="true" packaged="true" encoding="UTF-8">
<directory>src/main/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>
<fileSet filtered="true" encoding="UTF-8">
<directory>src/main/resources</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
</fileSet>
<fileSet filtered="true" encoding="UTF-8">
<directory>src/main/webapp</directory>
<includes>
<include>**/*.jsp</include>
<include>**/*.xml</include>
</includes>
</fileSet>
<fileSet filtered="true" packaged="true" encoding="UTF-8">
<directory>src/test/java</directory>
<includes>
<include>**/*.java</include>
</includes>
</fileSet>

配置骨架

配置描述符 archetype.xml

然后,配置 archetype.xml,详见:archetype descriptor

配置新工程的 pom.xml

使用占位符 ${artifactId}${groupId}${version},这些变量都将在 archetype:generate 命令运行时被初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${version}</version>
<packaging>jar</packaging>

<name>A custom project</name>
<url>http://www.myorganization.org</url>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

配置新工程的相关文件

将新工程所需文件,全部拷贝到 src/main/resources/archetype-resources/ 目录下。

安装本地仓库

创建骨架并配置完毕,首先安装到本地仓库:

  • 如果是使用 Maven 内置的 maven-archetype-archetype 构件创建的骨架工程样例,直接在该目录下执行安装命令即可。
  • 如果是使用命令 mvn archetype:create-from-project 从现有的项目中创建骨架,需要先 cd 进入到 target/generated-sources/archetype/ 目录,再运行 mvn install
1
mvn install

执行如下插件 goal:

1
2
3
4
5
6
maven-resources-plugin:resources  // 拷贝资源文件
maven-resources-plugin:testResources // 拷贝测试资源文件
maven-archetype-plugin:jar // 在 target 目录下构建出 archetype jar
maven-archetype-plugin:integration-test
maven-install-plugin:install // 将构建出来的 jar 和 pom 安装到本地仓库
maven-archetype-plugin:update-local-catalog // 更新本地仓库根目录下的 archetype-catalog.xml

安装完毕,构建出来的 archetype jar artifactId-archetype-version.jar 将会安装到本地仓库。此时需要更新本地仓库根目录下的 archetype-catalog.xml ,插入一段骨架配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<archetype-catalog xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0 http://maven.apache.org/xsd/archetype-catalog-1.0.0.xsd"
xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<archetypes>
<!-- 新插入的骨架 -->
<archetype>
<groupId>[your project's group id]</groupId>
<artifactId>[your project's artifact id]</artifactId>
<version>xxx</version>
<description>xxx</description>
</archetype>
</archetypes>
</archetype-catalog>

可以使用命令:mvn archetype:crawl,将自动搜索仓库中的骨架并更新骨架配置。

发布到远程仓库

骨架生成成功,并且一切符合预期之后,可以发布到远程仓库供他人使用:

1
mvn deploy

创建新项目

命令行方式

尝试创建项目,选择想要使用的骨架,并为新工程指定 groupIdartifactId,以及包名 package

1
2
3
4
5
6
7
8
mvn archetype:generate                                  \
-DarchetypeGroupId=<archetype-groupId> \
-DarchetypeArtifactId=<archetype-artifactId> \
-DarchetypeVersion=<archetype-version> \
-DgroupId=<my.groupid> \
-DartifactId=<my-artifactId> \
-Dversion=<my.version> \
-Dpackage=my.package

输出日志:

1
2
3
4
5
6
7
8
9
10
[INFO] Using property: groupId = <my.groupid>
[INFO] Using property: artifactId = <my-artifactId>
[INFO] Using property: version = <my.version>
[INFO] Using property: package = my.package
Confirm properties configuration:
groupId: <my.groupid>
artifactId: <my-artifactId>
version: <my.version>
package: my.package
Y:

回复 Y 确认即可。

IDEA

File > New Module > Maven,勾选 Create from archetype,点击 Add Archetype,配置如下:

IDEA 中添加 archetype

输入创建 archetype 工程时,定义的 GroupId、ArtifactId、Version,并选择你远程仓库的地址即可,例如:http://xxx/nexus/content/repositories/snapshots。

配置完毕,创建新工程时,将会执行命令:

1
2
3
4
5
6
7
8
9
-DinteractiveMode=false
-DarchetypeGroupId=
-DarchetypeArtifactId=
-DarchetypeVersion=
-DarchetypeRepository=http://xxx/nexus/content/repositories/snapshots
-DgroupId=
-DartifactId=
-Dversion=
org.apache.maven.plugins:maven-archetype-plugin:RELEASE:generate

goal generate 执行过程中会下载指定的 archetype jar,并根据指定参数创建新工程。

注意,由于这种方式只会下载指定的 archetype jar 到本地仓库,但不会将骨架添加到本地仓库根目录下的骨架目录文件 archetype-catalog.xml 之中。这将会导致在 IDEA 之外以命令行方式执行 mvn archetype:generate 生成新工程时,由于在骨架目录文件中找不到指定 archetype 而报错,因此需要将该 archetype 添加到骨架目录文件下。解决方法是执行命令:mvn archetype:crawl 遍历本地仓库搜索骨架并更新目录文件。

参考

https://maven.apache.org/guides/introduction/introduction-to-archetypes.html

https://maven.apache.org/guides/mini/guide-creating-archetypes.html

http://maven.apache.org/archetype/maven-archetype-plugin/

Maven 本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由 maven-compiler-plugin 完成的。每个插件会有一个或者多个目标(goal),例如 maven-compiler-plugin 插件的 compile 目标用来编译位于 src/main/java/ 目录下的主源码,testCompile 目标用来编译位于 src/test/java/ 目录下的测试源码。

用户可以通过两种方式调用 Maven 插件目标:

  1. 将插件目标与生命周期阶段(lifecycle phase)绑定,这样用户在命令行只是输入生命周期阶段而已,例如 Maven 默认将 maven-compiler-plugincompile 目标与 compile 生命周期阶段绑定,因此命令 mvn compile 实际上是先定位到 compile 这一生命周期阶段,然后再根据绑定关系调用 maven-compiler-plugincompile 目标。
  2. 直接在命令行指定要执行的插件目标(goal),例如 mvn archetype:generate 就表示调用 maven-archetype-plugingenerate 目标,这种带冒号的调用方式与生命周期无关

常用插件整理如下:

Maven 常用插件

核心插件

maven-clean-plugin

maven-resources-plugin

maven-compiler-plugin

https://maven.apache.org/plugins/maven-compiler-plugin/index.html

https://docs.oracle.com/en/java/javase/11/tools/javac.html

配置 javac 编译使用指定的 JDK

https://maven.apache.org/plugins/maven-compiler-plugin/examples/compile-using-different-jdk.html

配置 javac 编译版本

1
2
3
# -source  Specifies the version of source code accepted.
# -target Generates class files for a specific VM version.
$ javac -source 1.8 -target 1.8

https://maven.apache.org/plugins/maven-compiler-plugin/examples/set-compiler-source-and-target.html

方式一

1
2
3
4
5
6
<project>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
</project>

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

配置 javac 编译参数

https://maven.apache.org/plugins/maven-compiler-plugin/examples/pass-compiler-arguments.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>

maven-surefire-plugin

配置默认 Skip Tests

https://maven.apache.org/surefire/maven-surefire-plugin/examples/skipping-tests.html

方式一:

1
2
3
4
5
<project>
<properties>
<skipTests>true</skipTests>
</properties>
</project>

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>

maven-install-plugin

maven-deploy-plugin

打包工具

maven-jar-plugin

maven-war-plugin

其它工具

maven-archetype-plugin

用于生成骨架,详见:Maven 骨架快速搭建项目

maven-assembly-plugin

用于将项目输出及其依赖项、模块、站点文档和其它文件聚合构建成一个可执行的分发包。

项目简单配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.github.testproject.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>
jar-with-dependencies
</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>

执行 Goal assembly:single,将在 target 目录中生成一个 artifactId-version-jar-with-dependencies.jar 文件,内含所需的所有依赖,执行 java -jar 即可运行。

maven-dependency-plugin

用于分析项目依赖,例如通过 mvn dependency:tree 命令分析 Dubbo 默认依赖的第三方库:

1
2
3
4
[INFO] +- com.alibaba:dubbo:jar:2.5.9-SNAPSHOT:compile
[INFO] | +- org.springframework:spring-context:jar:4.3.10.RELEASE:compile
[INFO] | +- org.javassist:javassist:jar:3.21.0-GA:compile
[INFO] | \- org.jboss.netty:netty:jar:3.2.5.Final:compile

Spring Boot 插件

Spring Boot 提供了 spring-boot-maven-plugin 插件,可用于本地快速编译并运行、及项目打包。参考:Maven 插件

IDEA Maven 插件

最后来看下 IDEA Maven 插件提供的 Maven Projects tool window 功能:

IDEA Maven Projects

参考

http://maven.apache.org/plugins/index.html

Nginx 中配置 HTTPS/SSL 加密是非常简单的,只需要将可选 HTTP 模块中的 ngx_http_ssl_module 编译进去即可。然后有两种方式开启 SSL 模式:

  • ssl on
  • listen 443 ssl 此端口上接收的所有连接都工作在 SSL 模式。

建议使用 listen 指令的 ssl 参数替代 ssl on 指令,这样可以为同时处理 HTTP 和 HTTPS 请求的服务器提供更加紧凑的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# http 和 https(ssl) 并存配置:

http {
server {
server_name example.com;
listen 80;
listen 443 ssl;

ssl_certificate example.com.crt;
ssl_certificate_key example.com.key;

location / {
proxy_pass http://127.0.0.1:81/;
}
}
}

注意,如果两个配置同时启用,HTTP 访问可能会报错:

1
2
400 Bad Request
The plain HTTP request was sent to HTTPS port

集群

Nginx 标准 HTTP 模块 ngx_http_upstream_module 内置了集群和负载均衡功能,使用其中的 upstream 配合 proxy_pass 指令即可快速实现一个集群:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
upstream backend {
server backend1.example.com weight=5;
server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
server unix:/tmp/backend3;

server backup1.example.com:8080 backup;
server backup2.example.com:8080 down;
}

server {
location / {
proxy_pass http://backend;
}
}
}

其中 server 指令的常用参数描述如下:

参数 描述
weight=number 设置服务器的轮询权重,默认为 1。用于后端服务器性能不均的情况。
max_conns=number 设置被代理服务器的最大可用并发连接数限制,默认为 0,表示没有限制。
max_fails=number 设置最大失败重试次数,默认为 1。设置为 0 表示禁用重试。
fail_timeout=time 设置失败时间,默认 10 秒。
backup 将服务器标记为备份服务器。当主服务器不可用时,它将被传递请求。
down 将服务器标记为永久不可用。

负载均衡

负载均衡(Load Balance),其意思就是将运算或存储负载按一定的算法分摊到多个运算或存储单元上,下面介绍 Nginx 几种常见的负载均衡方法:

  • 默认策略:加权轮询策略(weighted round-robin)。
  • random,加权随机策略。
  • ip_hash,基于客户端 IP 计算出哈希值,再根据服务器数量取模选取服务器(ip_hash % server_size = server_no)。
  • hash key [consistent],基于指定 key 计算出哈希值,再根据服务器数量取模选取服务器。可选一致性哈希算法缓解重映射问题。
  • least_conn,基于最小活跃连接数(加权)。如果有多个服务器符合条件,则使用加权轮询策略依次响应。
  • least_time,基于最小平均响应时间和最小活跃连接数(加权)。如果有多个服务器符合条件,则使用加权轮询策略依次响应。

ip hash

使用 Nginx ip_hash 指令,配置如下:

1
2
3
4
5
6
7
8
upstream backend {
ip_hash;

server backend1.example.com;
server backend2.example.com;
server backend3.example.com down;
server backend4.example.com;
}

ip_hash 指令指定集群使用基于客户端 IP 地址的负载均衡方法。 客户端 IPv4 地址的前三个八位字节或整个 IPv6 地址用作哈希键。 该方法确保来自同一客户端的请求将始终传递到同一台服务器,除非此服务器不可用,客户端请求则将被转发到另一台服务器(多数情况下,始终是同一台服务器)。
如果其中一台服务器需要临时删除,则应使用 down 参数标记,以便保留当前客户端 IP 地址的哈希值。

一致性 hash

使用 Nginx hash 指令,常用的例如基于来源 IP 进行哈希,配置如下:

1
2
3
4
5
6
upstream backend {
hash $remote_addr consistent;

server backend1.example.com;
server backend2.example.com;
}

hash 指令指定集群使用基于指定 hash 散列键的负载均衡方法。散列键可以包含文本,变量及其组合。请注意,从集群中添加或删除服务器可能会导致大量键被重新映射到不同的服务器。

解决办法是使用 consistent 参数启用 ketama 一致性 hash 算法。 该算法将每个 server 虚拟成 n 个节点,均匀分布到 hash 环上。每次请求,根据配置的参数计算出一个 hash 值,在 hash 环上查找离这个 hash 最近的虚拟节点,对应的 server 作为该次请求的后端服务器。该算法确保在添加或删除服务器时,只会有少量键被重新映射到不同的服务器。这有助于为缓存服务器实现更高的缓存命中率。

会话保持

为了确保与某个客户端相关的所有请求都能够由同一台服务器进行处理,我们需要在负载均衡上启用会话保持功能,以确保负载均衡的部署不会影响到正常的业务处理,避免会话丢失。

Nginx 会话保持一般有两种方案:

  • 基于源 IP 地址的 ip_hash
  • sticky cookie 粘滞会话(也称会话保持\会话绑定)

ip_hash 实现会话保持的问题在于,当多个客户是通过正向代理(如翻墙)或 NAT 地址转换的方式来访问服务器时,由于都分配到同一台服务器上,会导致服务器之间的负载失衡。

sticky cookie 实现会话保持,在一定条件下可以保证同一个客户端访问的都是同一个后端服务器。通过 Nginx sticky 指令,配置如下:

1
2
3
4
5
6
upstream backend {
server backend1.example.com;
server backend2.example.com;

sticky cookie srv_id expires=1h domain=.example.com path=/;
}

下面这份配置在同一域名下有两个 location,分别对应了两组集群服务。为了分别实现会话保持,将 cookie 写入了对应的 path 下,避免 cookie 互相干扰,也减少了数据传输量:

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
http {
upstream backend1 {
server backup1.example.com:8080;
server backup1.example.com:8081;
sticky cookie srv_backend1 path=/backend1;
}

upstream backend2 {
server backup2.example.com:8080;
server backup2.example.com:8081;
sticky cookie srv_backend2 path=/backend2;
}

server {
server_name example.com;
listen 80;

location /backend1/ {
proxy_pass http://backend1;
}

location /backend2/ {
proxy_pass http://backend2;
}
}
}

健康检查

健康检查(Health Check)是保障集群可用性的重要手段,有三种常见的健康检查方法:

主动式健康检查

nginx_upstream_check_module 第三方模块为例,演示配置如下:

1
2
3
4
5
6
upstream backend1 {
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
check_keepalive_requests 100;
check_http_send "HEAD /m/monitor.html HTTP/1.1\r\nConnection: keep-alive\r\nHost: check.com\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}

这段配置表示:

  1. check 指令配置:每隔 interval 毫秒主动发送一个 http 健康检查包给后端服务器。请求超时时间为 timeout 毫秒。如果连续失败次数达到 fall_count,服务器就被认为是 down;如果连续成功次数达到 rise_count,服务器就被认为是 up。
  2. check_keepalive_requests 指令配置:一个连接发送的请求数。
  3. check_http_send 指令配置:请求包的内容(注意,这里必须配置 Host 请求头否则可能报错)。
  4. check_http_expect_alive 指令配置:响应状态码为 2XX3XX 表示请求成功、服务健康。

查看 Tomcat access.log 如下:

1
2
3
4
5
6
127.0.0.1 - - [06/Jun/2017:21:03:30 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:33 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:36 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:39 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:42 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -
127.0.0.1 - - [06/Jun/2017:21:03:45 +0800] "HEAD /m/monitor.html HTTP/1.1" 200 -

此时关闭某台后端服务器,一段时间后再访问,请求会被路由到其它服务器;重启后,该服务器自动加入集群。通过健康状态页面 /status 可见:

1
2
3
4
5
6
7
Nginx http upstream check status

Check upstream server number: 2, generation: 2

Index Upstream Name Status Rise counts Fall counts Check type Check port
0 backend1 127.0.0.1:8080 up 4741 0 http 0
1 backend1 127.0.0.1:8081 down 0 2340 http 0

常用变量

$upstream_addr

该模块中很常用的一个变量,用于标识集群中服务器的 IP 和端口。一般会加入到 Nginx 日志、同时脱敏后加入到响应头中,用于排查问题来源:

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

...

log_format main '"$http_x_forwarded_for" - "$upstream_addr" - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $remote_addr $request_time_msec'
access_log logs/access.log main;

map $upstream_addr $short_address {
~^\d+\.\d+\.\d+\.(.*) '';
}

server {
server_name example.com;
listen 80;

upstream backend {
server 127.0.0.1:81;
server 127.0.0.1:82;
}

location / {
add_header X-From $short_address$1;
proxy_pass http://backend/;
}
}
}

什么是代理?

一张图了解两种代理模式的区别:

两种代理模式

常见应用场景:

  • 正向代理:VPN 翻墙
  • 反向代理:Web 站点服务

Nginx 反向代理

Nginx 代理功能由标准 HTTP 模块中内置的 ngx_http_proxy_module 提供,因此无需额外编译,常见配置如下:

1
2
3
4
5
6
7
8
9
10
http {
server {
server_name example.com;
listen 80;

location / {
proxy_pass http://127.0.0.1:81/;
}
}
}

proxy_pass 指令用于配置被代理服务器的协议和地址。除了配置单机,还可以配置集群,详见 Nginx 负载均衡

常见问题

To slash or not to slash

Here is a handy table that shows you how the request will be received by your WebApp, depending on how you write the location and proxy_pass declarations. Assume all requests go to http://localhost:8080:

location proxy_pass Request Received by upstream
/webapp/ http://localhost:8080/api/ /webapp/foo?bar=baz /api/foo?bar=baz ✅
/webapp/ http://localhost:8080/api /webapp/foo?bar=baz /apifoo?bar=baz
/webapp http://localhost:8080/api/ /webapp/foo?bar=baz /api//foo?bar=baz
/webapp http://localhost:8080/api /webapp/foo?bar=baz /api/foo?bar=baz
/webapp http://localhost:8080/api /webappfoo?bar=baz /apifoo?bar=baz

In other words: You usually always want a trailing slash, never want to mix with and without trailing slash, and only want without trailing slash when you want to concatenate a certain path component together (which I guess is quite rarely the case).

上游无法获取真实的访问来源信息

使用反向代理之后,上游服务器(如 Tomcat)无法获取真实的访问来源信息(如协议、域名、访问 IP),例如下面代码:

1
2
3
4
5
6
request.getScheme() // 总是 http,而不是实际的 http 或 https
request.isSecure() // 总是 false
request.getRemoteAddr() // Nginx IP
request.getServerName() // 127.0.0.1
request.getRequestURL() // http://127.0.0.1:81/index
response.sendRedirect(...) // 总是重定向到 http

这个问题需要在 Nginx 和 Tomcat 中做一些配置以解决问题。

Nginx 配置:

使用 proxy_set_header 指令为上游服务器添加请求头:

1
2
3
4
5
6
7
8
9
10
http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

server {
...
}
}

Tomcat 配置:

在 Tomcat 的 server.xml 中配置 RemoteIpValve 让代码能够获取真实 IP 和协议:

1
2
3
<Valve className="org.apache.catalina.valves.RemoteIpValve" 
remoteIpHeader="X-Forwarded-For"
protocolHeader="X-Forwarded-Proto" />

解决结果:

1
2
3
4
5
6
7
request.getScheme() // 实际的 http 或 https
request.isSecure() // 对应的 false 或 true
request.getRemoteAddr() // 用户 IP
request.getHeader("X-Real-IP") // 用户 IP
request.getServerName() // example.com
request.getRequestURL() // 对应的 http://example.com/index 或 https://example.com/index
response.sendRedirect(...) // 实际的 http 或 https

全局 proxy_set_header 失效

先来看下 proxy_set_header 的语法:

1
2
3
4
5
6
7
8
9
语法:	proxy_set_header field value;
默认值: proxy_set_header Host $proxy_host;
proxy_set_header Connection close;
上下文: http, server, location

允许重新定义或者添加发往后端服务器的请求头。value 可以包含文本、变量或者它们的组合。当且仅当当前配置级别中没有定义 proxy_set_header 指令时,会从上面的级别继承配置。 默认情况下,只有两个请求头会被重新定义:

proxy_set_header Host $proxy_host;
proxy_set_header Connection close;

这里隐含一个坑:如果当前配置级别中定义了 proxy_set_header 指令,哪怕只配置了一个,都会导致无法从上面的级别继承配置,即导致全局级别的 proxy_set_header 配置失效。例如下述 HTTP 长连接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
http {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

upstream backend {
server 127.0.0.1:8080;
keepalive 16;
}

server {
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
...
}
}
}

导致全局级别的 proxy_set_header 配置失效:

proxy_set_header 失效

解决办法是在 location 中重新配置这四个请求头。

参考

How nginx processes a request ?

万字多图,搞懂 Nginx 高性能网络工作原理!

本文用于理顺 Maven 构建的生命周期(Build Lifecycle)概念,掌握执行 Maven 命令时其背后的原理。

构建

生命周期(Lifecycle)

生命周期(Lifecycle)是一个抽象的概念,意味着它并不做任何实质性的事情,也就是说它像接口,只定义规范,具体细节不管。具体的实现细节则交给了 Maven 各个插件(Plugin)。

生命周期(Lifecycle)是一系列有序的阶段(Phase),而插件的目标(Goal)是跟某个生命周期的阶段绑定在一起的,如果一个阶段没有绑定任何目标,则运行该阶段没有任何实质意义。

摘录一段官方的描述:

A Build lifecycle is Made Up of build Phases.

A build phase represents a stage in the lifecycle.

A Build Phase is Made Up of Goals.

Maven 内置三种生命周期:

内置 Lifecycle 描述 内置 Phase 数量
clean Project cleaning 3 个
default Project deployment 23 个
site Creation of project’s site documentation 4 个

阶段(Phase)

Phase 虽然很多,但其中带连字符 (pre-*, post-*, or process-*) 的 Phase 通常不会在命令行中直接使用。

那么有哪些常用的 Phase?

Phase 描述
clean remove all files generated by the previous build.
complie compile the source code of the project
test test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed.
package take the compiled code and package it in its distributable format, such as a JAR.
install install the package into the local repository, for use as a dependency in other projects locally
deploy done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.

如何运行一个 Phase?使用命令 mvn phase。运行时,首先由该 phase 确定对应的生命周期。然后从生命周期的第一个 Phase 开始,按顺序运行到该 phase。

如果要跳过测试,参数如下:mvn package -DskipTests

目标(Goal)

Phase 是一个逻辑概念,本身并不包含任何构建信息,运行 Phase 时,只是运行绑定到该 Phase 的 Goal。

Plugin 是一个物理概念,安装了 Plugin 才会有相应的一些 Goal。而每个 Goal 代表了该 Plugin 的一种能力。如果要直接运行一个 Goal,可以使用 mvn <plugin-prefix>:<goal>。其中 plugin-prefix 的约定格式如下:

约定格式 描述
maven-${prefix}-plugin Apache Maven 团队维护的官方插件。例如 maven-compiler-plugin,命令:mvn dependency:list
${prefix}-maven-plugin 其它插件。例如 spring-boot-maven-plugin,命令:mvn spring-boot:run

参考

http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

BOM

What Is Maven BOM❓

BOM stands for Bill Of Materials. A BOM is a special kind of POM that is used to control the versions of a project’s dependencies and provide a central place to define and update those versions.

BOM provides the flexibility to add a dependency to our module without worrying about the version that we should depend on.

在大型项目中,BOM 用于将一组相关的、可以良好协作的构建(Maven Artifact)组合在一起,提供版本管理,避免构件间潜在的版本不兼容风险。

BOM 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>project-n-bom</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<!-- 属性配置 -->
<properties...>
<!-- 依赖管理配置,声明该 BOM 管理的依赖 -->
<dependencyManagement...>
</project>

BOM 引用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 每个公司可能都拥有自己的标准父项目,为了解决 Maven 单继承问题,可以使用 `<dependencyManagement>` 通过组合方式来享受其提供的依赖版本统一管理的好处 -->
<dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-bom -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>${openfeign.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

常用的 BOM 如下:https://mvnrepository.com/tags/bom

Project Maven URL
JUnit 5 https://mvnrepository.com/artifact/org.junit/junit-bom
Log4j 2 https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-bom
Jackson https://mvnrepository.com/artifact/com.fasterxml.jackson/jackson-bom
OpenFeign https://mvnrepository.com/artifact/io.github.openfeign/feign-bom
Reactor https://mvnrepository.com/artifact/io.projectreactor/reactor-bom
Netty https://mvnrepository.com/artifact/io.netty/netty-bom
Jetty https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-bom
Jersey https://mvnrepository.com/artifact/org.glassfish.jersey/jersey-bom
Micrometer https://mvnrepository.com/artifact/io.micrometer/micrometer-bom
Spring Framework https://mvnrepository.com/artifact/org.springframework/spring-framework-bom
Spring Session https://mvnrepository.com/artifact/org.springframework.session/spring-session-bom
Spring Security https://mvnrepository.com/artifact/org.springframework.security/spring-security-bom
Spring Integration https://mvnrepository.com/artifact/org.springframework.integration/spring-integration-bom
Spring Boot https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies
Spring Cloud https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies

实践例子

<dependencyManagement> 用于在具有层级关系的项目间统一管理依赖的版本,一般用在父项目中,通过它来管理 jar 包的版本,让子项目中引用一个依赖而不用显式的指定版本号,以达到依赖版本统一管理的目的。

这种方法的好处是显而易见的。依赖的细节(如版本号)可以在一个中心位置集中设置,并传播到所有继承的 POM。

但由于现实中有可能是 N 个项目都继承同一个父项目,如果把它们的依赖全部放到父项目的 <DependencyManagement> 中管理,势必会导致父项目的依赖配置急剧膨胀,形成一个巨型 POM。更为关键的是,后续各项目组都有升级各自提供的依赖版本的需要,如果大家都去改这个公共的父项目,版本管理会变得很混乱。

解决办法是按项目组粒度对依赖进行分组管理,分而治之。将每组依赖抽取成像 spring-boot-dependencies 一样的 BOM,并在父项目中 <dependencyManagement> 使用 scope=import 方式将这些 BOM 组合起来,这样父项目的 POM 就会十分干净且稳定,各组依赖的版本管理也能转由各自项目组的 BOM 专门负责,类似一个金字塔模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                   +------------+
| parent-pom |
+------+-----+
|
+-----------------|-----------------+
| | |
+-------v-------+ +-------+-------+ +-------v-------+
| Project-A-BOM | | Project-B-BOM | | Project-N-BOM |
+---------------+ +-------+-------+ +---------------+
|
+--------------|---------------+
| | |
+-----v------+ +-----v------+ +------v-----+
| Artifact-A | | Artifact-B | | Artifact-N |
+------------+ +------------+ +------------+

例如某个公司的标准父项目 POM 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<project>
<modelVersion>4.0.0</modelVersion>

<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
<packaging>pom</packaging> <!-- 必须是 pom -->

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot 1 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!-- 此处省略 N 组依赖 -->
......
</dependencies>
</dependencyManagement>
</project>

开发阶段为了测试 Spring Boot 2 的兼容性,该公司 child 项目新开 git 分支,修改 POM 如下:

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
<project>
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>test</groupId>
<artifactId>parent</artifactId>
<version>1.0</version>
</parent>
<artifactId>child</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot 2 -->
<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>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

上例 child 项目覆盖了父项目 <dependencyManagement> 的 Spring Boot 版本,当兼容性测试通过后,子项目 <dependencyManagement> 即可去掉,转而升级父项目 Spring Boot 的版本号即可,这样所有子项目都会统一升级版本。

参考

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

https://www.baeldung.com/spring-maven-bom

https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-build-systems.html

https://projectreactor.io/docs/core/release/reference/#getting-started-understanding-bom