简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

IOC回顾

发表于 2019-01-20 | 分类于 spring

IOC

控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)

spring实现IOC的思路和方法

spring实现IOC的思路是提供一些配置信息用来描述类之间的依赖关系,然后由容器去解析这些配置信息,继而维护好对象之间的依赖关系。

spring实现IOC的思路大致可以拆分成:

1
2
3
应用程序中提供类和类的依赖关系(属性或者构造方法)
把需要容器管理的对象通过配置信息告诉容器(xml、annotation,javaconfig)
把各个类之间的依赖关系通过配置信息告诉容器

配置这些信息的方法有三种分别是xml,annotation和javaconfig,容器维护这些类和对象的过程称为自动注入,自动注入的方法有两种:构造方法和setter注入。

1
2
3
schemal-based-------xml
annotation-based-----annotation
java-based----java Configuration

spring注入详细配置(字符串、数组等)参考文档:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-properties-detailed

Constructor-based Dependency Injection

构造方法注入参考文档:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-constructor-injection

1
2
3
4
5
6
7
8
package x.y;

public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

Setter-based Dependency Injection

参考文档:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-setter-injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ExampleBean {

private AnotherBean beanOne;

private YetAnotherBean beanTwo;

private int i;

public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}

public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}

public void setIntegerProperty(int i) {
this.i = i;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<bean id="exampleBean" class="examples.ExampleBean">
<!-- setter injection using the nested ref element -->
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>

<!-- setter injection using the neater ref attribute -->
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

自动装配

IOC的注入需要提供的依赖关系,一是类的定义体现依赖关系,二是在配置文件中描述依赖关系。自动装配则把配置文件中描述依赖关系取消了,只要提供好类的依赖关系就可以把对象交给容器管理完成注入。

在实际开发中,描述类之间的依赖关系通常是大篇幅的,如果使用自动装配则省去了很多配置,并且如果对象的依赖发生更新我们可以不需要去更新配置,但是也带来了一定的缺点。

自动装配的优点参考文档:
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-autowire

缺点参考文档:
https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-autowired-exceptions

自动装配的方法有四种:no/byName/byType/constructor

自动装配的方式参考文档:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-autowire

20200420222616

从自动装配中排除Bean

在每个bean的基础上,您可以从自动装配中排除一个bean。使用Spring的XML格式,将元素的autowire-candidate属性设置为false。容器使特定的bean定义对自动装配基础结构不可用(包括注释样式配置,例如@Autowired)。

懒加载

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-lazy-init

1
2
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>

或注解形式@Lazy

Spring Bean的作用域

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes

20200420222645

xml方式

1
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

或者直接使用注解方式。

Spring的方法注入可分为两种

查找方法注入:用于注入方法返回结果,也就是说能通过配置方式替换方法返回结果。即我们通常所说的lookup-method注入。

1
2
单例模式的bean只会被创建一次,IoC容器会缓存该bean实例以供下次使用;原型模式的bean每次都会创建一个全新的bean,IoC容器不会缓存该bean的实例。
那么如果现在有一个单例模式的bean引用了一个原型模式的bean呢?如果无特殊处理,则被引用的原型模式的bean也会被缓存,这就违背了原型模式的初衷,这时使用lookup-method注入可以解决该问题。

替换方法注入:可以实现方法主体或返回结果的替换,即我们通常所说的replaced-method注入。

lookup-method注入 @Lookup

** 在Singleton 当中引用了一个Prototype的bean的时候引发的问题: **

1
2
3
4
在Spring的诸多应用场景中bean都是单例形式,当一个单利bean需要和一个非单利bean组合使用或者一个非单利bean和另一个非单利bean组合使用时,我们通常都是将依赖以属性的方式放到bean中来引用,然后以@Autowired来标记需要注入的属性。
但是这种方式在bean的生命周期不同时将会出现很明显的问题,假设单利bean A需要一个非单利bean B(原型),我们在A中注入bean B,每次调用bean A中的方法时都会用到bean B,
我们知道Spring Ioc容器只在容器初始化时执行一次,也就是bean A中的依赖bean B只有一次注入的机会,但是实际上bean B我们需要的是每次调用方法时都获取一个新的对象(原型)
所以问题明显就是:我们需要bean B是一个原型bean,而事实上bean B的依赖只注入了一次变成了事实上的单利bean。

解决方式:去找 或者赋值

1、Method Injection

继承ApplicationContextAware 每次调用方法时用上下文的getBean(name,class)方法去重新获取bean B的实例。

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
package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

private ApplicationContext applicationContext;

public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}

public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

2、Lookup Method Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class CommandManager {

public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}

通过一个注解或xml配置实现

1
2
3
4
5
6
7
8
9
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class CommandManager {

public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}

// 也可以写成 @Lookup("prototypeBean") 来指定需要注入的bean
@Lookup
protected abstract MyCommand createCommand();
}

//或者不用抽象类
@Lookup
public MyCommand createCommand() {
return null;
}

replaced-method注入

主要作用就是替换方法体及其返回值,其实现也比较简单

replace-method注入需实现MethodReplacer接口,并重写reimplement方法。

Lifecycle Callbacks 生命周期回调

初始化回调

实现接口org.springframework.beans.factory.InitializingBean 指定一个方法: void afterPropertiesSet() throws Exception;

或者是配置文件中直接一个初始化方法:

1
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>

销毁回调

org.springframework.beans.factory.DisposableBean当包含该接口的容器被销毁时,实现该接口可使Bean获得回调 public void destroy() 。

1
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>

注解方式实现:从Spring 2.5开始,可以使用三种方式来控制Bean生命周期行为:

1、在InitializingBean(初始化bean之后)和 DisposableBean回调接口

2、配置init()和destroy()方法

3、在@PostConstruct(初始化bean之前)和@PreDestroy 注释。您可以结合使用这些机制来控制给定的bean。

如果同一个bean配置的具有不同初始化方法的顺序为:

1
2
3
4
5
6
7
8
用注释的方法 @PostConstruct
afterPropertiesSet()由InitializingBean回调接口定义
定制配置的init()方法

销毁方法的调用顺序相同:
用注释的方法 @PreDestroy
destroy()由DisposableBean回调接口定义
定制配置的destroy()方法

相关注解

1
2
3
4
5
@Primary:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常 
@Qualifier:限定符 @Autowired 默认按类型装配,如果我们想使用按名称装配,可以结合@Qualifier注解一起使用
例子:
@Autowired
@Qualifier(注入的类)

depends-on

如果 bean 是另一个的依赖关系,则需要首先注入这个类。

1
2
3
4
5
6
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

生成候选组件的索引

当有很多个类需要扫描,会比较慢,使用如下包来在编译的时候就加入到索引,按照索引的方式去扫描就会很快。

1
2
3
4
5
6
7
8
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.2.3.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>

java也有一套注解,可以用来开发spring。

20200420222714

AOP回顾

发表于 2019-01-20 | 分类于 spring

Aop

与OOP对比,传统的OOP开发中的代码逻辑是自上而下的,而这些过程会产生一些横切性问题,但是这些问题和业务关系不大,也不会影响主逻辑,但是会散落到代码的各个部分,难以维护。而AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。

应用场景:日志记录、权限验证、效率检查、事务管理、exception。

JDK动态代理 CGLIB代理
编译时期的织入还是运行时期的织入? 运行时期织入 运行时期织入
初始化时期织入还是获取对象时期织入? 初始化时期织入 初始化时期织入

SpringAop和AspectJ的关系

Aop是一种概念,springAop、AspectJ都是Aop的实现,SpringAop有自己的语法,但是语法复杂,所以SpringAop借助了AspectJ的注解,但是底层实现还是自己的。

spring AOP提供两种编程风格

1
2
@AspectJ support        利用aspectj的注解
Schema-based AOP support xml aop:config 命名空间

Spring Aop的概念

1
2
3
4
5
6
7
aspect:一定要给spring去管理  
pointcut:切点,表示连接点的集合
Joinpoint:连接点 目标对象中的方法
Weaving:织入,把代理逻辑加入到目标对象上的过程
target:目标对象 原始对象
aop Proxy 代理对象 包含了原始对象的代码和增加后的代码的那个对象,如果是cglib则都一致。
advice:通知

advice通知类型

1
2
3
4
5
6
7
Before 连接点执行之前,但是无法阻止连接点的正常执行,除非该段执行抛出异常
After 连接点正常执行之后,执行过程中正常执行返回退出,非异常退出
After throwing 执行抛出异常的时候
After (finally) 无论连接点是正常退出还是异常退出,都会执行
Around advice: 围绕连接点执行,例如方法调用。这是最有用的切面方式。around通知可以在方法调用之前和之后执行自定义行为。它还负责选择是继续加入点还是通过返回自己的返回值或抛出异常来快速建议的方法执行。

@DeclareParents:其是一种Introduction类型的模型,在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现。

Proceedingjoinpoint 和JoinPoint的区别

Proceedingjoinpoint 继承了JoinPoint,并扩充实现了proceed()方法,用于继续执行连接点。JoinPoint仅能获取相关参数,无法执行连接点。

JoinPoint的方法

1
2
3
4
5
1.java.lang.Object[] getArgs():获取连接点方法运行时的入参列表; 
2.Signature getSignature() :获取连接点的方法签名对象;
3.java.lang.Object getTarget() :获取连接点所在的目标对象;
4.java.lang.Object getThis() :获取代理对象本身;
proceed()有重载,有个带参数的方法,可以修改目标方法的的参数

Introductions

通过将需要添加的新的行为逻辑,以新的接口定义增加到目标对象上。以@Aspect声明一个实列变量,它的类型对应的是新增加的接口类型,然后通过DeclareParents对其进行标注。通过@DeclareParents指定新接口定义的实现类以及将要加诸其上的目标对象。简单来说就是可以扩展原有的目标对象。

例如,给定名为的接口UsageTracked和名为的接口的实现DefaultUsageTracked,新声明服务接口的所有实现者也都实现该UsageTracked接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Aspect
public class UsageTracking {

@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;

@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}

}

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

切面模型 perthis

perthis:每个切入点表达式匹配的连接点对应的AOP对象都会创建一个新的切面实例,使用@Aspect(“perthis(切入点表达式)”)指定切入点表达式;默认的情况下多个连接点对应的切面对象是一个。

使用方式如下:

@Aspect(“perthis(com.xyz.myapp.SystemArchitecture.businessService())”)

1
2
1. AspectJ对象的注入类型为prototype
2. 目标对象也必须是prototype的

原因:只有目标对象是原型模式的,每次getBean得到的对象才是不一样的,由此针对每个对象就会产生新的切面对象,才能产生不同的切面结果。

SpringAop支持AspectJ

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

启用@AspectJ支持

1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {

}

XML配置启用@AspectJ支持:aop:aspectj-autoproxy/

声明一个Aspect

1
2
3
4
@Component
@Aspect
public class UserAspect {
}

声明切点pointCut

1
2
3
4
5
6
7
8
9
10
11
12
@Pointcut("execution(* transfer(..))")// 切入点表达式
private void anyOldTransfer() {}// 切入点签名

/**
* 申明切入点,匹配UserDao所有方法调用
* execution匹配方法执行连接点
* within:将匹配限制为特定类型中的连接点
* args:参数
* target:目标对象
* this:代理对象
*/
@Pointcut("execution(* com.yao.dao.UserDao.*(..))")

Advice通知

1
2
3
4
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doAccessCheck() {
// ...
}

AspectJ切入点指示符(PCD):

execution

用于匹配方法执行的连接点。这是使用Spring AOP时要使用的主要切入点指示符。最小粒度方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

这里问号表示当前项可以有也可以没有,其中各项的语义如下
modifiers-pattern:方法的可见性,如public,protected;
ret-type-pattern:方法的返回值类型,如int,void等;
declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
name-pattern:方法名类型,如buisinessService();
param-pattern:方法的参数类型,如java.lang.String;
throws-pattern:方法抛出的异常类型,如java.lang.Exception;

example:
@Pointcut("execution(* com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的任意方法
@Pointcut("execution(public * com.chenss.dao.*.*(..))")//匹配com.chenss.dao包下的任意接口和类的public方法
@Pointcut("execution(public * com.chenss.dao.*.*())")//匹配com.chenss.dao包下的任意接口和类的public 无方法参数的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String, ..))")//匹配com.chenss.dao包下的任意接口和类的第一个参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(* com.chenss.dao.*.*(java.lang.String))")//匹配com.chenss.dao包下的任意接口和类的只有一个参数,且参数为String类型的方法
@Pointcut("execution(public * *(..))")//匹配任意的public方法
@Pointcut("execution(* te*(..))")//匹配任意的以te开头的方法
@Pointcut("execution(* com.chenss.dao.IndexDao.*(..))")//匹配com.chenss.dao.IndexDao接口中任意的方法
@Pointcut("execution(* com.chenss.dao..*.*(..))")//匹配com.chenss.dao包及其子包中任意的方法
within

将匹配限制为某些类型内的连接点(使用Spring AOP时,在匹配类型内声明的方法的执行)。

1
2
3
within与execution相比,粒度更大,仅能实现到包和接口、类级别。而execution可以精确到方法的返回值,参数个数、修饰符、参数类型等
@Pointcut("within(com.chenss.dao.*)")//匹配com.chenss.dao包中的任意方法
@Pointcut("within(com.chenss.dao..*)")//匹配com.chenss.dao包及其子包中的任意方法
this

限制匹配到连接点(使用Spring AOP时方法的执行)的匹配,其中bean引用(Spring AOP代理)是给定类型的实例。

JDK代理时,指向接口和代理类proxy,cglib代理时 指向接口和子类(不使用proxy)

target

在目标对象(代理的应用程序对象)是给定类型的实例的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。

1
2
3
4
5
6
7
8
9
10
11
如果配置设置proxyTargetClass=false,或默认为false,则是用JDK代理,否则使用的是CGLIB代理
JDK代理的实现方式是基于接口实现,代理类继承Proxy,实现接口。

而CGLIB继承被代理的类来实现。
所以使用target会保证目标不变,关联对象不会受到这个设置的影响。
但是使用this对象时,会根据该选项的设置,判断是否能找到对象。

@Pointcut("target(com.chenss.dao.IndexDaoImpl)")//目标对象,也就是被代理的对象。限制目标对象为com.chenss.dao.IndexDaoImpl类
@Pointcut("this(com.chenss.dao.IndexDaoImpl)")//当前对象,也就是代理对象,代理对象时通过代理目标对象的方式获取新的对象,与原值并非一个
@Pointcut("@target(com.chenss.anno.Chenss)")//具有@Chenss的目标对象中的任意方法
@Pointcut("@within(com.chenss.anno.Chenss)")//等同于@targ
args

在参数为给定类型的实例的情况下,将匹配指定参数类型和指定参数数量的方法,与包名和类名无关。

1
2
3
4
5
6
7
/**
* args同execution不同的地方在于:
* args匹配的是运行时传递给方法的参数类型
* execution(* *(java.io.Serializable))匹配的是方法在声明时指定的方法参数类型。
*/
@Pointcut("args(java.io.Serializable)")//匹配运行时传递的参数类型为指定类型的、且参数个数和顺序匹配
@Pointcut("@args(com.chenss.anno.Chenss)")//接受一个参数,并且传递的参数的运行时类型具有@Classified
@target

在执行对象的类具有给定类型的注释的情况下,将匹配限制为连接点(使用Spring AOP时方法的执行)。

@args

限制匹配的连接点(使用Spring AOP时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。

@within

将匹配限制为具有给定注释的类型内的连接点(使用Spring AOP时,使用给定注释的类型中声明的方法的执行)。

@annotation

将匹配限制在连接点的主题(Spring AOP中正在执行的方法)具有给定注释的连接点上。

1
2
3
4
5
作用方法级别 上述所有表达式都有@ 比如@Target(里面是一个注解类xx,表示所有加了xx注解的类,和包名无关)

注意:上述所有的表达式可以混合使用,|| && !

@Pointcut(" @annotation(org.springframework.transaction.annotation.Transactional)")//匹配带有org.springframework.transaction.annotation.Transactional注解的方法

Redisson详解

发表于 2019-01-11 | 分类于 分布式

一、介绍

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。

Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。

二、配置

1、程序配置方式

通过类代码来构建配置:通过构建Config对象实例来实现的。

1
2
3
4
5
Config config = new Config();
config.setTransportMode(TransportMode.EPOLL);
config.useClusterServers()
//可以用"rediss://"来启用SSL连接
.addNodeAddress("redis://127.0.0.1:7181");

2、配置文件方式

Redisson的配置文件可以是JSON格式或YAML格式。 可以通过调用Config.fromJSON方法并指定一个File实例来实现读取JSON格式的配置:

1
2
Config config = Config.fromJSON(new File("config-file.json"));
RedissonClient redisson = Redisson.create(config);

或

1
2
Config config = Config.fromYAML(new File("config-file.yaml"));
RedissonClient redisson = Redisson.create(config);

通过Spring XML命名空间配置

1
2
3
4
5
6
7
8
9
10
11
<redisson:client>
<redisson:single-server ... />
<!-- 或者 -->
<redisson:master-slave-servers ... />
<!-- 或者 -->
<redisson:sentinel-servers ... />
<!-- 或者 -->
<redisson:cluster-servers ... />
<!-- 或者 -->
<redisson:replicated-servers ... />
</redisson:client>

3、常用设置

以下是关于org.redisson.Config类的配置参数,它适用于所有Redis组态模式(单机,集群和哨兵)

codec(编码)

默认值: org.redisson.codec.JsonJacksonCodec

可选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
编码类名称	说明
org.redisson.codec.JsonJacksonCodec Jackson JSON 编码 默认编码
org.redisson.codec.AvroJacksonCodec Avro 一个二进制的JSON编码
org.redisson.codec.SmileJacksonCodec Smile 另一个二进制的JSON编码
org.redisson.codec.CborJacksonCodec CBOR 又一个二进制的JSON编码
org.redisson.codec.MsgPackJacksonCodec MsgPack 再来一个二进制的JSON编码
org.redisson.codec.IonJacksonCodec Amazon Ion 亚马逊的Ion编码,格式与JSON类似
org.redisson.codec.KryoCodec Kryo 二进制对象序列化编码
org.redisson.codec.SerializationCodec JDK序列化编码
org.redisson.codec.FstCodec FST 10倍于JDK序列化性能而且100%兼容的编码
org.redisson.codec.LZ4Codec LZ4 压缩型序列化对象编码
org.redisson.codec.SnappyCodec Snappy 另一个压缩型序列化对象编码
org.redisson.client.codec.JsonJacksonMapCodec 基于Jackson的映射类使用的编码。可用于避免序列化类的信息,以及用于解决使用byte[]遇到的问题。
org.redisson.client.codec.StringCodec 纯字符串编码(无转换)
org.redisson.client.codec.LongCodec 纯整长型数字编码(无转换)
org.redisson.client.codec.ByteArrayCodec 字节数组编码
org.redisson.codec.CompositeCodec 用来组合多种不同编码在一起

threads(线程池数量)

默认值: 当前处理核数量 * 2

这个线程池数量被所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务共同共享。

nettyThreads (Netty线程池数量)

默认值: 当前处理核数量 * 2

这个线程池数量是在一个Redisson实例内,被其创建的所有分布式数据类型和服务,以及底层客户端所一同共享的线程池里保存的线程数量。

executor(线程池)

单独提供一个用来执行所有RTopic对象监听器,RRemoteService调用者和RExecutorService任务的线程池(ExecutorService)实例。

eventLoopGroup

用于特别指定一个EventLoopGroup. EventLoopGroup是用来处理所有通过Netty与Redis服务之间的连接发送和接受的消息。每一个Redisson都会在默认情况下自己创建管理一个EventLoopGroup实例。因此,如果在同一个JVM里面可能存在多个Redisson实例的情况下,采取这个配置实现多个Redisson实例共享一个EventLoopGroup的目的。

只有io.netty.channel.epoll.EpollEventLoopGroup或io.netty.channel.nio.NioEventLoopGroup才是允许的类型。

transportMode(传输模式)

默认值:TransportMode.NIO

可选参数:
TransportMode.NIO,
TransportMode.EPOLL - 需要依赖里有netty-transport-native-epoll包(Linux) TransportMode.KQUEUE - 需要依赖里有 netty-transport-native-kqueue包(macOS)

lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒)

默认值:30000

监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout参数的情况。如果该看门口未使用lockWatchdogTimeout去重新调整一个分布式锁的lockWatchdogTimeout超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。

keepPubSubOrder(保持订阅发布顺序)

默认值:true

通过该参数来修改是否按订阅发布消息的接收顺序出来消息,如果选否将对消息实行并行处理,该参数只适用于订阅发布消息的情况。

performanceMode(高性能模式)

默认值:HIGHER_THROUGHPUT

用来指定高性能引擎的行为。由于该变量值的选用与使用场景息息相关(NORMAL除外)我们建议对每个参数值都进行尝试。

该参数仅限于Redisson PRO版本。

可选模式:
HIGHER_THROUGHPUT - 将高性能引擎切换到 高通量 模式。 LOWER_LATENCY_AUTO - 将高性能引擎切换到 低延时 模式并自动探测最佳设定。 LOWER_LATENCY_MODE_1 - 将高性能引擎切换到 低延时 模式并调整到预设模式1。 LOWER_LATENCY_MODE_2 - 将高性能引擎切换到 低延时 模式并调整到预设模式2。 NORMAL - 将高性能引擎切换到 普通 模式

集群模式

1、程序方式

1
2
3
4
5
6
7
8
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
//可以用"rediss://"来启用SSL连接
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");

RedissonClient redisson = Redisson.create(config);

2、通过文件配置集群模式

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
{
"clusterServersConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"reconnectionTimeout":3000,
"failedAttempts":3,
"password":null,
"subscriptionsPerConnection":5,
"clientName":null,
"loadBalancer":{
"class":"org.redisson.connection.balancer.RoundRobinLoadBalancer"
},
"slaveSubscriptionConnectionMinimumIdleSize":1,
"slaveSubscriptionConnectionPoolSize":50,
"slaveConnectionMinimumIdleSize":32,
"slaveConnectionPoolSize":64,
"masterConnectionMinimumIdleSize":32,
"masterConnectionPoolSize":64,
"readMode":"SLAVE",
"nodeAddresses":[
"redis://127.0.0.1:7004",
"redis://127.0.0.1:7001",
"redis://127.0.0.1:7000"
],
"scanInterval":1000
},
"threads":0,
"nettyThreads": 0,
"codec":{
"class":"org.redisson.codec.JsonJacksonCodec"
},
"transportMode":"NIO"
}

或

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
clusterServersConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 32
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 32
masterConnectionPoolSize: 64
readMode: "SLAVE"
nodeAddresses:
- "redis://127.0.0.1:7004"
- "redis://127.0.0.1:7001"
- "redis://127.0.0.1:7000"
scanInterval: 1000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode":"NIO"
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
<redisson:client
id="redisson"
name="redisson1,redisson2"
threads="0"
netty-threads="0"
codec-ref="myCodec"
transport-mode="NIO"
redisson-reference-enabled="true"
codec-provider-ref="myCodecProvider"
resolver-provider-ref="myResolverProvider"
executor-ref="myExecutor"
event-loop-group-ref="myEventLoopGroup"
>
<!--
这里的name属性和qualifier子元素不能同时使用。

id和name的属性都可以被用来作为qualifier的备选值。
-->
<!--<qualifier value="redisson3"/>-->
<redisson:cluster-servers
idle-connection-timeout="10000"
ping-timeout="1000"
connect-timeout="10000"
timeout="3000"
retry-attempts="3"
retry-interval="1500"
reconnection-timeout="3000"
failed-attempts="3"
password="do_not_use_if_it_is_not_set"
subscriptions-per-connection="5"
client-name="none"
load-balancer-ref="myLoadBalancer"
subscription-connection-minimum-idle-size="1"
subscription-connection-pool-size="50"
slave-connection-minimum-idle-size="10"
slave-connection-pool-size="64"
master-connection-minimum-idle-size="10"
master-connection-pool-size="64"
read-mode="SLAVE"
subscription-mode="SLAVE"
scan-interval="1000"
>
<redisson:node-address value="redis://127.0.0.1:6379" />
<redisson:node-address value="redis://127.0.0.1:6380" />
<redisson:node-address value="redis://127.0.0.1:6381" />
</redisson:cluster-servers>
</redisson:client>

<!-- 最基本配置 -->
<redisson:client>
<redisson:cluster-servers>
<redisson:node-address value="redis://127.0.0.1:6379" />
<redisson:node-address value="redis://127.0.0.1:6380" />
<redisson:node-address value="redis://127.0.0.1:6381" />
...
</redisson:cluster-servers>
</redisson:client>

3、设置

Redis集群组态的最低要求是必须有三个主节点。Redisson的集群模式的使用方法如下:ClusterServersConfig clusterConfig = config.useClusterServers();

ClusterServersConfig 类的设置参数如下:

nodeAddresses(添加节点地址)

可以通过host:port的格式来添加Redis集群节点的地址。多个节点可以一次性批量添加。

scanInterval(集群扫描间隔时间)

默认值: 1000

对Redis集群节点状态扫描的时间间隔。单位是毫秒。

slots(分片数量)

默认值: 231 用于指定数据分片过程中的分片数量。支持数据分片/框架结构有:集(Set)、映射(Map)、BitSet、Bloom filter, Spring Cache和Hibernate Cache等.

readMode(读取操作的负载均衡模式)

默认值: SLAVE(只在从服务节点里读取)

注:在从服务节点里读取的数据说明已经至少有两个节点保存了该数据,确保了数据的高可用性。

设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。 MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取。

subscriptionMode(订阅操作的负载均衡模式)

默认值:SLAVE(只在从服务节点里订阅)

设置订阅操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里订阅。 MASTER - 只在主服务节点里订阅。

loadBalancer(负载均衡算法类的选择)

默认值: org.redisson.connection.balancer.RoundRobinLoadBalancer

在多Redis服务节点的环境里,可以选用以下几种负载均衡方式选择一个节点: org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法 org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法 org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法

subscriptionConnectionMinimumIdleSize(从节点发布和订阅连接的最小空闲连接数)

默认值:1

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的最小保持连接数(长连接)。Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。

subscriptionConnectionPoolSize(从节点发布和订阅连接池大小)

默认值:50

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

slaveConnectionMinimumIdleSize(从节点最小空闲连接数)

默认值:32

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时读取反映速度。

slaveConnectionPoolSize(从节点连接池大小)

默认值:64

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

masterConnectionMinimumIdleSize(主节点最小空闲连接数)

默认值:32

多节点的环境里,每个 主节点的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时写入反应速度。

masterConnectionPoolSize(主节点连接池大小)

默认值:64

多主节点的环境里,每个 主节点的连接池最大容量。连接池的连接数量自动弹性伸缩。

idleConnectionTimeout(连接空闲超时,单位:毫秒)

默认值:10000

如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。

connectTimeout(连接超时,单位:毫秒)

默认值:10000

同任何节点建立连接时的等待超时。时间单位是毫秒。

timeout(命令等待超时,单位:毫秒)

默认值:3000

等待节点回复命令的时间。该时间从命令发送成功时开始计时。

retryAttempts(命令失败重试次数)

默认值:3

如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。

retryInterval(命令重试发送时间间隔,单位:毫秒)

默认值:1500

在一条命令发送失败以后,等待重试发送的时间间隔。时间单位是毫秒。

reconnectionTimeout(重新连接时间间隔,单位:毫秒)

默认值:3000

当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。

failedAttempts(执行失败最大次数)

默认值:3

在某个节点执行相同或不同命令时,连续 失败 failedAttempts(执行失败最大次数) 时,该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。

password(密码)

默认值:null

用于节点身份验证的密码。

subscriptionsPerConnection(单个连接最大订阅数量)

默认值:5

每个连接的最大订阅数量。

clientName(客户端名称)

默认值:null

在Redis节点里显示的客户端名称。

sslEnableEndpointIdentification(启用SSL终端识别)

默认值:true

开启SSL终端识别能力。

sslProvider(SSL实现方式)

默认值:JDK

确定采用哪种方式(JDK或OPENSSL)来实现SSL连接。

sslTruststore(SSL信任证书库路径)

默认值:null

指定SSL信任证书库的路径。

sslTruststorePassword(SSL信任证书库密码)

默认值:null

指定SSL信任证书库的密码。

sslKeystore(SSL钥匙库路径)

默认值:null

指定SSL钥匙库的路径。

sslKeystorePassword(SSL钥匙库密码)

默认值:null

指定SSL钥匙库的密码。

单Redis节点模式

1、程序化配置

1
2
3
4
5
6
// 默认连接地址 127.0.0.1:6379
RedissonClient redisson = Redisson.create();

Config config = new Config();
config.useSingleServer().setAddress("myredisserver:6379");
RedissonClient redisson = Redisson.create(config);

2、单节点设置

SingleServerConfig 类的设置参数如下:

address(节点地址)

可以通过host:port的格式来指定节点地址。

subscriptionConnectionMinimumIdleSize(发布和订阅连接的最小空闲连接数)

默认值:1

用于发布和订阅连接的最小保持连接数(长连接)。Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。

subscriptionConnectionPoolSize(发布和订阅连接池大小)

默认值:50

用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

connectionMinimumIdleSize(最小空闲连接数)

默认值:32

最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时写入反应速度。

connectionPoolSize(连接池大小)

默认值:64

连接池最大容量。连接池的连接数量自动弹性伸缩。

dnsMonitoring(是否启用DNS监测)

默认值:false

在启用该功能以后,Redisson将会监测DNS的变化情况。

dnsMonitoringInterval(DNS监测时间间隔,单位:毫秒)

默认值:5000

监测DNS的变化情况的时间间隔。

idleConnectionTimeout(连接空闲超时,单位:毫秒)

默认值:10000

如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。

connectTimeout(连接超时,单位:毫秒)

默认值:10000

同节点建立连接时的等待超时。时间单位是毫秒。

timeout(命令等待超时,单位:毫秒)

默认值:3000

等待节点回复命令的时间。该时间从命令发送成功时开始计时。

retryAttempts(命令失败重试次数)

默认值:3

如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。

retryInterval(命令重试发送时间间隔,单位:毫秒)

默认值:1500

在一条命令发送失败以后,等待重试发送的时间间隔。时间单位是毫秒。

reconnectionTimeout(重新连接时间间隔,单位:毫秒)

默认值:3000

当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。

failedAttempts(执行失败最大次数)

默认值:3

在某个节点执行相同或不同命令时,连续 失败 failedAttempts(执行失败最大次数) 时,该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。

database(数据库编号)

默认值:0

尝试连接的数据库编号。

password(密码)

默认值:null

用于节点身份验证的密码。

subscriptionsPerConnection(单个连接最大订阅数量)

默认值:5

每个连接的最大订阅数量。

clientName(客户端名称)

默认值:null

在Redis节点里显示的客户端名称。

sslEnableEndpointIdentification(启用SSL终端识别)

默认值:true

开启SSL终端识别能力。

sslProvider(SSL实现方式)

默认值:JDK

确定采用哪种方式(JDK或OPENSSL)来实现SSL连接。

sslTruststore(SSL信任证书库路径)

默认值:null

指定SSL信任证书库的路径。

sslTruststorePassword(SSL信任证书库密码)

默认值:null

指定SSL信任证书库的密码。

sslKeystore(SSL钥匙库路径)

默认值:null

指定SSL钥匙库的路径。

sslKeystorePassword(SSL钥匙库密码)

默认值:null

指定SSL钥匙库的密码。

3、配置文件

配置单节点模式可以通过指定一个JSON格式的文件来实现。以下是JSON格式的配置文件样本。文件中的字段名称必须与SingleServerConfig和Config对象里的字段名称相符。

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
{
"singleServerConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"reconnectionTimeout":3000,
"failedAttempts":3,
"password":null,
"subscriptionsPerConnection":5,
"clientName":null,
"address": "redis://127.0.0.1:6379",
"subscriptionConnectionMinimumIdleSize":1,
"subscriptionConnectionPoolSize":50,
"connectionMinimumIdleSize":32,
"connectionPoolSize":64,
"database":0,
"dnsMonitoring":false,
"dnsMonitoringInterval":5000
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.JsonJacksonCodec"
},
"transportMode":"NIO"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
singleServerConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: null
subscriptionsPerConnection: 5
clientName: null
address: "redis://127.0.0.1:6379"
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 32
connectionPoolSize: 64
database: 0
dnsMonitoring: false
dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode":"NIO"
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
<redisson:client
id="redisson"
name="redisson1,redisson2"
threads="0"
netty-threads="0"
codec-ref="myCodec"
transport-mode="NIO"
redisson-reference-enabled="true"
codec-provider-ref="myCodecProvider"
resolver-provider-ref="myResolverProvider"
executor-ref="myExecutor"
event-loop-group-ref="myEventLoopGroup"
>
<!--
这里的name属性和qualifier子元素不能同时使用。

id和name的属性都可以被用来作为qualifier的备选值。
-->
<!--<qualifier value="redisson3"/>-->
<redisson:single-server
idle-connection-timeout="10000"
ping-timeout="1000"
connect-timeout="10000"
timeout="3000"
retry-attempts="3"
retry-interval="1500"
reconnection-timeout="3000"
failed-attempts="3"
password="do_not_use_if_it_is_not_set"
subscriptions-per-connection="5"
client-name="none"
address="redis://127.0.0.1:6379"
subscription-connection-minimum-idle-size="1"
subscription-connection-pool-size="50"
connection-minimum-idle-size="10"
connection-pool-size="64"
database="0"
dns-monitoring="false"
dns-monitoring-interval="5000"
/>
</redisson:client>

<!-- 最基本配置 -->
<!-- 默认地址是 127.0.0.1:6379 -->
<redisson:client>
<redisson:single-server/>
</redisson:client>

哨兵模式

1、程序化配置

1
2
3
4
5
6
7
8
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
//可以用"rediss://"来启用SSL连接
.addSentinelAddress("127.0.0.1:26389", "127.0.0.1:26379")
.addSentinelAddress("127.0.0.1:26319");

RedissonClient redisson = Redisson.create(config);

2、设置

SentinelServersConfig 类的设置参数如下:

dnsMonitoringInterval(DNS监控间隔,单位:毫秒)

默认值:5000

用来指定检查节点DNS变化的时间间隔。使用的时候应该确保JVM里的DNS数据的缓存时间保持在足够低的范围才有意义。用-1来禁用该功能。

masterName(主服务器的名称)

主服务器的名称是哨兵进程中用来监测主从服务切换情况的。

addSentinelAddress(添加哨兵节点地址)

可以通过host:port的格式来指定哨兵节点的地址。多个节点可以一次性批量添加。

readMode(读取操作的负载均衡模式)

默认值: SLAVE(只在从服务节点里读取)

注:在从服务节点里读取的数据说明已经至少有两个节点保存了该数据,确保了数据的高可用性。

设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。 MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取。

subscriptionMode(订阅操作的负载均衡模式)

默认值:SLAVE(只在从服务节点里订阅)

设置订阅操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里订阅。 MASTER - 只在主服务节点里订阅。

loadBalancer(负载均衡算法类的选择)

默认值: org.redisson.connection.balancer.RoundRobinLoadBalancer

在使用多个Redis服务节点的环境里,可以选用以下几种负载均衡方式选择一个节点: org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法 org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法 org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法

subscriptionConnectionMinimumIdleSize(从节点发布和订阅连接的最小空闲连接数)

默认值:1

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的最小保持连接数(长连接)。Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。

subscriptionConnectionPoolSize(从节点发布和订阅连接池大小)

默认值:50

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

slaveConnectionMinimumIdleSize(从节点最小空闲连接数)

默认值:32

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时读取反映速度。

slaveConnectionPoolSize(从节点连接池大小)

默认值:64

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

masterConnectionMinimumIdleSize(主节点最小空闲连接数)

默认值:32

多从节点的环境里,每个 主节点的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时写入反应速度。

masterConnectionPoolSize(主节点连接池大小)

默认值:64

主节点的连接池最大容量。连接池的连接数量自动弹性伸缩。

idleConnectionTimeout(连接空闲超时,单位:毫秒)

默认值:10000

如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。

connectTimeout(连接超时,单位:毫秒)

默认值:10000

同任何节点建立连接时的等待超时。时间单位是毫秒。

timeout(命令等待超时,单位:毫秒)

默认值:3000

等待节点回复命令的时间。该时间从命令发送成功时开始计时。

retryAttempts(命令失败重试次数)

默认值:3

如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。

retryInterval(命令重试发送时间间隔,单位:毫秒)

默认值:1500

在一条命令发送失败以后,等待重试发送的时间间隔。时间单位是毫秒。

reconnectionTimeout(重新连接时间间隔,单位:毫秒)

默认值:3000

当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。

failedAttempts(执行失败最大次数)

默认值:3

在某个节点执行相同或不同命令时,连续 失败 failedAttempts(执行失败最大次数) 时,该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。

database(数据库编号)

默认值:0

尝试连接的数据库编号。

password(密码)

默认值:null

用于节点身份验证的密码。

subscriptionsPerConnection(单个连接最大订阅数量)

默认值:5

每个连接的最大订阅数量。

clientName(客户端名称)

默认值:null

在Redis节点里显示的客户端名称。

sslEnableEndpointIdentification(启用SSL终端识别)

默认值:true

开启SSL终端识别能力。

sslProvider(SSL实现方式)

默认值:JDK

确定采用哪种方式(JDK或OPENSSL)来实现SSL连接。

sslTruststore(SSL信任证书库路径)

默认值:null

指定SSL信任证书库的路径。

sslTruststorePassword(SSL信任证书库密码)

默认值:null

指定SSL信任证书库的密码。

sslKeystore(SSL钥匙库路径)

默认值:null

指定SSL钥匙库的路径。

sslKeystorePassword(SSL钥匙库密码)

默认值:null

指定SSL钥匙库的密码。

3、配置文件

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
{
"sentinelServersConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"reconnectionTimeout":3000,
"failedAttempts":3,
"password":null,
"subscriptionsPerConnection":5,
"clientName":null,
"loadBalancer":{
"class":"org.redisson.connection.balancer.RoundRobinLoadBalancer"
},
"slaveSubscriptionConnectionMinimumIdleSize":1,
"slaveSubscriptionConnectionPoolSize":50,
"slaveConnectionMinimumIdleSize":32,
"slaveConnectionPoolSize":64,
"masterConnectionMinimumIdleSize":32,
"masterConnectionPoolSize":64,
"readMode":"SLAVE",
"sentinelAddresses":[
"redis://127.0.0.1:26379",
"redis://127.0.0.1:26389"
],
"masterName":"mymaster",
"database":0
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.JsonJacksonCodec"
},
"transportMode":"NIO"
}
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
sentinelServersConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 32
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 32
masterConnectionPoolSize: 64
readMode: "SLAVE"
sentinelAddresses:
- "redis://127.0.0.1:26379"
- "redis://127.0.0.1:26389"
masterName: "mymaster"
database: 0
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode":"NIO"
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
<redisson:client
id="redisson"
name="redisson1,redisson2"
threads="0"
netty-threads="0"
codec-ref="myCodec"
transport-mode="NIO"
redisson-reference-enabled="true"
codec-provider-ref="myCodecProvider"
resolver-provider-ref="myResolverProvider"
executor-ref="myExecutor"
event-loop-group-ref="myEventLoopGroup"
>
<!--
这里的name属性和qualifier子元素不能同时使用。

id和name的属性都可以被用来作为qualifier的备选值。
-->
<!--<qualifier value="redisson3"/>-->
<redisson:sentinel-servers
idle-connection-timeout="10000"
ping-timeout="1000"
connect-timeout="10000"
timeout="3000"
retry-attempts="3"
retry-interval="1500"
reconnection-timeout="3000"
failed-attempts="3"
password="do_not_use_if_it_is_not_set"
subscriptions-per-connection="5"
client-name="none"
load-balancer-ref="myLoadBalancer"
subscription-connection-minimum-idle-size="1"
subscription-connection-pool-size="50"
slave-connection-minimum-idle-size="10"
slave-connection-pool-size="64"
master-connection-minimum-idle-size="10"
master-connection-pool-size="64"
read-mode="SLAVE"
subscription-mode="SLAVE"
master-name="myMaster"
database="0"
>
<redisson:sentinel-address value="redis://127.0.0.1:26379" />
<redisson:sentinel-address value="redis://127.0.0.1:26380" />
</redisson:sentinel-servers>
</redisson:client>

<!-- 最基本配置 -->
<redisson:client>
<redisson:sentinel-servers master-name="myMaster">
<redisson:sentinel-address value="redis://127.0.0.1:26379" />
<redisson:sentinel-address value="redis://127.0.0.1:26380" />
....
</redisson:sentinel-servers>
</redisson:client>

主从模式

1、程序化配置

1
2
3
4
5
6
7
8
Config config = new Config();
config.useMasterSlaveServers()
//可以用"rediss://"来启用SSL连接
.setMasterAddress("redis://127.0.0.1:6379")
.addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419")
.addSlaveAddress("redis://127.0.0.1:6399");

RedissonClient redisson = Redisson.create(config);

2、主从模式设置

MasterSlaveServersConfig 类的设置参数如下:

dnsMonitoringInterval(DNS监控间隔,单位:毫秒)

默认值:5000

用来指定检查节点DNS变化的时间间隔。使用的时候应该确保JVM里的DNS数据的缓存时间保持在足够低的范围才有意义。用-1来禁用该功能。

masterAddress(主节点地址)

可以通过host:port的格式来指定主节点地址。

addSlaveAddress(添加从主节点地址)

可以通过host:port的格式来指定从节点的地址。多个节点可以一次性批量添加。

readMode(读取操作的负载均衡模式)

默认值: SLAVE(只在从服务节点里读取)

注:在从服务节点里读取的数据说明已经至少有两个节点保存了该数据,确保了数据的高可用性。

设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。 MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取。

subscriptionMode(订阅操作的负载均衡模式)

默认值:SLAVE(只在从服务节点里订阅)

设置订阅操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里订阅。 MASTER - 只在主服务节点里订阅。

loadBalancer(负载均衡算法类的选择)

默认值: org.redisson.connection.balancer.RoundRobinLoadBalancer

在使用多个Redis服务节点的环境里,可以选用以下几种负载均衡方式选择一个节点: org.redisson.connection.balancer.WeightedRoundRobinBalancer - 权重轮询调度算法 org.redisson.connection.balancer.RoundRobinLoadBalancer - 轮询调度算法 org.redisson.connection.balancer.RandomLoadBalancer - 随机调度算法

subscriptionConnectionMinimumIdleSize(从节点发布和订阅连接的最小空闲连接数)

默认值:1

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的最小保持连接数(长连接)。Redisson内部经常通过发布和订阅来实现许多功能。长期保持一定数量的发布订阅连接是必须的。

subscriptionConnectionPoolSize(从节点发布和订阅连接池大小)

默认值:50

多从节点的环境里,每个 从服务节点里用于发布和订阅连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

slaveConnectionMinimumIdleSize(从节点最小空闲连接数)

默认值:32

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时读取反映速度。

slaveConnectionPoolSize(从节点连接池大小)

默认值:64

多从节点的环境里,每个 从服务节点里用于普通操作(非 发布和订阅)连接的连接池最大容量。连接池的连接数量自动弹性伸缩。

masterConnectionMinimumIdleSize(主节点最小空闲连接数)

默认值:32

多从节点的环境里,每个 主节点的最小保持连接数(长连接)。长期保持一定数量的连接有利于提高瞬时写入反应速度。

masterConnectionPoolSize(主节点连接池大小)

默认值:64

主节点的连接池最大容量。连接池的连接数量自动弹性伸缩。

idleConnectionTimeout(连接空闲超时,单位:毫秒)

默认值:10000

如果当前连接池里的连接数量超过了最小空闲连接数,而同时有连接空闲时间超过了该数值,那么这些连接将会自动被关闭,并从连接池里去掉。时间单位是毫秒。

connectTimeout(连接超时,单位:毫秒)

默认值:10000

同任何节点建立连接时的等待超时。时间单位是毫秒。

timeout(命令等待超时,单位:毫秒)

默认值:3000

等待节点回复命令的时间。该时间从命令发送成功时开始计时。

retryAttempts(命令失败重试次数)

默认值:3

如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。

retryInterval(命令重试发送时间间隔,单位:毫秒)

默认值:1500

在一条命令发送失败以后,等待重试发送的时间间隔。时间单位是毫秒。

reconnectionTimeout(重新连接时间间隔,单位:毫秒)

默认值:3000

当与某个节点的连接断开时,等待与其重新建立连接的时间间隔。时间单位是毫秒。

failedAttempts(执行失败最大次数)

默认值:3

在某个节点执行相同或不同命令时,连续 失败 failedAttempts(执行失败最大次数) 时,该节点将被从可用节点列表里清除,直到 reconnectionTimeout(重新连接时间间隔) 超时以后再次尝试。

database(数据库编号)

默认值:0

尝试连接的数据库编号。

password(密码)

默认值:null

用于节点身份验证的密码。

subscriptionsPerConnection(单个连接最大订阅数量)

默认值:5

每个连接的最大订阅数量。

clientName(客户端名称)

默认值:null

在Redis节点里显示的客户端名称。

sslEnableEndpointIdentification(启用SSL终端识别)

默认值:true

开启SSL终端识别能力。

sslProvider(SSL实现方式)

默认值:JDK

确定采用哪种方式(JDK或OPENSSL)来实现SSL连接。

sslTruststore(SSL信任证书库路径)

默认值:null

指定SSL信任证书库的路径。

sslTruststorePassword(SSL信任证书库密码)

默认值:null

指定SSL信任证书库的密码。

sslKeystore(SSL钥匙库路径)

默认值:null

指定SSL钥匙库的路径。

sslKeystorePassword(SSL钥匙库密码)

默认值:null

指定SSL钥匙库的密码。

3、文件配置主从模式

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
{
"masterSlaveServersConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"reconnectionTimeout":3000,
"failedAttempts":3,
"password":null,
"subscriptionsPerConnection":5,
"clientName":null,
"loadBalancer":{
"class":"org.redisson.connection.balancer.RoundRobinLoadBalancer"
},
"slaveSubscriptionConnectionMinimumIdleSize":1,
"slaveSubscriptionConnectionPoolSize":50,
"slaveConnectionMinimumIdleSize":32,
"slaveConnectionPoolSize":64,
"masterConnectionMinimumIdleSize":32,
"masterConnectionPoolSize":64,
"readMode":"SLAVE",
"slaveAddresses":[
"redis://127.0.0.1:6381",
"redis://127.0.0.1:6380"
],
"masterAddress": "redis://127.0.0.1:6379",
"database":0
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.JsonJacksonCodec"
},
"transportMode":"NIO"
}
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
masterSlaveServersConfig:
idleConnectionTimeout: 10000
pingTimeout: 1000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
reconnectionTimeout: 3000
failedAttempts: 3
password: null
subscriptionsPerConnection: 5
clientName: null
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
slaveSubscriptionConnectionMinimumIdleSize: 1
slaveSubscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 32
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 32
masterConnectionPoolSize: 64
readMode: "SLAVE"
slaveAddresses:
- "redis://127.0.0.1:6381"
- "redis://127.0.0.1:6380"
masterAddress: "redis://127.0.0.1:6379"
database: 0
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
"transportMode":"NIO"
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
<redisson:client
id="redisson"
name="redisson1,redisson2"
threads="0"
netty-threads="0"
codec-ref="myCodec"
transport-mode="NIO"
redisson-reference-enabled="true"
codec-provider-ref="myCodecProvider"
resolver-provider-ref="myResolverProvider"
executor-ref="myExecutor"
event-loop-group-ref="myEventLoopGroup"
>
<!--
这里的name属性和qualifier子元素不能同时使用。

id和name的属性都可以被用来作为qualifier的备选值。
-->
<!--<qualifier value="redisson3"/>-->
<redisson:master-slave-servers
idle-connection-timeout="10000"
ping-timeout="1000"
connect-timeout="10000"
timeout="3000"
retry-attempts="3"
retry-interval="1500"
reconnection-timeout="3000"
failed-attempts="3"
password="do_not_use_if_it_is_not_set"
subscriptions-per-connection="5"
client-name="none"
load-balancer-ref="myLoadBalancer"
subscription-connection-minimum-idle-size="1"
subscription-connection-pool-size="50"
slave-connection-minimum-idle-size="10"
slave-connection-pool-size="64"
master-connection-minimum-idle-size="10"
master-connection-pool-size="64"
read-mode="SLAVE"
subscription-mode="SLAVE"
master-address="redis://127.0.0.1:6379"
database="0"
>
<redisson:slave-address value="redis://127.0.0.1:6380" />
<redisson:slave-address value="redis://127.0.0.1:6381" />
</redisson:master-slave-servers>
</redisson:client>

<!-- 最基本配置 -->
<redisson:client>
<redisson:master-slave-servers master-address="redis://127.0.0.1:6379">
<redisson:slave-address value="redis://127.0.0.1:6380" />
<redisson:slave-address value="redis://127.0.0.1:6381" />
...
</redisson:master-slave-servers>
</redisson:client>

三、程序接口调用方式

RedissonClient、RedissonReactiveClient和RedissonRxClient实例本身和Redisson提供的所有分布式对象都是线程安全的。

Redisson为每个操作都提供了** 自动重试策略 **,当某个命令执行失败时,Redisson会自动进行重试。自动重试策略可以通过修改retryAttempts(默认值:3)参数和retryInterval(默认值:1000毫秒)参数来进行优化调整。当等待时间达到retryInterval指定的时间间隔以后,将自动重试下一次。全部重试失败以后将抛出错误。

Redisson框架提供的几乎所有对象都包含了同步和异步相互匹配的方法。这些对象都可以通过RedissonClient接口获取。同时还为大部分Redisson对象提供了满足异步流处理标准的程序接口RedissonReactiveClient。除此外还提供了RxJava2规范的RedissonRxClient程序接口。

以下是关于使用RAtomicLong对象的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RedissonClient client = Redisson.create(config);
RAtomicLong longObject = client.getAtomicLong('myLong');
// 同步执行方式
longObject.compareAndSet(3, 401);
// 异步执行方式
RFuture<Boolean> result = longObject.compareAndSetAsync(3, 401);

RedissonReactiveClient client = Redisson.createReactive(config);
RAtomicLongReactive longObject = client.getAtomicLong('myLong');
// 异步流执行方式
Mono<Boolean> result = longObject.compareAndSet(3, 401);
RedissonRxClient client = Redisson.createRx(config);
RAtomicLongRx longObject= client.getAtomicLong("myLong");
// RxJava2方式
Flowable<Boolean result = longObject.compareAndSet(3, 401);

异步执行方式

几乎所有的Redisson对象都实现了一个异步接口,异步接口提供的方法名称与其同步接口的方法名称相互匹配。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// RAtomicLong接口继承了RAtomicLongAsync接口
RAtomicLongAsync longObject = client.getAtomicLong("myLong");
RFuture<Boolean> future = longObject.compareAndSetAsync(1, 401);
异步执行的方法都会返回一个实现了RFuture接口的对象。该对象同时提供了java.util.concurrent.CompletionStage和java.util.concurrent.Future两个异步接口。

future.whenComplete((res, exception) -> {
// ...
});
// 或者
future.thenAccept(res -> {
// 处理返回
}).exceptionally(exception -> {
// 处理错误
});

异步流执行方式

Redisson为大多数分布式数据结构提供了满足Reactor项目的异步流处理标准的程序接口。该接口通过两种方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
基于Project Reactor标准的实现方式。使用范例如下:
RedissonReactiveClient client = Redisson.createReactive(config);
RAtomicLongReactive atomicLong = client.getAtomicLong("myLong");
Mono<Boolean> cs = longObject.compareAndSet(10, 91);
Mono<Long> get = longObject.get();

Publisher<Long> getPublisher = longObject.get();
基于RxJava2标准的实现方式。使用范例如下:
RedissonRxClient client = Redisson.createRx(config);
RAtomicLongRx atomicLong = client.getAtomicLong("myLong");
Single<Boolean> cs = longObject.compareAndSet(10, 91);
Single<Long> get = longObject.get();

单个集合数据分片

在集群模式下,Redisson为单个Redis集合类型提供了自动分片的功能。

Redisson提供的所有数据结构都支持在集群环境下使用,但每个数据结构只被保存在一个固定的槽内。Redisson PRO提供的自动分片功能能够将单个数据结构拆分,然后均匀的分布在整个集群里,而不是被挤在单一一个槽里。自动分片功能的优势主要有以下几点:

  • 单个数据结构可以充分利用整个集群内存资源,而不是被某一个节点的内存限制。
  • 将单个数据结构分片以后分布在集群中不同的节点里,不仅可以大幅提高读写性能,还能够保证读写性能随着集群的扩张而自动提升。

Redisson通过自身的分片算法,将一个大集合拆分为若干个片段(默认231个,分片数量范围是3 - 16834),然后将拆分后的片段均匀的分布到集群里各个节点里,保证每个节点分配到的片段数量大体相同。比如在默认情况下231个片段分到含有4个主节点的集群里,每个主节点将会分配到大约57个片段,同样的道理如果有5个主节点,每个节点会分配到大约46个片段。

目前支持的数据结构类型和服务包括集(Set)、映射(Map)、BitSet、布隆过滤器(Bloom Filter)、Spring Cache和Hibernate Cache。该功能仅限于Redisson PRO版本。

四、分布式对象

每个Redisson对象实例都会有一个与之对应的Redis数据实例,可以通过调用getName方法来取得Redis数据实例的名称(key)。

1
2
3
4
5
6
7
8
9
10
11
12
RMap map = redisson.getMap("mymap");
map.getName(); // = mymap
所有与Redis key相关的操作都归纳在RKeys这个接口里:

RKeys keys = redisson.getKeys();

Iterable<String> allKeys = keys.getKeys();
Iterable<String> foundedKeys = keys.getKeysByPattern('key*');
long numOfDeletedKeys = keys.delete("obj1", "obj2", "obj3");
long deletedKeysAmount = keys.deleteByPattern("test?");
String randomKey = keys.randomKey();
long keysAmount = keys.count();

通用对象桶(Object Bucket)

Redisson的分布式RBucketJava对象是一种通用对象桶可以用来存放任类型的对象。 除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RBucket<AnyObject> bucket = redisson.getBucket("anyObject");
bucket.set(new AnyObject(1));
AnyObject obj = bucket.get();

bucket.trySet(new AnyObject(3));
bucket.compareAndSet(new AnyObject(4), new AnyObject(5));
bucket.getAndSet(new AnyObject(6));
还可以通过RBuckets接口实现批量操作多个RBucket对象:

RBuckets buckets = redisson.getBuckets();
List<RBucket<V>> foundBuckets = buckets.find("myBucket*");
Map<String, V> loadedBuckets = buckets.get("myBucket1", "myBucket2", "myBucket3");

Map<String, Object> map = new HashMap<>();
map.put("myBucket1", new MyObject());
map.put("myBucket2", new MyObject());

// 利用Redis的事务特性,同时保存所有的通用对象桶,如果任意一个通用对象桶已经存在则放弃保存其他所有数据。
buckets.trySet(map);
// 同时保存全部通用对象桶。
buckets.set(map);

二进制流(Binary Stream)

Redisson的分布式RBinaryStream Java对象同时提供了InputStream接口和OutputStream接口的实现。流的最大容量受Redis主节点的内存大小限制。

1
2
3
4
5
6
7
8
9
10
11
RBinaryStream stream = redisson.getBinaryStream("anyStream");
byte[] content = ...
stream.set(content);

InputStream is = stream.getInputStream();
byte[] readBuffer = new byte[512];
is.read(readBuffer);

OutputStream os = stream.getOuputStream();
byte[] contentToWrite = ...
os.write(contentToWrite);

地理空间对象桶(Geospatial Bucket)

Redisson的分布式RGeo Java对象是一种专门用来储存与地理位置有关的对象桶。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
9
10
RGeo<String> geo = redisson.getGeo("test");
geo.add(new GeoEntry(13.361389, 38.115556, "Palermo"),
new GeoEntry(15.087269, 37.502669, "Catania"));
geo.addAsync(37.618423, 55.751244, "Moscow");

Double distance = geo.dist("Palermo", "Catania", GeoUnit.METERS);
geo.hashAsync("Palermo", "Catania");
Map<String, GeoPosition> positions = geo.pos("test2", "Palermo", "test3", "Catania", "test1");
List<String> cities = geo.radius(15, 37, 200, GeoUnit.KILOMETERS);
Map<String, GeoPosition> citiesWithPositions = geo.radiusWithPosition(15, 37, 200, GeoUnit.KILOMETERS);

BitSet

Redisson的分布式RBitSetJava对象采用了与java.util.BiteSet类似结构的设计风格。可以理解为它是一个分布式的可伸缩式位向量。需要注意的是RBitSet的大小受Redis限制,最大长度为4 294 967 295。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
RBitSet set = redisson.getBitSet("simpleBitset");
set.set(0, true);
set.set(1812, false);
set.clear(0);
set.addAsync("e");
set.xor("anotherBitset");

BitSet数据分片(Sharding)(分布式RoaringBitMap)

基于Redis的Redisson集群分布式BitSet通过RClusteredBitSet接口,为集群状态下的Redis环境提供了BitSet数据分片的功能。通过优化后更加有效的分布式RoaringBitMap算法,突破了原有的BitSet大小限制,达到了集群物理内存容量大小。在这里可以获取更多的内部信息。

1
2
3
4
5
6
7
RClusteredBitSet set = redisson.getClusteredBitSet("simpleBitset");
set.set(0, true);
set.set(1812, false);
set.clear(0);
set.addAsync("e");
set.xor("anotherBitset");
该功能仅限于Redisson PRO版本。

原子整长形(AtomicLong)

Redisson的分布式整长形RAtomicLong对象和Java中的java.util.concurrent.atomic.AtomicLong对象类似。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
RAtomicLong atomicLong = redisson.getAtomicLong("myAtomicLong");
atomicLong.set(3);
atomicLong.incrementAndGet();
atomicLong.get();

原子双精度浮点(AtomicDouble)

Redisson还提供了分布式原子双精度浮点RAtomicDouble,弥补了Java自身的不足。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
RAtomicDouble atomicDouble = redisson.getAtomicDouble("myAtomicDouble");
atomicDouble.set(2.81);
atomicDouble.addAndGet(4.11);
atomicDouble.get();

话题(订阅分发)

Redisson的分布式话题RTopic标准的接口。

1
2
3
4
5
6
7
RTopic topic = redisson.getTopic("anyTopic");
topic.addListener(SomeObject.class, new MessageListener<SomeObject>() {
@Override
public void onMessage(String channel, SomeObject message) {
//...
}
});

// 在其他线程或JVM节点
RTopic topic = redisson.getTopic(“anyTopic”);
long clientsReceivedMessage = topic.publish(new SomeObject());
在Redis节点故障转移(主从切换)或断线重连以后,所有的话题监听器将自动完成话题的重新订阅。

模糊话题

Redisson的模糊话题RPatternTopic对象可以通过正式表达式来订阅多个话题。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
// 订阅所有满足`topic1.*`表达式的话题
RPatternTopic topic1 = redisson.getPatternTopic("topic1.*");
int listenerId = topic1.addListener(Message.class, new PatternMessageListener<Message>() {
@Override
public void onMessage(String pattern, String channel, Message msg) {
Assert.fail();
}
});

在Redis节点故障转移(主从切换)或断线重连以后,所有的模糊话题监听器将自动完成话题的重新订阅。

布隆过滤器(Bloom Filter)

Redisson利用Redis实现了Java分布式布隆过滤器(Bloom Filter)。所含最大比特数量为2^32。

1
2
3
4
5
6
RBloomFilter<SomeObject> bloomFilter = redisson.getBloomFilter("sample");
// 初始化布隆过滤器,预计统计元素数量为55000000,期望误差率为0.03
bloomFilter.tryInit(55000000L, 0.03);
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));

布隆过滤器数据分片(Sharding)

基于Redis的Redisson集群分布式布隆过滤器通过RClusteredBloomFilter接口,为集群状态下的Redis环境提供了布隆过滤器数据分片的功能。 通过优化后更加有效的算法,通过压缩未使用的比特位来释放集群内存空间。每个对象的状态都将被分布在整个集群中。所含最大比特数量为2^64。在这里可以获取更多的内部信息。

1
2
3
4
5
6
7
8
9
RClusteredBloomFilter<SomeObject> bloomFilter = redisson.getClusteredBloomFilter("sample");
// 采用以下参数创建布隆过滤器
// expectedInsertions = 255000000
// falseProbability = 0.03
bloomFilter.tryInit(255000000L, 0.03);
bloomFilter.add(new SomeObject("field1Value", "field2Value"));
bloomFilter.add(new SomeObject("field5Value", "field8Value"));
bloomFilter.contains(new SomeObject("field1Value", "field8Value"));
该功能仅限于Redisson PRO版本。

基数估计算法(HyperLogLog)

Redisson利用Redis实现了Java分布式基数估计算法(HyperLogLog)对象。该对象可以在有限的空间内通过概率算法统计大量的数据。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
RHyperLogLog<Integer> log = redisson.getHyperLogLog("log");
log.add(1);
log.add(2);
log.add(3);

log.count();

整长型累加器(LongAdder)

基于Redis的Redisson分布式整长型累加器(LongAdder)采用了与java.util.concurrent.atomic.LongAdder类似的接口。通过利用客户端内置的LongAdder对象,为分布式环境下递增和递减操作提供了很高得性能。据统计其性能最高比分布式AtomicLong对象快 12000 倍。完美适用于分布式统计计量场景。

1
2
3
4
5
6
7
8
9
RLongAdder atomicLong = redisson.getLongAdder("myLongAdder");
atomicLong.add(12);
atomicLong.increment();
atomicLong.decrement();
atomicLong.sum();
当不再使用整长型累加器对象的时候应该自行手动销毁,如果Redisson对象被关闭(shutdown)了,则不用手动销毁。

RLongAdder atomicLong = ...
atomicLong.destroy();

双精度浮点累加器(DoubleAdder)

基于Redis的Redisson分布式双精度浮点累加器(DoubleAdder)采用了与java.util.concurrent.atomic.DoubleAdder类似的接口。通过利用客户端内置的DoubleAdder对象,为分布式环境下递增和递减操作提供了很高得性能。据统计其性能最高比分布式AtomicDouble对象快 12000 倍。完美适用于分布式统计计量场景。

1
2
3
4
5
RLongDouble atomicDouble = redisson.getLongDouble("myLongDouble");
atomicDouble.add(12);
atomicDouble.increment();
atomicDouble.decrement();
atomicDouble.sum();

当不再使用双精度浮点累加器对象的时候应该自行手动销毁,如果Redisson对象被关闭(shutdown)了,则不用手动销毁。

限流器(RateLimiter)

基于Redis的分布式限流器(RateLimiter)可以用来在分布式环境下现在请求方的调用频率。既适用于不同Redisson实例下的多线程限流,也适用于相同Redisson实例下的多线程限流。该算法不保证公平性。除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
RRateLimiter rateLimiter = redisson.getRateLimiter("myRateLimiter");
// 初始化
// 最大流速 = 每1秒钟产生10个令牌
rateLimiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);

CountDownLatch latch = new CountDownLatch(2);
limiter.acquire(3);
// ...

Thread t = new Thread(() -> {
limiter.acquire(2);
// ...
});

五、分布式集合

1、映射(Map)

基于Redis的Redisson的分布式映射结构的RMap Java对象实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。与HashMap不同的是,RMap保持了元素的插入顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

除了同步接口外,还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。如果你想用Redis Map来保存你的POJO的话,可以考虑使用分布式实时对象(Live Object)服务。

在特定的场景下,映射缓存(Map)上的高度频繁的读取操作,使网络通信都被视为瓶颈时,可以使用Redisson提供的带有本地缓存功能的映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
RMap<String, SomeObject> map = redisson.getMap("anyMap");
SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");

map.fastPut("321", new SomeObject());
map.fastRemove("321");

RFuture<SomeObject> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");

map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");

映射的字段锁的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RMap<MyKey, MyValue> map = redisson.getMap("anyMap");
MyKey k = new MyKey();
RLock keyLock = map.getLock(k);
keyLock.lock();
try {
MyValue v = map.get(k);
// 其他业务逻辑
} finally {
keyLock.unlock();
}

RReadWriteLock rwLock = map.getReadWriteLock(k);
rwLock.readLock().lock();
try {
MyValue v = map.get(k);
// 其他业务逻辑
} finally {
keyLock.readLock().unlock();
}

映射(Map)的元素淘汰(Eviction),本地缓存(LocalCache)和数据分片(Sharding)

Redisson提供了一系列的映射类型的数据结构,这些结构按特性主要分为三大类:

*元素淘汰(Eviction) *类 – 带有元素淘汰(Eviction)机制的映射类允许针对一个映射中每个元素单独设定 有效时间 和 最长闲置时间 。

本地缓存(LocalCache) 类 – 本地缓存(Local Cache)也叫就近缓存(Near Cache)。这类映射的使用主要用于在特定的场景下,映射缓存(MapCache)上的高度频繁的读取操作,使网络通信都被视为瓶颈的情况。Redisson与Redis通信的同时,还将部分数据保存在本地内存里。这样的设计的好处是它能将读取速度提高最多 45倍 。 所有同名的本地缓存共用一个订阅发布话题,所有更新和过期消息都将通过该话题共享。

数据分片(Sharding) 类 – 数据分片(Sharding)类仅适用于Redis集群环境下,因此带有数据分片(Sharding)功能的映射也叫集群分布式映射。它利用分库的原理,将单一一个映射结构切分为若干个小的映射,并均匀的分布在集群中的各个槽里。这样的设计能使一个单一映射结构突破Redis自身的容量限制,让其容量随集群的扩大而增长。在扩容的同时,还能够使读写性能和元素淘汰处理能力随之成线性增长。

以下列表是Redisson提供的所有映射的名称及其特性:

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
接口名称
中文名称 RedissonClient
对应的构造方法 本地缓存功能
Local Cache 数据分片功能
Sharding 元素淘汰功能
Eviction
RMap
映射 getMap()
No No No
RMapCache
映射缓存 getMapCache()
No No Yes
RLocalCachedMap
本地缓存映射 getLocalCachedMap()
Yes No No
RLocalCachedMap
Cache
本地缓存映射缓存
仅限于Redisson PRO版本 getLocalCachedMapCache() Yes No Yes
RClusteredMap
集群分布式映射存
仅限于Redisson PRO版本 getClusteredMap() No Yes No
RClusteredMapCache
集群分布式映射缓存存
仅限于Redisson PRO版本 getClusteredMapCache() No Yes Yes
RClusteredLocal
CachedMap
集群分布式本地缓存映射存
仅限于Redisson PRO版本 getClusteredLocal
CachedMap() Yes Yes No
RClusteredLocal
CachedMapCache
集群分布式本地缓存映射缓存存
仅限于Redisson PRO版本 getClusteredLocal
CachedMapCache() Yes Yes Yes
除此以外,Redisson还提供了Spring Cache和JCache的实现。

元素淘汰功能(Eviction)

Redisson的分布式的RMapCache Java对象在基于RMap的前提下实现了针对单个元素的淘汰机制。同时仍然保留了元素的插入顺序。由于RMapCache是基于RMap实现的,使它同时继承了java.util.concurrent.ConcurrentMap接口和java.util.Map接口。Redisson提供的Spring Cache整合以及JCache正是基于这样的功能来实现的。

目前的Redis自身并不支持散列(Hash)当中的元素淘汰,因此所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理300个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到1小时之间。比如该次清理时删除了300条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍。

1
2
3
4
5
6
7
8
9
10
RMapCache<String, SomeObject> map = redisson.getMapCache("anyMap");
// 有效时间 ttl = 10分钟
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES);
// 有效时间 ttl = 10分钟, 最长闲置时间 maxIdleTime = 10秒钟
map.put("key1", new SomeObject(), 10, TimeUnit.MINUTES, 10, TimeUnit.SECONDS);

// 有效时间 = 3 秒钟
map.putIfAbsent("key2", new SomeObject(), 3, TimeUnit.SECONDS);
// 有效时间 ttl = 40秒钟, 最长闲置时间 maxIdleTime = 10秒钟
map.putIfAbsent("key2", new SomeObject(), 40, TimeUnit.SECONDS, 10, TimeUnit.SECONDS);

本地缓存功能(Local Cache)

在特定的场景下,映射(Map)上的高度频繁的读取操作,使网络通信都被视为瓶颈时,使用Redisson提供的带有本地缓存功能的分布式本地缓存映射RLocalCachedMapJava对象会是一个很好的选择。它同时实现了java.util.concurrent.ConcurrentMap和java.util.Map两个接口。本地缓存功能充分的利用了JVM的自身内存空间,对部分常用的元素实行就地缓存,这样的设计让读取操作的性能较分布式映射相比提高最多 45倍 。以下配置参数可以用来创建这个实例:

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
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
// 用于淘汰清除本地缓存内的元素
// 共有以下几种选择:
// LFU - 统计元素的使用频率,淘汰用得最少(最不常用)的。
// LRU - 按元素使用时间排序比较,淘汰最早(最久远)的。
// SOFT - 元素用Java的WeakReference来保存,缓存元素通过GC过程清除。
// WEAK - 元素用Java的SoftReference来保存, 缓存元素通过GC过程清除。
// NONE - 永不淘汰清除缓存元素。
.evictionPolicy(EvictionPolicy.NONE)
// 如果缓存容量值为0表示不限制本地缓存容量大小
.cacheSize(1000)
// 以下选项适用于断线原因造成了未收到本地缓存更新消息的情况。
// 断线重连的策略有以下几种:
// CLEAR - 如果断线一段时间以后则在重新建立连接以后清空本地缓存
// LOAD - 在服务端保存一份10分钟的作废日志
// 如果10分钟内重新建立连接,则按照作废日志内的记录清空本地缓存的元素
// 如果断线时间超过了这个时间,则将清空本地缓存中所有的内容
// NONE - 默认值。断线重连时不做处理。
.reconnectionStrategy(ReconnectionStrategy.NONE)
// 以下选项适用于不同本地缓存之间相互保持同步的情况
// 缓存同步策略有以下几种:
// INVALIDATE - 默认值。当本地缓存映射的某条元素发生变动时,同时驱逐所有相同本地缓存映射内的该元素
// UPDATE - 当本地缓存映射的某条元素发生变动时,同时更新所有相同本地缓存映射内的该元素
// NONE - 不做任何同步处理
.syncStrategy(SyncStrategy.INVALIDATE)
// 每个Map本地缓存里元素的有效时间,默认毫秒为单位
.timeToLive(10000)
// 或者
.timeToLive(10, TimeUnit.SECONDS)
// 每个Map本地缓存里元素的最长闲置时间,默认毫秒为单位
.maxIdle(10000)
// 或者
.maxIdle(10, TimeUnit.SECONDS);
RLocalCachedMap<String, Integer> map = redisson.getLocalCachedMap("test", options);

String prevObject = map.put("123", 1);
String currentObject = map.putIfAbsent("323", 2);
String obj = map.remove("123");

// 在不需要旧值的情况下可以使用fast为前缀的类似方法
map.fastPut("a", 1);
map.fastPutIfAbsent("d", 32);
map.fastRemove("b");

RFuture<String> putAsyncFuture = map.putAsync("321");
RFuture<Void> fastPutAsyncFuture = map.fastPutAsync("321");

map.fastPutAsync("321", new SomeObject());
map.fastRemoveAsync("321");

当不再使用Map本地缓存对象的时候应该手动销毁,如果Redisson对象被关闭(shutdown)了,则不用手动销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RLocalCachedMap<String, Integer> map = ...
map.destroy();
如何通过加载数据的方式来降低过期淘汰事件发布信息对网络的影响
代码范例:

public void loadData(String cacheName, Map<String, String> data) {
RLocalCachedMap<String, String> clearMap = redisson.getLocalCachedMap(cacheName,
LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.INVALIDATE));
RLocalCachedMap<String, String> loadMap = redisson.getLocalCachedMap(cacheName,
LocalCachedMapOptions.defaults().cacheSize(1).syncStrategy(SyncStrategy.NONE));

loadMap.putAll(data);
clearMap.clearLocalCache();
}

数据分片功能(Sharding)

Map数据分片是Redis集群模式下的一个功能。Redisson提供的分布式集群映射RClusteredMap Java对象也是基于RMap实现的。它同时实现了java.util.concurrent.ConcurrentMap和java.util.Map两个接口。在这里可以获取更多的内部信息。

1
2
3
4
5
6
7
8
RClusteredMap<String, SomeObject> map = redisson.getClusteredMap("anyMap");

SomeObject prevObject = map.put("123", new SomeObject());
SomeObject currentObject = map.putIfAbsent("323", new SomeObject());
SomeObject obj = map.remove("123");

map.fastPut("321", new SomeObject());
map.fastRemove("321");
映射持久化方式(缓存策略)

Redisson供了将映射中的数据持久化到外部储存服务的功能。主要场景有一下几种:

  • 将Redisson的分布式映射类型作为业务和外部储存媒介之间的缓存。
  • 或是用来增加Redisson映射类型中数据的持久性,或是用来增加已被驱逐的数据的寿命。
  • 或是用来缓存数据库,Web服务或其他数据源的数据。
Read-through策略

通俗的讲,如果一个被请求的数据不存在于Redisson的映射中的时候,Redisson将通过预先配置好的MapLoader对象加载数据。

Write-through(数据同步写入)策略

在遇到映射中某条数据被更改时,Redisson会首先通过预先配置好的MapWriter对象写入到外部储存系统,然后再更新Redis内的数据。

Write-behind(数据异步写入)策略

对映射的数据的更改会首先写入到Redis,然后再使用异步的方式,通过MapWriter对象写入到外部储存系统。在并发环境下可以通过writeBehindThreads参数来控制写入线程的数量,已达到对外部储存系统写入并发量的控制。

以上策略适用于所有实现了RMap、RMapCache、RLocalCachedMap和RLocalCachedMapCache接口的对象。

配置范例:

1
2
3
4
5
6
7
8
9
10
11
MapOptions<K, V> options = MapOptions.<K, V>defaults()
.writer(myWriter)
.loader(myLoader);

RMap<K, V> map = redisson.getMap("test", options);
// 或
RMapCache<K, V> map = redisson.getMapCache("test", options);
// 或
RLocalCachedMap<K, V> map = redisson.getLocalCachedMap("test", options);
// 或
RLocalCachedMapCache<K, V> map = redisson.getLocalCachedMapCache("test", options);

映射监听器(Map Listener)

Redisson为所有实现了RMapCache或RLocalCachedMapCache接口的对象提供了监听以下事件的监听器:

  • 事件 | 监听器 元素 添加 事件 | org.redisson.api.map.event.EntryCreatedListener
  • 元素 过期 事件 | org.redisson.api.map.event.EntryExpiredListener
  • 元素 删除 事件 | org.redisson.api.map.event.EntryRemovedListener
  • 元素 更新 事件 | org.redisson.api.map.event.EntryUpdatedListener

使用范例:

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
RMapCache<String, Integer> map = redisson.getMapCache("myMap");
// 或
RLocalCachedMapCache<String, Integer> map = redisson.getLocalCachedMapCache("myMap", options);

int updateListener = map.addListener(new EntryUpdatedListener<Integer, Integer>() {
@Override
public void onUpdated(EntryEvent<Integer, Integer> event) {
event.getKey(); // 字段名
event.getValue() // 新值
event.getOldValue() // 旧值
// ...
}
});

int createListener = map.addListener(new EntryCreatedListener<Integer, Integer>() {
@Override
public void onCreated(EntryEvent<Integer, Integer> event) {
event.getKey(); // 字段名
event.getValue() // 值
// ...
}
});

int expireListener = map.addListener(new EntryExpiredListener<Integer, Integer>() {
@Override
public void onExpired(EntryEvent<Integer, Integer> event) {
event.getKey(); // 字段名
event.getValue() // 值
// ...
}
});

int removeListener = map.addListener(new EntryRemovedListener<Integer, Integer>() {
@Override
public void onRemoved(EntryEvent<Integer, Integer> event) {
event.getKey(); // 字段名
event.getValue() // 值
// ...
}
});

map.removeListener(updateListener);
map.removeListener(createListener);
map.removeListener(expireListener);
map.removeListener(removeListener);

LRU有界映射

Redisson提供了基于Redis的以LRU为驱逐策略的分布式LRU有界映射对象。顾名思义,分布式LRU有界映射允许通过对其中元素按使用时间排序处理的方式,主动移除超过规定容量限制的元素。

1
2
3
4
5
6
7
8
9
RMapCache<String, String> map = redisson.getMapCache("map");
// 尝试将该映射的最大容量限制设定为10
map.trySetMaxSize(10);

// 将该映射的最大容量限制设定或更改为10
map.setMaxSize(10);

map.put("1", "2");
map.put("3", "3", 1, TimeUnit.SECONDS);

2、多值映射(Multimap)

基于Redis的Redisson的分布式RMultimap Java对象允许Map中的一个字段值包含多个元素。 字段总数受Redis限制,每个Multimap最多允许有4 294 967 295个不同字段。

基于集(Set)的多值映射(Multimap)

基于Set的Multimap不允许一个字段值包含有重复的元素。

RSetMultimap<SimpleKey, SimpleValue> map = redisson.getSetMultimap(“myMultimap”);
map.put(new SimpleKey(“0”), new SimpleValue(“1”));
map.put(new SimpleKey(“0”), new SimpleValue(“2”));
map.put(new SimpleKey(“3”), new SimpleValue(“4”));

Set allValues = map.get(new SimpleKey(“0”));

List newValues = Arrays.asList(new SimpleValue(“7”), new SimpleValue(“6”), new SimpleValue(“5”));
Set oldValues = map.replaceValues(new SimpleKey(“0”), newValues);

Set removedValues = map.removeAll(new SimpleKey(“0”));

基于列表(List)的多值映射(Multimap)

基于List的Multimap在保持插入顺序的同时允许一个字段下包含重复的元素。

RListMultimap<SimpleKey, SimpleValue> map = redisson.getListMultimap(“test1”);
map.put(new SimpleKey(“0”), new SimpleValue(“1”));
map.put(new SimpleKey(“0”), new SimpleValue(“2”));
map.put(new SimpleKey(“0”), new SimpleValue(“1”));
map.put(new SimpleKey(“3”), new SimpleValue(“4”));

List allValues = map.get(new SimpleKey(“0”));

Collection newValues = Arrays.asList(new SimpleValue(“7”), new SimpleValue(“6”), new SimpleValue(“5”));
List oldValues = map.replaceValues(new SimpleKey(“0”), newValues);

List removedValues = map.removeAll(new SimpleKey(“0”));

多值映射(Multimap)淘汰机制(Eviction)

Multimap对象的淘汰机制是通过不同的接口来实现的。它们是RSetMultimapCache接口和RListMultimapCache接口,分别对应的是Set和List的Multimaps。

所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理100个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到2小时之间。比如该次清理时删除了100条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍。

RSetMultimapCache的使用范例:

1
2
3
4
5
6
7
8
9
RSetMultimapCache<String, String> multimap = redisson.getSetMultimapCache("myMultimap");
multimap.put("1", "a");
multimap.put("1", "b");
multimap.put("1", "c");

multimap.put("2", "e");
multimap.put("2", "f");

multimap.expireKey("2", 10, TimeUnit.MINUTES);

3、集(Set)

基于Redis的Redisson的分布式Set结构的RSet Java对象实现了java.util.Set接口。通过元素的相互状态比较保证了每个元素的唯一性。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
RSet<SomeObject> set = redisson.getSet("anySet");
set.add(new SomeObject());
set.remove(new SomeObject());
Redisson PRO版本中的Set对象还可以在Redis集群环境下支持单集合数据分片。

集(Set)淘汰机制(Eviction)

基于Redis的Redisson的分布式RSetCache Java对象在基于RSet的前提下实现了针对单个元素的淘汰机制。由于RSetCache是基于RSet实现的,使它还集成了java.util.Set接口。

目前的Redis自身并不支持Set当中的元素淘汰,因此所有过期元素都是通过org.redisson.EvictionScheduler实例来实现定期清理的。为了保证资源的有效利用,每次运行最多清理100个过期元素。任务的启动时间将根据上次实际清理数量自动调整,间隔时间趋于1秒到2小时之间。比如该次清理时删除了100条元素,那么下次执行清理的时间将在1秒以后(最小间隔时间)。一旦该次清理数量少于上次清理数量,时间间隔将增加1.5倍。

1
2
3
RSetCache<SomeObject> set = redisson.getSetCache("anySet");
// ttl = 10 seconds
set.add(new SomeObject(), 10, TimeUnit.SECONDS);

集(Set)数据分片(Sharding)

Set数据分片是Redis集群模式下的一个功能。Redisson提供的分布式RClusteredSet Java对象也是基于RSet实现的。在这里可以获取更多的信息。

1
2
3
RClusteredSet<SomeObject> set = redisson.getClusteredSet("anySet");
set.add(new SomeObject());
set.remove(new SomeObject());

除了RClusteredSet以外,Redisson还提供了另一种集群模式下的分布式集(Set),它不仅提供了透明的数据分片功能,还为每个元素提供了淘汰机制。RClusteredSetCache 类分别同时提供了RClusteredSet 和RSetCache 这两个接口的实现。当然这些都是基于java.util.Set的接口实现上的。

该功能仅限于Redisson PRO版本。

4、有序集(SortedSet)

基于Redis的Redisson的分布式RSortedSet Java对象实现了java.util.SortedSet接口。在保证元素唯一性的前提下,通过比较器(Comparator)接口实现了对元素的排序。

1
2
3
4
5
6
7
8
RSortedSet<Integer> set = redisson.getSortedSet("anySet");
set.trySetComparator(new MyComparator()); // 配置元素比较器
set.add(3);
set.add(1);
set.add(2);

set.removeAsync(0);
set.addAsync(5);

5、计分排序集(ScoredSortedSet)

基于Redis的Redisson的分布式RScoredSortedSet Java对象是一个可以按插入时指定的元素评分排序的集合。它同时还保证了元素的唯一性。

1
2
3
4
5
6
7
8
9
10
11
RScoredSortedSet<SomeObject> set = redisson.getScoredSortedSet("simple");

set.add(0.13, new SomeObject(a, b));
set.addAsync(0.251, new SomeObject(c, d));
set.add(0.302, new SomeObject(g, d));

set.pollFirst();
set.pollLast();

int index = set.rank(new SomeObject(g, d)); // 获取元素在集合中的位置
Double score = set.getScore(new SomeObject(g, d)); // 获取元素的评分

6、字典排序集(LexSortedSet)

基于Redis的Redisson的分布式RLexSortedSet Java对象在实现了java.util.Set接口的同时,将其中的所有字符串元素按照字典顺序排列。它公式还保证了字符串元素的唯一性。

1
2
3
4
5
6
7
8
RLexSortedSet set = redisson.getLexSortedSet("simple");
set.add("d");
set.addAsync("e");
set.add("f");

set.lexRangeTail("d", false);
set.lexCountHead("e");
set.lexRange("d", true, "z", false);

7、列表(List)

基于Redis的Redisson分布式列表(List)结构的RList Java对象在实现了java.util.List接口的同时,确保了元素插入时的顺序。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
RList<SomeObject> list = redisson.getList("anyList");
list.add(new SomeObject());
list.get(0);
list.remove(new SomeObject());

8、队列(Queue)

基于Redis的Redisson分布式无界队列(Queue)结构的RQueue Java对象实现了java.util.Queue接口。尽管RQueue对象无初始大小(边界)限制,但对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
RQueue<SomeObject> queue = redisson.getQueue("anyQueue");
queue.add(new SomeObject());
SomeObject obj = queue.peek();
SomeObject someObj = queue.poll();

9、双端队列(Deque)

基于Redis的Redisson分布式无界双端队列(Deque)结构的RDeque Java对象实现了java.util.Deque接口。尽管RDeque对象无初始大小(边界)限制,但对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
5
RDeque<SomeObject> queue = redisson.getDeque("anyDeque");
queue.addFirst(new SomeObject());
queue.addLast(new SomeObject());
SomeObject obj = queue.removeFirst();
SomeObject someObj = queue.removeLast();

10、阻塞队列(Blocking Queue)

基于Redis的Redisson分布式无界阻塞队列(Blocking Queue)结构的RBlockingQueue Java对象实现了java.util.concurrent.BlockingQueue接口。尽管RBlockingQueue对象无初始大小(边界)限制,但对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
5
6
RBlockingQueue<SomeObject> queue = redisson.getBlockingQueue("anyQueue");
queue.offer(new SomeObject());

SomeObject obj = queue.peek();
SomeObject someObj = queue.poll();
SomeObject ob = queue.poll(10, TimeUnit.MINUTES);

poll, pollFromAny, pollLastAndOfferFirstTo和take方法内部采用话题订阅发布实现,在Redis节点故障转移(主从切换)或断线重连以后,内置的相关话题监听器将自动完成话题的重新订阅。

11、有界阻塞队列(Bounded Blocking Queue)

基于Redis的Redisson分布式有界阻塞队列(Bounded Blocking Queue)结构的RBoundedBlockingQueue Java对象实现了java.util.concurrent.BlockingQueue接口。该对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。队列的初始容量(边界)必须在使用前设定好。

1
2
3
4
5
6
7
8
9
10
11
12
13
RBoundedBlockingQueue<SomeObject> queue = redisson.getBoundedBlockingQueue("anyQueue");
// 如果初始容量(边界)设定成功则返回`真(true)`,
// 如果初始容量(边界)已近存在则返回`假(false)`。
queue.trySetCapacity(2);

queue.offer(new SomeObject(1));
queue.offer(new SomeObject(2));
// 此时容量已满,下面代码将会被阻塞,直到有空闲为止。
queue.put(new SomeObject());

SomeObject obj = queue.peek();
SomeObject someObj = queue.poll();
SomeObject ob = queue.poll(10, TimeUnit.MINUTES);

poll, pollFromAny, pollLastAndOfferFirstTo和take方法内部采用话题订阅发布实现,在Redis节点故障转移(主从切换)或断线重连以后,内置的相关话题监听器将自动完成话题的重新订阅。

12. 阻塞双端队列(Blocking Deque)

基于Redis的Redisson分布式无界阻塞双端队列(Blocking Deque)结构的RBlockingDeque Java对象实现了java.util.concurrent.BlockingDeque接口。尽管RBlockingDeque对象无初始大小(边界)限制,但对象的最大容量受Redis限制,最大元素数量是4 294 967 295个。

1
2
3
4
5
6
7
RBlockingDeque<Integer> deque = redisson.getBlockingDeque("anyDeque");
deque.putFirst(1);
deque.putLast(2);
Integer firstValue = queue.takeFirst();
Integer lastValue = queue.takeLast();
Integer firstValue = queue.pollFirst(10, TimeUnit.MINUTES);
Integer lastValue = queue.pollLast(3, TimeUnit.MINUTES);

poll, pollFromAny, pollLastAndOfferFirstTo和take方法内部采用话题订阅发布实现,在Redis节点故障转移(主从切换)或断线重连以后,内置的相关话题监听器将自动完成话题的重新订阅。

13. 阻塞公平队列(Blocking Fair Queue)

基于Redis的Redisson分布式无界阻塞公平队列(Blocking Fair Queue)结构的RBlockingFairQueue Java对象在实现Redisson分布式无界阻塞队列(Blocking Queue)结构RBlockingQueue接口的基础上,解决了多个队列消息的处理者在复杂的网络环境下,网络延时的影响使“较远”的客户端最终收到消息数量低于“较近”的客户端的问题。从而解决了这种现象引发的个别处理节点过载的情况。

以分布式无界阻塞队列为基础,采用公平获取消息的机制,不仅保证了poll、pollFromAny、pollLastAndOfferFirstTo和take方法获取消息的先入顺序,还能让队列里的消息被均匀的发布到处在复杂分布式环境中的各个处理节点里。

1
2
3
4
5
6
RBlockingFairQueue queue = redisson.getBlockingFairQueue("myQueue");
queue.offer(new SomeObject());

SomeObject obj = queue.peek();
SomeObject someObj = queue.poll();
SomeObject ob = queue.poll(10, TimeUnit.MINUTES);

该功能仅限于Redisson PRO版本。

14. 阻塞公平双端队列(Blocking Fair Deque)

基于Redis的Redisson分布式无界阻塞公平双端队列(Blocking Fair Deque)结构的RBlockingFairDeque Java对象在实现Redisson分布式无界阻塞双端队列(Blocking Deque)结构RBlockingDeque接口的基础上,解决了多个队列消息的处理者在复杂的网络环境下,网络延时的影响使“较远”的客户端最终收到消息数量低于“较近”的客户端的问题。从而解决了这种现象引发的个别处理节点过载的情况。

以分布式无界阻塞双端队列为基础,采用公平获取消息的机制,不仅保证了poll、take、pollFirst、takeFirst、pollLast和takeLast方法获取消息的先入顺序,还能让队列里的消息被均匀的发布到处在复杂分布式环境中的各个处理节点里。

1
2
3
4
5
6
7
8
9
10
11
12
RBlockingFairDeque deque = redisson.getBlockingFairDeque("myDeque");
deque.offer(new SomeObject());

SomeObject firstElement = queue.peekFirst();
SomeObject firstElement = queue.pollFirst();
SomeObject firstElement = queue.pollFirst(10, TimeUnit.MINUTES);
SomeObject firstElement = queue.takeFirst();

SomeObject lastElement = queue.peekLast();
SomeObject lastElement = queue.pollLast();
SomeObject lastElement = queue.pollLast(10, TimeUnit.MINUTES);
SomeObject lastElement = queue.takeLast();

该功能仅限于Redisson PRO版本。

15. 延迟队列(Delayed Queue)

基于Redis的Redisson分布式延迟队列(Delayed Queue)结构的RDelayedQueue Java对象在实现了RQueue接口的基础上提供了向队列按要求延迟添加项目的功能。该功能可以用来实现消息传送延迟按几何增长或几何衰减的发送策略。

1
2
3
4
5
6
7
8
9
10
RQueue<String> distinationQueue = ...
RDelayedQueue<String> delayedQueue = getDelayedQueue(distinationQueue);
// 10秒钟以后将消息发送到指定队列
delayedQueue.offer("msg1", 10, TimeUnit.SECONDS);
// 一分钟以后将消息发送到指定队列
delayedQueue.offer("msg2", 1, TimeUnit.MINUTES);
在该对象不再需要的情况下,应该主动销毁。仅在相关的Redisson对象也需要关闭的时候可以不用主动销毁。

RDelayedQueue<String> delayedQueue = ...
delayedQueue.destroy();

16. 优先队列(Priority Queue)

基于Redis的Redisson分布式优先队列(Priority Queue)Java对象实现了java.util.Queue的接口。可以通过比较器(Comparator)接口来对元素排序。

1
2
3
4
5
6
7
8
9
10
RPriorityQueue<Integer> queue = redisson.getPriorityQueue("anyQueue");
queue.trySetComparator(new MyComparator()); // 指定对象比较器
queue.add(3);
queue.add(1);
queue.add(2);

queue.removeAsync(0);
queue.addAsync(5);

queue.poll();

17. 优先双端队列(Priority Deque)

基于Redis的Redisson分布式优先双端队列(Priority Deque)Java对象实现了java.util.Deque的接口。可以通过比较器(Comparator)接口来对元素排序。

1
2
3
4
5
6
7
8
9
10
11
RPriorityDeque<Integer> queue = redisson.getPriorityDeque("anyQueue");
queue.trySetComparator(new MyComparator()); // 指定对象比较器
queue.addLast(3);
queue.addFirst(1);
queue.add(2);

queue.removeAsync(0);
queue.addAsync(5);

queue.pollFirst();
queue.pollLast();

18. 优先阻塞队列(Priority Blocking Queue)

基于Redis的分布式无界优先阻塞队列(Priority Blocking Queue)Java对象的结构与java.util.concurrent.PriorityBlockingQueue类似。可以通过比较器(Comparator)接口来对元素排序。PriorityBlockingQueue的最大容量是4 294 967 295个元素。

1
2
3
4
5
6
7
8
9
10
RPriorityBlockingQueue<Integer> queue = redisson.getPriorityBlockingQueue("anyQueue");
queue.trySetComparator(new MyComparator()); // 指定对象比较器
queue.add(3);
queue.add(1);
queue.add(2);

queue.removeAsync(0);
queue.addAsync(5);

queue.take();

当Redis服务端断线重连以后,或Redis服务端发生主从切换,并重新建立连接后,断线时正在使用poll,pollLastAndOfferFirstTo或take方法的对象Redisson将自动再次为其订阅相关的话题。

19. 优先阻塞双端队列(Priority Blocking Deque)

基于Redis的分布式无界优先阻塞双端队列(Priority Blocking Deque)Java对象实现了java.util.Deque的接口。addLast、 addFirst、push方法不能再这个对里使用。PriorityBlockingDeque的最大容量是4 294 967 295个元素。

1
2
3
4
5
6
7
8
9
10
11
RPriorityBlockingDeque<Integer> queue = redisson.getPriorityBlockingDeque("anyQueue");
queue.trySetComparator(new MyComparator()); // 指定对象比较器
queue.add(2);

queue.removeAsync(0);
queue.addAsync(5);

queue.pollFirst();
queue.pollLast();
queue.takeFirst();
queue.takeLast();

当Redis服务端断线重连以后,或Redis服务端发生主从切换,并重新建立连接后,断线时正在使用poll,pollLastAndOfferFirstTo或take方法的对象Redisson将自动再次为其订阅相关的话题。

六、分布式锁和同步器

1. 可重入锁(Reentrant Lock)

基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
lock.lock();

大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}

Redisson同时还为分布式锁提供了异步执行的相关方法:

1
2
3
4
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);

RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误。但是如果遇到需要其他进程也能解锁的情况,请使用分布式信号量Semaphore 对象.

2. 公平锁(Fair Lock)

基于Redis的Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。

1
2
3
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
...
fairLock.unlock();
Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:

RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);

3. 联锁(MultiLock)

基于Redis的Redisson分布式联锁RedissonMultiLock对象可以将多个RLock对象关联为一个联锁,每个RLock对象实例可以来自于不同的Redisson实例。

1
2
3
4
5
6
7
8
9
10
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

1
2
3
4
5
6
7
8
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

4. 红锁(RedLock)

基于Redis的Redisson红锁RedissonRedLock对象实现了Redlock介绍的加锁算法。该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。

1
2
3
4
5
6
7
8
9
10
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
1
2
3
4
5
6
7
8
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 给lock1,lock2,lock3加锁,如果没有手动解开的话,10秒钟后将会自动解开
lock.lock(10, TimeUnit.SECONDS);

// 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

5. 读写锁(ReadWriteLock)

基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。

分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。

1
2
3
4
5
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();

另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。

1
2
3
4
5
6
7
8
9
10
11
12
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);

// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
...
lock.unlock();

6. 信号量(Semaphore)

基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();

7. 可过期性信号量(PermitExpirableSemaphore)

基于Redis的Redisson可过期性信号量(PermitExpirableSemaphore)是在RSemaphore对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID来辨识,释放时只能通过提交这个ID才能释放。它提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。

1
2
3
4
5
6
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);

8. 闭锁(CountDownLatch)

基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。

1
2
3
4
5
6
7
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();

// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();

七、分布式服务

1. 分布式远程服务(Remote Service)

基于Redis的Java分布式远程服务,可以用来通过共享接口执行存在于另一个Redisson实例里的对象方法。换句话说就是通过Redis实现了Java的远程过程调用(RPC)。分布式远程服务基于可以用POJO对象,方法的参数和返回类不受限制,可以是任何类型。

分布式远程服务(Remote Service)提供了两种类型的RRemoteService实例:

服务端(远端)实例 - 用来执行远程方法(工作者实例即worker instance). 例如:

1
2
3
4
5
6
7
8
9
RRemoteService remoteService = redisson.getRemoteService();
SomeServiceImpl someServiceImpl = new SomeServiceImpl();

// 在调用远程方法以前,应该首先注册远程服务
// 只注册了一个服务端工作者实例,只能同时执行一个并发调用
remoteService.register(SomeServiceInterface.class, someServiceImpl);

// 注册了12个服务端工作者实例,可以同时执行12个并发调用
remoteService.register(SomeServiceInterface.class, someServiceImpl, 12);

客户端(本地)实例 - 用来请求远程方法. 例如:

1
2
3
4
RRemoteService remoteService = redisson.getRemoteService();
SomeServiceInterface service = remoteService.get(SomeServiceInterface.class);

String result = service.doSomeStuff(1L, "secondParam", new AnyParam());

客户端和服务端必须使用一样的共享接口,生成两者的Redisson实例必须采用相同的连接配置。客户端和服务端实例可以运行在同一个JVM里,也可以是不同的。客户端和服务端的数量不收限制。(注意:尽管Redisson不做任何限制,但是Redis的限制仍然有效。)

在服务端工作者可用实例数量 大于1 的时候,将并行执行并发调用的远程方法。

并行执行工作者数量计算方法如下: T = R * N

T - 并行执行工作者总数 R - Redisson服务端数量 N - 注册服务端时指定的执行工作者数量

超过该数量的并发请求将在列队中等候执行。

在服务端工作者实例可用数量为 1 时,远程过程调用将会按 顺序执行。这种情况下,每次只有一个请求将会被执行,其他请求将在列队中等候执行。

分布式远程服务工作流程

分布式远程服务为每个注册接口建立了两个列队。一个列队用于请求,由服务端监听,另一个列队用于应答回执和结果回复,由客户端监听。应答回执用于判定该请求是否已经被接受。如果在指定的超时时间内没有被执行工作者执行将会抛出RemoteServiceAckTimeoutException错误。

下图描述了每次发起远程过程调用请求的工作流程。

20200423150409

发送即不管(Fire-and-Forget)模式和应答回执(Ack-Response)模式

分布式远程服务通过org.redisson.core.RemoteInvocationOptions类,为每个远程过程调用提供了一些可配置选项。这些选项可以用来指定和修改请求超时和选择跳过应答回执或结果的发送模式。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 应答回执超时1秒钟,远程执行超时30秒钟
RemoteInvocationOptions options = RemoteInvocationOptions.defaults();

// 无需应答回执,远程执行超时30秒钟
RemoteInvocationOptions options = RemoteInvocationOptions.defaults().noAck();

// 应答回执超时1秒钟,不等待执行结果
RemoteInvocationOptions options = RemoteInvocationOptions.defaults().noResult();

// 应答回执超时1分钟,不等待执行结果
RemoteInvocationOptions options = RemoteInvocationOptions.defaults().expectAckWithin(1, TimeUnit.MINUTES).noResult();

// 发送即不管(Fire-and-Forget)模式,无需应答回执,不等待结果
RemoteInvocationOptions options = RemoteInvocationOptions.defaults().noAck().noResult();

RRemoteService remoteService = redisson.getRemoteService();
YourService service = remoteService.get(YourService.class, options);

异步调用

远程过程调用也可以采用异步的方式执行。异步调用需要单独提交一个带有@RRemoteAsync注解(annotation)的异步接口类。异步接口方法签名必须与远程接口的方法签名相符。异步接口的返回类必须是org.redisson.api.RFuture对象或其子对象。在调用RRemoteService.get方法时将对异步接口的方法进行验证。异步接口无须包含所有的远程接口里的方法,只需要包含要求异步执行的方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 远程接口
public interface RemoteInterface {

Long someMethod1(Long param1, String param2);

void someMethod2(MyObject param);

MyObject someMethod3();

}

// 匹配远程接口的异步接口
@RRemoteAsync(RemoteInterface.class)
public interface RemoteInterfaceAsync {

RFuture<Long> someMethod1(Long param1, String param2);

RFuture<Void> someMethod2(MyObject param);

}

RRemoteService remoteService = redisson.getRemoteService();
RemoteInterfaceAsync asyncService = remoteService.get(RemoteInterfaceAsync.class);

** 取消异步调用 **
通过调用Future.cancel()方法可以非常方便的取消一个异步调用。分布式远程服务允许在三个阶段中任何一个阶段取消异步调用:

  • 远程调用请求在列队中排队阶段
  • 远程调用请求已经被分布式远程服务接受,还未发送应答回执,执行尚未开始。
  • 远程调用请求已经在执行阶段
  • 想要正确的处理第三个阶段,在服务端代码里应该检查Thread.currentThread().isInterrupted()的返回状态。范例如下:
    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
    // 远程接口
    public interface MyRemoteInterface {

    Long myBusyMethod(Long param1, String param2);

    }

    // 匹配远程接口的异步接口
    @RRemoteAsync(MyRemoteInterface.class)
    public interface MyRemoteInterfaceAsync {

    RFuture<Long> myBusyMethod(Long param1, String param2);

    }

    // 远程接口的实现
    public class MyRemoteServiceImpl implements MyRemoteInterface {

    public Long myBusyMethod(Long param1, String param2) {
    for (long i = 0; i < Long.MAX_VALUE; i++) {
    iterations.incrementAndGet();
    if (Thread.currentThread().isInterrupted()) {
    System.out.println("interrupted! " + i);
    return;
    }
    }
    }

    }

    RRemoteService remoteService = redisson.getRemoteService();
    ExecutorService executor = Executors.newFixedThreadPool(5);
    // 注册远程服务的服务端的同时,通过单独指定的ExecutorService来配置执行线程池
    MyRemoteInterface serviceImpl = new MyRemoteServiceImpl();
    remoteService.register(MyRemoteInterface.class, serviceImpl, 5, executor);

    // 异步调用方法
    MyRemoteInterfaceAsync asyncService = remoteService.get(MyRemoteInterfaceAsync.class);
    RFuture<Long> future = asyncService.myBusyMethod(1L, "someparam");
    // 取消异步调用
    future.cancel(true);

    2、分布式实时对象(Live Object)服务

    介绍

    分布式实时对象(Live Object) 可以被理解为一个功能强化后的Java对象。该对象不仅可以被一个JVM里的各个线程相引用,还可以被多个位于不同JVM里的线程同时引用。

Redisson分布式实时对象(Redisson Live Object,简称RLO)运用即时生成的代理类(Proxy),将一个指定的普通Java类里的所有字段,以及针对这些字段的操作全部映射到一个Redis Hash的数据结构,实现这种理念。每个字段的get和set方法最终被转译为针对同一个Redis Hash的hget和hset命令,从而使所有连接到同一个Redis节点的所有可以客户端同时对一个指定的对象进行操作。众所周知,一个对象的状态是由其内部的字段所赋的值来体现的,通过将这些值保存在一个像Redis这样的远程共享的空间的过程,把这个对象强化成了一个分布式对象。这个分布式对象就叫做Redisson分布式实时对象(Redisson Live Object,简称RLO)。

通过使用RLO,运行在不同服务器里的多个程序之间,共享一个对象实例变得和在单机程序里共享一个对象实例一样了。同时还避免了针对任何一个字段操作都需要将整个对象序列化和反序列化的繁琐,进而降低了程序开发的复杂性和其数据模型的复杂性:从任何一个客户端修改一个字段的值,处在其他服务器上的客户端即刻便能查看到。而且实现代码与单机程序代码无异。(连接到从节点的客户端仍然受Redis的最终一致性的特性限制)

鉴于Redis是一个单线程的程序,针对实时对象的所有的字段操作可以理解为全部是原子性操作,也就是说在读取一个字段的过程不会担心被其他线程所修改。

通过使用RLO,可以把Redis当作一个允许被多个JVM同时操作且不受GC影响的共享堆(Heap Space)。

使用方法

Redisson为分布式实时对象提供了一系列不同功能的注解,其中@REntity和@RId两个注解是分布式实时对象的必要条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@REntity
public class MyObject {

@RId
private String id;
@RIndex
private String value;
private MyObject parent;

public MyObject(String id) {
this.id = id;
}

public MyObject() {
}

// getters and setters

}

在开始使用分布式实时对象以前,需要先通过Redisson服务将指定的对象连接(attach),合并(merge)或持久化(persist)到Redis里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RLiveObjectService service = redisson.getLiveObjectService();
MyLiveObject myObject = new MyLiveObject();
myObject.setId("1");
// 将myObject对象当前的状态持久化到Redis里并与之保持同步。
myObject = service.persist(myObject);

MyLiveObject myObject = new MyLiveObject("1");
// 抛弃myObject对象当前的状态,并与Redis里的数据建立连接并保持同步。
myObject = service.attach(myObject);

MyLiveObject myObject = new MyLiveObject();
myObject.setId("1");
// 将myObject对象当前的状态与Redis里的数据合并之后与之保持同步。
myObject = service.merge(myObject);
myObject.setValue("somevalue");

// 通过ID获取分布式实时对象
MyLiveObject myObject = service.get(MyLiveObject.class, "1");

// 通过索引查找分布式实时对象
Collection<MyLiveObject> myObjects = service.find(MyLiveObject.class, Conditions.in("value", "somevalue", "somevalue2"));

Collection<MyLiveObject> myObjects = service.find(MyLiveObject.class, Conditions.and(Conditions.in("value", "somevalue", "somevalue2"), Conditions.eq("secondfield", "test")));

“parent”字段中包含了指向到另一个分布式实时对象的引用,它可以与包含类是同一类型也可以不同。Redisson内部采用了与Java的引用类似的方式保存这个关系,而非将全部对象序列化,可视为与普通的引用同等效果。

1
2
3
4
//RLO对象:
MyObject myObject = service.get(MyObject.class, "1");
MyObject myParentObject = service.get(MyObject.class, "2");
myObject.setValue(myParentObject);

RLO的字段类型基本上无限制,可以是任何类型。比如Java util包里的集合类,Map类等,也可以是自定义的对象。只要指定的编码解码器能够对其进行编码和解码操作便可。

尽管RLO的字段类型基本上无限制,个别类型还是受限。注解了RId的字段类型不能是数组类(Array),比如int[],long[],double[],byte[]等等。

为了保证RLO的用法和普通Java对象的用法尽可能一直,Redisson分布式实时对象服务自动将以下普通Java对象转换成与之匹配的Redisson分布式对象RObject。

1
2
3
4
5
6
7
8
9
10
普通Java类	转换后的Redisson类
SortedSet.class RedissonSortedSet.class
Set.class RedissonSet.class
ConcurrentMap.class RedissonMap.class
Map.class RedissonMap.class
BlockingDeque.class RedissonBlockingDeque.class
Deque.class RedissonDeque.class
BlockingQueue.class RedissonBlockingQueue.class
Queue.class RedissonQueue.class
List.class RedissonList.class

类型转换将按照从上至下的顺序匹配类型,例如LinkedList类同时实现了Deque,List和Queue,由于Deque排在靠上的位置,因此它将会被转换成一个RedissonDeque类型。

Redisson的分布式对象也采用类似的方式,将自身的状态储存于Redis当中,(几乎^)所有的状态改变都直接映射到Redis里,不在本地JVM中保留任何赋值。(^本地缓存对象除外,比如RLocalCachedMap)

高级使用方法

正如上述介绍,RLO类其实都是按需实时生成的代理(Proxy)类。生成的代理类和原类都一同缓存Redisson实例里。这个过程会消耗一些时间,在对耗时比较敏感的情况下,建议通过RedissonLiveObjectService提前注册所有的RLO类。这个服务也可以用来注销不再需要的RLO类,也可以用来查询一个类是否已经注册了。

1
2
3
4
RLiveObjectService service = redisson.getLiveObjectService();
service.registerClass(MyClass.class);
service.unregisterClass(MyClass.class);
Boolean registered = service.isClassRegistered(MyClass.class);

注解(Annotation)使用方法

@REntity

仅适用于类。通过指定@REntity的各个参数,可以详细的对每个RLO类实现特殊定制,以达到改变RLO对象的行为。

1
2
3
namingScheme - 命名方案。命名方案规定了每个实例在Redis中对应key的名称。它不仅被用来与已存在的RLO建立关联,还被用来储存新建的RLO实例。默认采用Redisson自带的DefaultNamingScheme对象。
codec - 编码解码器。在运行当中,Redisson用编码解码器来对RLO中的每个字段进行编码解码。Redisson内部采用了实例池管理不同类型的编码解码器实例。Redisson提供了多种不同的编码解码器,默认使用JsonJacksonCodec。
fieldTransformation - 字段转换模式。如上所述,为了尽可能的保证RLO的用法和普通Java对象一致,Redisson会自动将常用的普通Java对象转换成与其匹配的Redisson分布式对象。这是由于字段转换模式的默认值是ANNOTATION_BASED,修改为IMPLEMENTATION_BASED就可以不转换。

@RId

仅适用于字段。@RId注解只能用在具备区分实例的字段上,这类字段可以理解为一个类的id字段或主键字段。这个字段的值将被命名方案namingScheme用来与事先存在的RLO建立引用。加了该注解的字段是唯一在本地JVM里同时保存赋值的字段。一个类只能有一个字段包含@RId注解。

可以通过指定一个生成器generator策略来实现自动生成这个字段的值。默认不提供生成器。

@RIndex

仅适用于字段。用来指定可用于搜索的字段。可以通过RLiveObjectService.find方法来根据条件精细查找分布式实时对象。查询条件可以是含(IN),或(OR),和(AND)或相等(EQ)以及它们的任意组合。

使用范例如下:

1
2
3
4
5
6
7
8
9
public class MyObject {
@RIndex
String field1;
@RIndex
String field2;
@RIndex
String field3;
}
Collection<MyObject> objects = RLiveObjectService.find(MyObject.class, Conditions.or(Conditions.and(Conditions.eq("field1", "value"), Conditions.eq("field2", "value")), Conditions.in("field3", "value1", "value2"));

@RObjectField

仅适用于字段。允许通过该注解中的namingScheme或codec来改变该字段的命名或编码方式,用来区别于@REntity中指定的预设方式。

@RCascade

仅适用于字段。用来指定包含于分布式实时对象字段内其它对象的级联操作方式。

可选的级联操作方式为如下:

1
2
RCascadeType.ALL - 执行所有级联操作
RCascadeType.PERSIST - 仅在执行RLiveObjectService.persist()方法时进行级联操作 RCascadeType.DETACH - 仅在执行RLiveObjectService.detach()方法时进行级联操作 RCascadeType.MERGE - 仅在执行RLiveObjectService.merge()方法时进行级联操作 RCascadeType.DELETE - 仅在执行RLiveObjectService.delete()方法时进行级联操作

使用限制

如上所述,带有RId注解字段的类型不能使数组类,这是因为目前默认的命名方案类DefaultNamingScheme还不能正确地将数组类序列化和反序列化。在改善了DefaultNamingScheme类的不足以后会考虑取消这个限制。另外由于带有RId注解的字段是用来指定Redis中映射的key的名称,因此组建一个只含有唯一一个字段的RLO类是毫无意义的。选用RBucket会更适合这样的场景。

3、分布式执行服务(Executor Service)

概述

Redisson的分布式执行服务实现了java.util.concurrent.ExecutorService接口,支持在不同的独立节点里执行基于java.util.concurrent.Callable接口或java.lang.Runnable接口或Lambda的任务。这样的任务也可以通过使用Redisson实例,实现对储存在Redis里的数据进行操作。Redisson分布式执行服务是最快速和有效执行分布式运算的方法。

任务

Redisson独立节点不要求任务的类在类路径里。他们会自动被Redisson独立节点的ClassLoader加载。因此每次执行一个新任务时,不需要重启Redisson独立节点。

采用Callable任务的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CallableTask implements Callable<Long> {

@RInject
private RedissonClient redissonClient;

@Override
public Long call() throws Exception {
RMap<String, Integer> map = redissonClient.getMap("myMap");
Long result = 0;
for (Integer value : map.values()) {
result += value;
}
return result;
}

}

采用Runnable任务的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RunnableTask implements Runnable {

@RInject
private RedissonClient redissonClient;

private long param;

public RunnableTask() {
}

public RunnableTask(long param) {
this.param = param;
}

@Override
public void run() {
RAtomicLong atomic = redissonClient.getAtomicLong("myAtomic");
atomic.addAndGet(param);
}

}

在创建ExecutorService时可以配置以下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ExecutorOptions options = ExecutorOptions.defaults()

// 指定重新尝试执行任务的时间间隔。
// ExecutorService的工作节点将等待10分钟后重新尝试执行任务
//
// 设定为0则不进行重试
//
// 默认值为5分钟
options.taskRetryInterval(10, TimeUnit.MINUTES);
RExecutorService executorService = redisson.getExecutorService("myExecutor", options);
executorService.submit(new RunnableTask(123));

RExecutorService executorService = redisson.getExecutorService("myExecutor", options);
Future<Long> future = executorService.submit(new CallableTask());
Long result = future.get();

使用Lambda任务的范例:

1
2
3
4
5
RExecutorService executorService = redisson.getExecutorService("myExecutor", options);
Future<Long> future = executorService.submit((Callable & Serializable)() -> {
System.out.println("task has been executed!");
});
Long result = future.get();

可以通过@RInject注解来为任务实时注入Redisson实例依赖。

取消任务

通过Future.cancel()方法可以很方便的取消所有已提交的任务。通过对Thread.currentThread().isInterrupted()方法的调用可以在已经处于运行状态的任务里实现任务中断:

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
public class CallableTask implements Callable<Long> {

@RInject
private RedissonClient redissonClient;

@Override
public Long call() throws Exception {
RMap<String, Integer> map = redissonClient.getMap("myMap");
Long result = 0;
// map里包含了许多的元素
for (Integer value : map.values()) {
if (Thread.currentThread().isInterrupted()) {
// 任务被取消了
return null;
}
result += value;
}
return result;
}

}

RExecutorService executorService = redisson.getExecutorService("myExecutor");
Future<Long> future = executorService.submit(new CallableTask());
// 或
RFuture<Long> future = executorService.submitAsync(new CallableTask());
// ...
future.cancel(true);

4、分布式调度任务服务(Scheduler Service)

分布式调度任务服务概述

Redisson的分布式调度任务服务实现了java.util.concurrent.ScheduledExecutorService接口,支持在不同的独立节点里执行基于java.util.concurrent.Callable接口或java.lang.Runnable接口的任务。Redisson独立节点按顺序运行Redis列队里的任务。调度任务是一种需要在未来某个指定时间运行一次或多次的特殊任务。

设定任务计划

Redisson独立节点不要求任务的类在类路径里。他们会自动被Redisson独立节点的ClassLoader加载。因此每次执行一个新任务时,不需要重启Redisson独立节点。

采用Callable任务的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CallableTask implements Callable<Long> {

@RInject
private RedissonClient redissonClient;

@Override
public Long call() throws Exception {
RMap<String, Integer> map = redissonClient.getMap("myMap");
Long result = 0;
for (Integer value : map.values()) {
result += value;
}
return result;
}

}

在创建ExecutorService时可以配置以下参数:

1
2
3
4
5
6
7
8
9
10
11
12
ExecutorOptions options = ExecutorOptions.defaults()

// 指定重新尝试执行任务的时间间隔。
// ExecutorService的工作节点将等待10分钟后重新尝试执行任务
//
// 设定为0则不进行重试
//
// 默认值为5分钟
options.taskRetryInterval(10, TimeUnit.MINUTES);
RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<Long> future = executorService.schedule(new CallableTask(), 10, TimeUnit.MINUTES);
Long result = future.get();

使用Lambda任务的范例:

1
2
3
4
5
RExecutorService executorService = redisson.getExecutorService("myExecutor", options);
ScheduledFuture<Long> future = executorService.schedule((Callable & Serializable)() -> {
System.out.println("task has been executed!");
}, 10, TimeUnit.MINUTES);
Long result = future.get();

采用Runnable任务的范例:

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
public class RunnableTask implements Runnable {

@RInject
private RedissonClient redissonClient;

private long param;

public RunnableTask() {
}

public RunnableTask(long param) {
this.param= param;
}

@Override
public void run() {
RAtomicLong atomic = redissonClient.getAtomicLong("myAtomic");
atomic.addAndGet(param);
}

}

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
ScheduledFuture<?> future1 = executorService.schedule(new RunnableTask(123), 10, TimeUnit.HOURS);
// ...
ScheduledFuture<?> future2 = executorService.scheduleAtFixedRate(new RunnableTask(123), 10, 25, TimeUnit.HOURS);
// ...
ScheduledFuture<?> future3 = executorService.scheduleWithFixedDelay(new RunnableTask(123), 5, 10, TimeUnit.HOURS);

通过CRON表达式设定任务计划

在分布式调度任务中,可以通过CRON表达式来为任务设定一个更复杂的计划。表达式与Quartz的CRON格式完全兼容。

1
2
3
4
5
6
RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
executorService.schedule(new RunnableTask(), CronSchedule.of("10 0/5 * * * ?"));
// ...
executorService.schedule(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
// ...
executorService.schedule(new RunnableTask(), CronSchedule.weeklyOnDayAndHourAndMinute(12, 4, Calendar.MONDAY, Calendar.FRIDAY));

取消计划任务

分布式调度任务服务提供了两张取消任务的方式:通过调用ScheduledFuture.cancel()方法或调用RScheduledExecutorService.cancelScheduledTask方法。通过对Thread.currentThread().isInterrupted()方法的调用可以在已经处于运行状态的任务里实现任务中断:

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
public class RunnableTask implements Callable<Long> {

@RInject
private RedissonClient redissonClient;

@Override
public Long call() throws Exception {
RMap<String, Integer> map = redissonClient.getMap("myMap");
Long result = 0;
// map里包含了许多的元素
for (Integer value : map.values()) {
if (Thread.currentThread().isInterrupted()) {
// 任务被取消了
return null;
}
result += value;
}
return result;
}

}

RScheduledExecutorService executorService = redisson.getExecutorService("myExecutor");
RScheduledFuture<Long> future = executorService.scheduleAsync(new RunnableTask(), CronSchedule.dailyAtHourAndMinute(10, 5));
// ...
future.cancel(true);
// 或
String taskId = future.getTaskId();
// ...
executorService.cancelScheduledTask(taskId);

5、分布式映射归纳服务(MapReduce)

介绍

Redisson提供了通过映射归纳(MapReduce)编程模式来处理储存在Redis环境里的大量数据的服务。这个想法来至于其他的类似实现方式和谷歌发表的研究。所有 映射(Map) 和 归纳(Reduce) 阶段中的任务都是被分配到各个独立节点(Redisson Node)里并行执行的。以下所有接口均支持映射归纳(MapReduce)功能: RMap、 RMapCache、 RLocalCachedMap、 RSet、 RSetCache、 RList、 RSortedSet、 RScoredSortedSet、 RQueue、 RBlockingQueue、 RDeque、 RBlockingDeque、 RPriorityQueue 和 RPriorityDeque

映射归纳(MapReduce)的功能是通过RMapper、 RCollectionMapper、 RReducer 和 RCollator 这几个接口实现的。

  1. RMapper 映射器接口适用于映射(Map)类,它用来把映射(Map)中的每个元素转换为另一个作为归纳(Reduce)处理用的键值对。
    1
    2
    3
    4
    5
    public interface RMapper<KIn, VIn, KOut, VOut> extends Serializable {

    void map(KIn key, VIn value, RCollector<KOut, VOut> collector);

    }
  2. RCollectionMapper 映射器接口仅适用于集合(Collection)类型的对象,它用来把集合(Collection)中的元素转换成一组作为归纳(Reduce)处理用的键值对。
    1
    2
    3
    4
    5
    public interface RCollectionMapper<VIn, KOut, VOut> extends Serializable {

    void map(VIn value, RCollector<KOut, VOut> collector);

    }
  3. RReducer 归纳器接口用来将上面这些,由映射器生成的键值对列表进行归纳整理。
    1
    2
    3
    4
    5
    public interface RReducer<K, V> extends Serializable {

    V reduce(K reducedKey, Iterator<V> values);

    }
  4. RCollator 收集器接口用来把归纳整理以后的结果化简为单一一个对象。
    1
    2
    3
    4
    5
    public interface RCollator<K, V, R> extends Serializable {

    R collate(Map<K, V> resultMap);

    }
    以上每个阶段的任务都可以用@RInject注解的方式来获取RedissonClient实例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class WordMapper implements RMapper<String, String, String, Integer> {

    @RInject
    private RedissonClient redissonClient;

    @Override
    public void map(String key, String value, RCollector<String, Integer> collector) {

    // ...

    redissonClient.getAtomicLong("mapInvocations").incrementAndGet();
    }

    }

    映射(Map)类型的使用范例

    Redisson提供的RMap、 RMapCache和RLocalCachedMap这三种映射(Map)类型的对象均可以使用这种分布式映射归纳(MapReduce)服务。

以下是在映射(Map)类型的基础上采用映射归纳(MapReduce)来实现字数统计的范例:

Mapper对象将每个映射的值用空格且分开。

1
2
3
4
5
6
7
8
9
10
11
public class WordMapper implements RMapper<String, String, String, Integer> {

@Override
public void map(String key, String value, RCollector<String, Integer> collector) {
String[] words = value.split("[^a-zA-Z]");
for (String word : words) {
collector.emit(word, 1);
}
}

}

Reducer对象计算统计所有单词的使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class WordReducer implements RReducer<String, Integer> {

@Override
public Integer reduce(String reducedKey, Iterator<Integer> iter) {
int sum = 0;
while (iter.hasNext()) {
Integer i = (Integer) iter.next();
sum += i;
}
return sum;
}

}

Collator对象统计所有单词的使用情况。

1
2
3
4
5
6
7
8
9
10
11
12
public class WordCollator implements RCollator<String, Integer, Integer> {

@Override
public Integer collate(Map<String, Integer> resultMap) {
int result = 0;
for (Integer count : resultMap.values()) {
result += count;
}
return result;
}

}

把上面的各个对象串起来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RMap<String, String> map = redisson.getMap("wordsMap");
map.put("line1", "Alice was beginning to get very tired");
map.put("line2", "of sitting by her sister on the bank and");
map.put("line3", "of having nothing to do once or twice she");
map.put("line4", "had peeped into the book her sister was reading");
map.put("line5", "but it had no pictures or conversations in it");
map.put("line6", "and what is the use of a book");
map.put("line7", "thought Alice without pictures or conversation");

RMapReduce<String, String, String, Integer> mapReduce
= map.<String, Integer>mapReduce()
.mapper(new WordMapper())
.reducer(new WordReducer());

// 统计词频
Map<String, Integer> mapToNumber = mapReduce.execute();

// 统计字数
Integer totalWordsAmount = mapReduce.execute(new WordCollator());

集合(Collection)类型的使用范例

Redisson提供的RSet、 RSetCache、 RList、 RSortedSet、 RScoredSortedSet、 RQueue、 RBlockingQueue、 RDeque、 RBlockingDeque、 RPriorityQueue和RPriorityDeque这几种集合(Collection)类型的对象均可以使用这种分布式映射归纳(MapReduce)服务。

以下是在集合(Collection)类型的基础上采用映射归纳(MapReduce)来实现字数统计的范例:

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
public class WordMapper implements RCollectionMapper<String, String, Integer> {

@Override
public void map(String value, RCollector<String, Integer> collector) {
String[] words = value.split("[^a-zA-Z]");
for (String word : words) {
collector.emit(word, 1);
}
}

}
public class WordReducer implements RReducer<String, Integer> {

@Override
public Integer reduce(String reducedKey, Iterator<Integer> iter) {
int sum = 0;
while (iter.hasNext()) {
Integer i = (Integer) iter.next();
sum += i;
}
return sum;
}

}
public class WordCollator implements RCollator<String, Integer, Integer> {

@Override
public Integer collate(Map<String, Integer> resultMap) {
int result = 0;
for (Integer count : resultMap.values()) {
result += count;
}
return result;
}

}
RList<String> list = redisson.getList("myList");
list.add("Alice was beginning to get very tired");
list.add("of sitting by her sister on the bank and");
list.add("of having nothing to do once or twice she");
list.add("had peeped into the book her sister was reading");
list.add("but it had no pictures or conversations in it");
list.add("and what is the use of a book");
list.add("thought Alice without pictures or conversation");

RCollectionMapReduce<String, String, Integer> mapReduce
= list.<String, Integer>mapReduce()
.mapper(new WordMapper())
.reducer(new WordReducer());

// 统计词频
Map<String, Integer> mapToNumber = mapReduce.execute();

// 统计字数
Integer totalWordsAmount = mapReduce.execute(new WordCollator());

八、其他

1、对Redis节点的操作

Redisson的NodesGroup对象提供了许些对Redis节点的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NodesGroup nodesGroup = redisson.getNodesGroup();
nodesGroup.addConnectionListener(new ConnectionListener() {
public void onConnect(InetSocketAddress addr) {
// Redis节点连接成功
}

public void onDisconnect(InetSocketAddress addr) {
// Redis节点连接断开
}
});
也可以用来PING单个Redis节点或全部节点。

NodesGroup nodesGroup = redisson.getNodesGroup();
Collection<Node> allNodes = nodesGroup.getNodes();
for (Node n : allNodes) {
n.ping();
}
// 或者
nodesGroup.pingAll();

2. 复杂多维对象结构和对象引用的支持

Redisson突破了Redis数据结构维度的限制,通过一个特殊引用对象的帮助,Redisson允许以任意的组合方式构建多维度的复杂对象结构,实现了对象之间的类似传统数据库里的关联关系。使用范例如下:

1
2
3
4
5
6
7
8
RMap<RSet<RList>, RList<RMap>> map = redisson.getMap("myMap");
RSet<RList> set = redisson.getSet("mySet");
RList<RMap> list = redisson.getList("myList");

map.put(set, list);
// 在特殊引用对象的帮助下,我们甚至可以构建一个循环引用,这是通过普通序列化方式实现不了的。
set.add(list);
list.add(map);

在map包含的元素发生改变以后,我们无需再次“保存/持久”这些对象。因为map对象所记录的并不是序列化以后的值,而是元素对象的引用。这让Redisson提供的对象在使用方法上,与普通Java对象的使用方法一致。从而让Redis成为内存的一部分,而不仅仅是一个储存空间。

以上范例中,一共创建了三个Redis数据结构:一个Redis HASH,一个Redis SET和一个Redis LIST。

3. 命令的批量执行

多个连续命令可以通过RBatch对象在一次网络会话请求里合并发送,这样省去了产生多个请求消耗的时间和资源。这在Redis中叫做管道。

用户可以通过以下方式调整通过管道方式发送命令的方式:

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
BatchOptions options = BatchOptions.defaults()
// 指定执行模式
//
// ExecutionMode.REDIS_READ_ATOMIC - 所有命令缓存在Redis节点中,以原子性事务的方式执行。
//
// ExecutionMode.REDIS_WRITE_ATOMIC - 所有命令缓存在Redis节点中,以原子性事务的方式执行。
//
// ExecutionMode.IN_MEMORY - 所有命令缓存在Redisson本机内存中统一发送,但逐一执行(非事务)。默认模式。
//
// ExecutionMode.IN_MEMORY_ATOMIC - 所有命令缓存在Redisson本机内存中统一发送,并以原子性事务的方式执行。
//
.executionMode(ExecutionMode.IN_MEMORY)

// 告知Redis不用返回结果(可以减少网络用量)
.skipResult()

// 将写入操作同步到从节点
// 同步到2个从节点,等待时间为1秒钟
.syncSlaves(2, 1, TimeUnit.SECONDS)

// 处理结果超时为2秒钟
.responseTimeout(2, TimeUnit.SECONDS)

// 命令重试等待间隔时间为2秒钟
.retryInterval(2, TimeUnit.SECONDS);

// 命令重试次数。仅适用于未发送成功的命令
.retryAttempts(4);
使用方式如下:

RBatch batch = redisson.createBatch();
batch.getMap("test").fastPutAsync("1", "2");
batch.getMap("test").fastPutAsync("2", "3");
batch.getMap("test").putAsync("2", "5");
batch.getAtomicLongAsync("counter").incrementAndGetAsync();
batch.getAtomicLongAsync("counter").incrementAndGetAsync();

BatchResult res = batch.execute();
// 或者
Future<BatchResult> asyncRes = batch.executeAsync();
List<?> response = res.getResponses();
res.getSyncedSlaves();

在集群模式下,所有的命令会按各个槽所在的节点,筛选分配到各个节点并同时发送。每个节点返回的结果将会汇总到最终的结果列表里。

4. Redisson事务

Redisson为RMap、RMapCache、RLocalCachedMap、RSet、RSetCache和RBucket这样的对象提供了具有ACID属性的事务功能。Redisson事务通过分布式锁保证了连续写入的原子性,同时在内部通过操作指令队列实现了Redis原本没有的提交与滚回功能。当提交与滚回遇到问题的时候,将通过org.redisson.transaction.TransactionException告知用户。

目前支持的环境如下: SINGLE, MASTER/SLAVE, SENTINEL, ELASTICACHE REPLICATED, AZURE CACHE, RLEC。

Redisson事务支持的事务隔离等级为: READ_COMMITTED,即仅读取提交后的结果。

另见 Spring事务管理器 和本章 XA事务(XA Transactions)。

以下选项可以用来配置事务属性:

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
TransactionOptions options = TransactionOptions.defaults()
// 设置参与本次事务的主节点与其从节点同步的超时时间。
// 默认值是5秒。
.syncSlavesTimeout(5, TimeUnit.SECONDS)

// 处理结果超时。
// 默认值是3秒。
.responseTimeout(3, TimeUnit.SECONDS)

// 命令重试等待间隔时间。仅适用于未发送成功的命令。
// 默认值是1.5秒。
.retryInterval(2, TimeUnit.SECONDS)

// 命令重试次数。仅适用于未发送成功的命令。
// 默认值是3次。
.retryAttempts(3)

// 事务超时时间。如果规定时间内没有提交该事务则自动滚回。
// 默认值是5秒。
.timeout(5, TimeUnit.SECONDS);
代码范例:

RTransaction transaction = redisson.createTransaction(TransactionOptions.defaults());

RMap<String, String> map = transaction.getMap("myMap");
map.put("1", "2");
String value = map.get("3");
RSet<String> set = transaction.getSet("mySet")
set.add(value);

try {
transaction.commit();
} catch(TransactionException e) {
transaction.rollback();
}

5. XA事务(XA Transactions)

Redisson提供了XAResource标准的实现。该实现可用于JTA事务中。

另见本章Redisson事务和Spring事务管理器。

该功能仅适用于Redisson PRO版本

代码范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Transaction对象可以从所有兼容JTA接口的事务管理器中获取。
Transaction globalTransaction = transactionManager.getTransaction();

RXAResource xaResource = redisson.getXAResource();
globalTransaction.enlistResource(xaResource);

RTransaction transaction = xaResource.getTransaction();
RBucket<String> bucket = transaction.getBucket("myBucket");
bucket.set("simple");
RMap<String, String> map = transaction.getMap("myMap");
map.put("myKey", "myValue");

transactionManager.commit();

6. 脚本执行

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
redisson.getBucket("foo").set("bar");
String r = redisson.getScript().eval(Mode.READ_ONLY,
"return redis.call('get', 'foo')", RScript.ReturnType.VALUE);

// 通过预存的脚本进行同样的操作
RScript s = redisson.getScript();
// 首先将脚本保存到所有的Redis主节点
String res = s.scriptLoad("return redis.call('get', 'foo')");
// 返回值 res == 282297a0228f48cd3fc6a55de6316f31422f5d17

// 再通过SHA值调用脚本
Future<Object> r1 = redisson.getScript().evalShaAsync(Mode.READ_ONLY,
"282297a0228f48cd3fc6a55de6316f31422f5d17",
RScript.ReturnType.VALUE, Collections.emptyList());
10.7. 底层Redis客户端
Redisson在底层采用了高性能异步非阻塞式Java客户端,它同时支持异步和同步两种通信模式。如果有哪些命令Redisson还没提供支持,也可以直接通过调用底层Redis客户端来实现。Redisson支持的命令在Redis命令和Redisson对象匹配列表里做了详细对比参照。

// 在使用多个客户端的情况下可以共享同一个EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();

RedisClientConfig config = new RedisClientConfig();
config.setAddress("redis://localhost:6379") // 或者用rediss://使用加密连接
.setPassword("myPassword")
.setDatabase(0)
.setClientName("myClient")
.setGroup(group);

RedisClient client = RedisClient.create(config);
RedisConnection conn = client.connect();
// 或
RFuture<RedisConnection> connFuture = client.connectAsync();

conn.sync(StringCodec.INSTANCE, RedisCommands.SET, "test", 0);
// 或
conn.async(StringCodec.INSTANCE, RedisCommands.GET, "test");

conn.close()
// 或
conn.closeAsync()

client.shutdown();
// 或
client.shutdownAsync();

EhCache介绍和运用

发表于 2018-12-23 | 分类于 EhCache

Spring Boot 的缓存机制

高速缓存抽象不提供实际存储,并且依赖于由org.springframework.cache.Cache和org.springframework.cache.CacheManager接口实现的抽象。 Spring Boot根据实现自动配置合适的CacheManager,只要缓存支持通过@EnableCaching注释启用即可。

EhCache

Ehcache是一个用Java实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的cache管理方案。同时ehcache作为开放源代码项目,采用限制比较宽松的Apache License V2.0作为授权方式,被广泛地用于Hibernate, Spring,Cocoon等其他开源系统。Ehcache 从 Hibernate 发展而来,逐渐涵盖了 Cahce 界的全部功能,是目前发展势头最好的一个项目。具有快速,简单,低消耗,依赖性小,扩展性强,支持对象或序列化缓存,支持缓存或元素的失效,提供 LRU、LFU 和 FIFO 缓存策略,支持内存缓存和磁盘缓存,分布式缓存机制等等特点。

20200423140459

快速;简单;多种缓存策略;
缓存数据有两级:内存和磁盘,因此无需担心容量问题;
缓存数据会在虚拟机重启的过程中写入磁盘;
可以通过 RMI、可插入 API 等方式进行分布式缓存;
具有缓存和缓存管理器的侦听接口;
支持多缓存管理器实例,以及一个实例的多个缓存区域;
提供 Hibernate 的缓存实现;

说明

CacheManager:是缓存管理器,可以通过单例或者多例的方式创建,也是Ehcache的入口类。
Cache:每个CacheManager可以管理多个Cache,每个Cache可以采用hash的方式管理多个Element。
Element:用于存放真正缓存内容的。

Ehcache的缓存数据淘汰策略

FIFO:先进先出
LFU:最少被使用,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU:最近最少使用,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
Ehcache的缓存数据过期策略
Ehcache采用的是懒淘汰机制,每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期。

生命周期

这段代码介绍了Ehcache3,缓存使用生命周期的一个过程。

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
CacheManager cacheManager =
// 静态方法CacheManagerBuilder.newCacheManagerBuilder将返回一个新的org.ehcache.config.builders.CacheManagerBuilder的实例。
CacheManagerBuilder.newCacheManagerBuilder()
.withCache("preConfigured",
// 当我们要构建一个缓存管理器的时候,使用CacheManagerBuilder来创建一个预配置(pre-configured)缓存。
// 第一个参数是一个别名,用于Cache和Cachemanager进行配合。
// 第二个参数是org.ehcache.config.CacheConfiguration主要用来配置Cache。
// 我们使用org.ehcache.config.builders.CacheConfigurationBuilder的静态方法newCacheConfigurationBuilder来创建一个默认配置实例。
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.heap(100))
// 调用.build方法返回一个完整的实例,也能使用CacheManager来初始化。
.build())
//在你开始使用CacheManager的时候,需要使用init()方法进行初始化。
.build(true);

Cache<Long, String> preConfigured
//取回前面中设定的pre-configured别名,我们对于key和要传递的值类型,要求是类型安全的,否则将抛出ClassCastException异常。
= cacheManager.getCache("preConfigured", Long.class, String.class);
//通过CacheManager创建出新的Cache。实例化和完整实例化的Cache将通过CacheManager.getCache API返回。
Cache<Long, String> myCache = cacheManager.createCache("myCache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class,
ResourcePoolsBuilder.heap(100)).build());
//使用put方法存储数据。
myCache.put(1L, "da one!");
//使用get方法获取数据。
String value = myCache.get(1L);
//close方法将释放CacheManager所管理的缓存资源。
cacheManager.close();

使用

1、引入依赖

1
2
3
4
5
6
7
8
9
10
<!--开启 cache 缓存-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ehcache 缓存 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

引入配置文件 ehcache.xml

在resource文件夹下创建文件ehcache.xml,并进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU" />

<!-- 这里的 users 缓存空间是为了下面的 demo 做准备 -->
<cache
name="users"
eternal="false"
maxElementsInMemory="100"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="300"
memoryStoreEvictionPolicy="LRU" />
</ehcache>

ehcache.xml 文件配置详解

name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。

在主类加上启动注解

在 Spring Boot 主类加上开启缓存的注解@EnableCaching。表示启动缓存功能。

在方法或类上就直接可以使用了,如:@Cacheable(value=”neteaseAccountInfoCache”,key=”#fid+#unionId “,unless=”#result==null”)

如果需要对缓存做更详细操作,可以参考https://www.cnblogs.com/zhangzhen894095789/p/6525845.html,比较全面,感谢!

Idea SpringBoot多模块项目打包

发表于 2018-12-23 | 分类于 工具使用

一、打包配置

多模块时候,一般parant的pom中不用配置打包插件,直接在主模块中配置就行,如果有mybatis的模块,也可以配置,如:

主模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com......ApplicationMain</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

有mybatis的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**.*</include>
<include>**/*.*</include><!-- i18n能读取到 -->
<include>**/*/*.*</include>
</includes>
</resource>
</resources>
</build>

二、打包测试

打包时默认会进行测试,而由于多模块架构,测试一直失败,跳过测试的方法:

1、在pom文件里加插件:因为是多模块结构以及springboot的原因,导致每个pom都要添加,还可能出现意外问题。

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

2、控制台,cd到父项目下,执行命令:mvn clean install -DskipTests

3、同上,执行命令:mvn clean install -Dmaven.test.skip=true

4、如果在idea中,直接这样:

通常我都是,只在主模块配置打包,其他不处理,mapper文件放在数据层模块的资源文件中,默认spring boot会自动去编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.......ApplicationMain</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Maven多模块项目单独编译子模块项目时报错:Failed to execute goal on project/Could not resolve dependencies for project

背景:常规的父子项目搭建的工程,参考:http://www.cnblogs.com/EasonJim/p/6863987.html

解决方法:

1、需要把parent工程,也就是package是pom的那个工程先install一下;之后再install公共引入的模块,最后就可以单独编译子模块。

2、不用install,直接编译parent项目;这种方式只能在parent项目下进行,不能单独编译子模块。

Tomcat是如何实现异步Servlet

发表于 2018-11-01 | 分类于 Tomcat

在Spring boot中实现一个servlet,需要再启动类加上@ServletComponentScan注解来扫描Servlet

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
@WebServlet(urlPatterns = "/async",asyncSupported = true)
@Slf4j
public class AsyncServlet extends HttpServlet {

ExecutorService executorService =Executors.newSingleThreadExecutor();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//开启异步,获取异步上下文
final AsyncContext ctx = req.startAsync();
// 提交线程池异步执行
executorService.execute(new Runnable() {

@Override
public void run() {
try {
log.info("async Service 准备执行了");
//模拟耗时任务
Thread.sleep(10000L);
ctx.getResponse().getWriter().print("async servlet");
log.info("async Service 执行了");
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
//最后执行完成后完成回调。
ctx.complete();
}
});
}

异步Servlet的内部原理

上面主要是:final AsyncContext ctx = req.startAsync()和ctx.complete(),查看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public AsyncContext startAsync(ServletRequest request,
ServletResponse response) {
if (!isAsyncSupported()) {
IllegalStateException ise =
new IllegalStateException(sm.getString("request.asyncNotSupported"));
log.warn(sm.getString("coyoteRequest.noAsync",
StringUtils.join(getNonAsyncClassNames())), ise);
throw ise;
}

if (asyncContext == null) {
asyncContext = new AsyncContextImpl(this);
}

asyncContext.setStarted(getContext(), request, response,
request==getRequest() && response==getResponse().getResponse());
asyncContext.setTimeout(getConnector().getAsyncTimeout());

return asyncContext;
}

req.startAsync()只是保存了一个异步上下文,同时设置一些基础信息,比如Timeout,这里设置的默认超时时间是30S,如果你的异步处理逻辑超过30S,此时执行ctx.complete()就会抛出IllegalStateException 异常。

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
  public void complete() {
if (log.isDebugEnabled()) {
logDebug("complete ");
}
check();
request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
}
//类:AbstractProcessor
public final void action(ActionCode actionCode, Object param) {
case ASYNC_COMPLETE: {
clearDispatches();
if (asyncStateMachine.asyncComplete()) {
processSocketEvent(SocketEvent.OPEN_READ, true);
}
break;
}
}
//类:AbstractProcessor
protected void processSocketEvent(SocketEvent event, boolean dispatch) {
SocketWrapperBase<?> socketWrapper = getSocketWrapper();
if (socketWrapper != null) {
socketWrapper.processSocket(event, dispatch);
}
}
//类:AbstractEndpoint
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
//省略部分代码
SocketProcessorBase<S> sc = null;
if (processorCache != null) {
sc = processorCache.pop();
}
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}

return true;
}

所以,这里最终会调用AbstractEndpoint的processSocket方法,EndPoint是用来接受和处理请求的,接下来就会交给Processor去进行协议处理。

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
///类:AbstractProcessorLight
public SocketState process(SocketWrapperBase<?> socketWrapper, SocketEvent status)
throws IOException {
//省略部分diam
SocketState state = SocketState.CLOSED;
Iterator<DispatchType> dispatches = null;
do {
if (dispatches != null) {
DispatchType nextDispatch = dispatches.next();
state = dispatch(nextDispatch.getSocketStatus());
} else if (status == SocketEvent.DISCONNECT) {

} else if (isAsync() || isUpgrade() || state == SocketState.ASYNC_END) {
state = dispatch(status);
if (state == SocketState.OPEN) {
state = service(socketWrapper);
}
} else if (status == SocketEvent.OPEN_WRITE) {
state = SocketState.LONG;
} else if (status == SocketEvent.OPEN_READ){
state = service(socketWrapper);
} else {
state = SocketState.CLOSED;
}

} while (state == SocketState.ASYNC_END ||
dispatches != null && state != SocketState.CLOSED);

return state;
}

AbstractProcessorLight会根据SocketEvent的状态来判断是不是要去调用service(socketWrapper),该方法最终会去调用到容器,从而完成业务逻辑的调用,我们这个请求是执行完成后调用的,肯定不能进容器了,不然就是死循环了,这里通过isAsync()判断,就会进入dispatch(status),最终会调用CoyoteAdapter的asyncDispatch方法。

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
public boolean asyncDispatch(org.apache.coyote.Request req, org.apache.coyote.Response res,
SocketEvent status) throws Exception {
//省略部分代码
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);
boolean success = true;
AsyncContextImpl asyncConImpl = request.getAsyncContextInternal();
try {
if (!request.isAsync()) {
response.setSuspended(false);
}

if (status==SocketEvent.TIMEOUT) {
if (!asyncConImpl.timeout()) {
asyncConImpl.setErrorState(null, false);
}
} else if (status==SocketEvent.ERROR) {

}

if (!request.isAsyncDispatching() && request.isAsync()) {
WriteListener writeListener = res.getWriteListener();
ReadListener readListener = req.getReadListener();
if (writeListener != null && status == SocketEvent.OPEN_WRITE) {
ClassLoader oldCL = null;
try {
oldCL = request.getContext().bind(false, null);
res.onWritePossible();//这里执行浏览器响应,写入数据
if (request.isFinished() && req.sendAllDataReadEvent() &&
readListener != null) {
readListener.onAllDataRead();
}
} catch (Throwable t) {

} finally {
request.getContext().unbind(false, oldCL);
}
}
}
}
//这里判断异步正在进行,说明这不是一个完成方法的回调,是一个正常异步请求,继续调用容器。
if (request.isAsyncDispatching()) {
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
Throwable t = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
if (t != null) {
asyncConImpl.setErrorState(t, true);
}
}
//注意,这里,如果超时或者出错,request.isAsync()会返回false,这里是为了尽快的输出错误给客户端。
if (!request.isAsync()) {
//这里也是输出逻辑
request.finishRequest();
response.finishResponse();
}
//销毁request和response
if (!success || !request.isAsync()) {
updateWrapperErrorCount(request, response);
request.recycle();
response.recycle();
}
}
return success;
}

ctx.complete()执行最终的方法了,完成了数据的输出,最终输出到浏览器。

第一次doGet请求执行完成后,Tomcat是怎么知道不用返回到客户端的呢?关键代码在CoyoteAdapter中的service方法,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
postParseSuccess = postParseRequest(req, request, res, response);
//省略部分代码
if (postParseSuccess) {
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
connector.getService().getContainer().getPipeline().getFirst().invoke(
request, response);
}
if (request.isAsync()) {
async = true;
} else {
//输出数据到客户端
request.finishRequest();
response.finishResponse();
if (!async) {
updateWrapperErrorCount(request, response);
//销毁request和response
request.recycle();
response.recycle();
}

调用完Servlet后,会通过request.isAsync()来判断是否是异步请求,如果是异步请求,就设置async = true。如果是非异步请求就执行输出数据到客户端逻辑,同时销毁request和response。这里就完成了请求结束后不响应客户端的操作。

Spring Boot的@EnableAsync注解不是异步Servlet

从业务层面来说,确实是异步编程,但是有一个问题,抛开业务的并行处理来说,针对整个请求来说,并不是异步的,也就是说不能立即释放Tomcat的线程,从而不能达到异步Servlet的效果。为什么它不是异步的。

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
@RestController
@Slf4j
public class TestController {
@Autowired
private TestService service;

@GetMapping("/hello")
public String test() {
try {
log.info("testAsynch Start");
CompletableFuture<String> test1 = service.test1();
CompletableFuture<String> test2 = service.test2();
CompletableFuture<String> test3 = service.test3();
CompletableFuture.allOf(test1, test2, test3);
log.info("test1=====" + test1.get());
log.info("test2=====" + test2.get());
log.info("test3=====" + test3.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return "hello";
}
@Service
public class TestService {
@Async("asyncExecutor")
public CompletableFuture<String> test1() throws InterruptedException {
Thread.sleep(3000L);
return CompletableFuture.completedFuture("test1");
}

@Async("asyncExecutor")
public CompletableFuture<String> test2() throws InterruptedException {
Thread.sleep(3000L);
return CompletableFuture.completedFuture("test2");
}

@Async("asyncExecutor")
public CompletableFuture<String> test3() throws InterruptedException {
Thread.sleep(3000L);
return CompletableFuture.completedFuture("test3");
}
}
@SpringBootApplication
@EnableAsync
public class TomcatdebugApplication {

public static void main(String[] args) {
SpringApplication.run(TomcatdebugApplication.class, args);
}

@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsynchThread-");
executor.initialize();
return executor;
}

这里我请求之后,在调用容器执行业务逻辑之前打了一个断点,然后在返回之后的同样打了一个断点,在Controller执行完之后,请求才回到了CoyoteAdapter中,并且判断request.isAsync(),根据图中看到,是为false,那么接下来就会执行request.finishRequest()和response.finishResponse()
来执行响应的结束,并销毁请求和响应体。很有趣的事情是,我实验的时候发现,在执行request.isAsync()之前,浏览器的页面上已经出现了响应体,这是SpringBoot框架已经通过StringHttpMessageConverter类中的writeInternal方法已经进行输出了。
以上分析的核心逻辑就是,Tomcat的线程执行CoyoteAdapter调用容器后,必须要等到请求返回,然后再判断是否是异步请求,再处理请求,然后执行完毕后,线程才能进行回收。而我一最开始的异步Servlet例子,执行完doGet方法后,就会立即返回,也就是会直接到request.isAsync()的逻辑,然后整个线程的逻辑执行完毕,线程被回收。

Tomcat源码分析-web请求处理过程

发表于 2018-09-04 | 分类于 Tomcat

请求

请求时由Connector来处理。从启动连接器开始:

启动连接器

连接器构造方法中实例化了一个ProtocolHandler协议处理器,用来处理请求。然后在启动方法中启动协议处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void startInternal() throws LifecycleException {

// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}

setState(LifecycleState.STARTING);

try {
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}

然后在AbstractProtocol中启动endpoint:

启动endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void start() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.start", getName()));
}

endpoint.start();

// Start timeout thread
asyncTimeout = new AsyncTimeout();
Thread timeoutThread = new Thread(asyncTimeout, getNameInternal() + "-AsyncTimeout");
int priority = endpoint.getThreadPriority();
if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
priority = Thread.NORM_PRIORITY;
}
timeoutThread.setPriority(priority);
timeoutThread.setDaemon(true);
timeoutThread.start();
}

endpoint的实现有几种:
20200406092859

比如:NioEndPoint

NioEndPoint

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
/**
* 启动NIO端点,创建acceptor和poller线程。
*/
@Override
public void startInternal() throws Exception {

if (!running) {
running = true;
paused = false;

processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache());
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache());
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool());

// 创建线程池
if ( getExecutor() == null ) {
createExecutor();
}

initializeConnectionLatch();

// 启动轮询器线程
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}
//启动线程s 是父类AbstractEndpoint方法
startAcceptorThreads();
}
}

中定义了内部类,通过线程方式处理。

Acceptor接收请求

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
protected class Acceptor extends AbstractEndpoint.Acceptor {

@Override
public void run() {

int errorDelay = 0;

// Loop until we receive a shutdown command
while (running) {

// Loop if endpoint is paused 自旋的方式
while (paused && running) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}

if (!running) {
break;
}
state = AcceptorState.RUNNING;

try {
//i如果达到最大连接数,请稍候
countUpOrAwaitConnection();

SocketChannel socket = null;
try {
// 接受套接字
socket = serverSock.accept();
} catch (IOException ioe) {
// We didn't get a socket
countDownConnection();
if (running) {
// 引入延迟
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
} else {
break;
}
}
// 成功接受,重置错误延迟
errorDelay = 0;

// 配置套接字
if (running && !paused) {
// 把套接字传递给适当的处理器
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else {
closeSocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
}
}
state = AcceptorState.ENDED;
}
private void closeSocket(SocketChannel socket) {
countDownConnection();
try {
socket.socket().close();
} catch (IOException ioe) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("endpoint.err.close"), ioe);
}
}
try {
socket.close();
} catch (IOException ioe) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("endpoint.err.close"), ioe);
}
}
}
}

调用setSocketOptions方法

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
//处理指定的连接
protected boolean setSocketOptions(SocketChannel socket) {
// Process the connection
try {
//禁用阻塞
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);

//就是把SocketChannel转换为Nio Channel
NioChannel channel = nioChannels.pop();
if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer());
if (isSSLEnabled()) {//HTTPS
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset();
}
//注册进去
getPoller0().register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error("",t);
} catch (Throwable tt) {
ExceptionUtils.handleThrowable(tt);
}
// Tell to close the socket
return false;
}
return true;
}

//Poller方法,向轮询器注册新创建的套接字
public void register(final NioChannel socket) {
socket.setPoller(this);
NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
socket.setSocketWrapper(ka);
ka.setPoller(this);
ka.setReadTimeout(getSocketProperties().getSoTimeout());
ka.setWriteTimeout(getSocketProperties().getSoTimeout());
ka.setKeepAliveLeft(NioEndpoint.this.getMaxKeepAliveRequests());
ka.setSecure(isSSLEnabled());
ka.setReadTimeout(getConnectionTimeout());
ka.setWriteTimeout(getConnectionTimeout());
PollerEvent r = eventCache.pop();
ka.interestOps(SelectionKey.OP_READ);//this is what OP_REGISTER turns into.
//新建PollerEvent 加入到 EventQueue
if ( r==null) r = new PollerEvent(socket,ka,OP_REGISTER);
else r.reset(socket,ka,OP_REGISTER);
addEvent(r);
}

Poller

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
@Override
public void run() {
// Loop until destroy() is called
while (true) {

boolean hasEvents = false;

try {
if (!close) {
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
//非阻塞
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
if (close) {
/**
* 遍历所有eventqueue中所有的PollorEvent,然后依次调用PollorEvent的run方法,将socket注册到selector
*/
events();
timeout(0, false);
try {
selector.close();
} catch (IOException ioe) {
log.error(sm.getString("endpoint.nio.selectorCloseFail"), ioe);
}
break;
}
} catch (Throwable x) {
ExceptionUtils.handleThrowable(x);
log.error("",x);
continue;
}
//either we timed out or we woke up, process events first
if ( keyCount == 0 ) hasEvents = (hasEvents | events());

Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// Walk through the collection of ready keys and dispatch any active event.
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (attachment == null) {
iterator.remove();
} else {
//接受请求后,移除。
iterator.remove();
processKey(sk, attachment);
}
}//while

//超时
timeout(keyCount,hasEvents);
}//while

getStopLatch().countDown();
}

//处理轮询器事件队列中的事件
public boolean events() {
boolean result = false;
//PollerEvent
PollerEvent pe = null;
for (int i = 0, size = events.size(); i < size && (pe = events.poll()) != null; i++ ) {
result = true;
try {
pe.run();
pe.reset();
if (running && !paused) {
eventCache.push(pe);
}
} catch ( Throwable x ) {
log.error("",x);
}
}

return result;
}

protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
try {
if ( close ) {
cancelledKey(sk);
} else if ( sk.isValid() && attachment != null ) {
if (sk.isReadable() || sk.isWritable() ) {
if ( attachment.getSendfileData() != null ) {
processSendfile(sk,attachment, false);
} else {
unreg(sk, attachment, sk.readyOps());
boolean closeSocket = false;
// 先读后写
if (sk.isReadable()) {
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
if (!closeSocket && sk.isWritable()) {
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
if (closeSocket) {
cancelledKey(sk);
}
}
}
} else {
//invalid key
cancelledKey(sk);
}
} catch ( CancelledKeyException ckx ) {
cancelledKey(sk);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error("",t);
}
}

//通过处理器 处理具有给定状态的给定SocketWrapper。
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
//如果没有拿到socket处理器,就去创建一个。
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
//通过线程池处理
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
getLog().warn(sm.getString("endpoint.executor.fail", socketWrapper) , ree);
return false;
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// OOM或类似的创建线程,或者池及其队列已满
getLog().error(sm.getString("endpoint.process.fail"), t);
return false;
}
return true;
}
1
protected abstract SocketProcessorBase<S> createSocketProcessor(SocketWrapperBase<S> socketWrapper, SocketEvent event);

createSocketProcessor方法是抽象方法,去他的处理器实现:

20200406100928

AbstractHttp11Protocol extends AbstractProtocol

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
@Override
protected Processor createProcessor() {
//Http11Processor
Http11Processor processor = new Http11Processor(this, getEndpoint());
processor.setAdapter(getAdapter());
//默认的keepalive的情况下,默认每个socket最大处理请求次数
processor.setMaxKeepAliveRequests(getMaxKeepAliveRequests());
//开启keepalive的timeout
processor.setConnectionUploadTimeout(getConnectionUploadTimeout());
//文件上传的默认超时时间
processor.setDisableUploadTimeout(getDisableUploadTimeout());

processor.setRestrictedUserAgents(getRestrictedUserAgents());
processor.setMaxSavePostSize(getMaxSavePostSize());
return processor;
}
```
通过处理后,设置对应的适配器。processor.setAdapter(getAdapter());通过适配器去匹配Mapper中的映射,如果找不到,就是请求错了。

最中到public class CoyoteAdapter implements Adapter中去处理。通过适配器就过度到相应的容器上去处理。
### 通过Adapter找到对应的容器
适配器执行方法:public class CoyoteAdapter implements Adapter
```java
@Override
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res)
throws Exception {
//把 coyote对象,转换为 http的对象
Request request = (Request) req.getNote(ADAPTER_NOTES);
Response response = (Response) res.getNote(ADAPTER_NOTES);

if (request == null) {
// 通过连接器创建Request Response
request = connector.createRequest();
request.setCoyoteRequest(req);
response = connector.createResponse();
response.setCoyoteResponse(res);

// 关联
request.setResponse(response);
response.setRequest(request);

// Set as notes
req.setNote(ADAPTER_NOTES, request);
res.setNote(ADAPTER_NOTES, response);

// 设置URICharset 编码
req.getParameters().setQueryStringCharset(connector.getURICharset());
}

if (connector.getXpoweredBy()) {
response.addHeader("X-Powered-By", POWERED_BY);
}

boolean async = false;
boolean postParseSuccess = false;

req.getRequestProcessor().setWorkerThreadName(THREAD_NAME.get());

try {
// 分析并设置Catalina和配置特定的请求参数
// 在map中解析请求
//在分析HTTP头之后执行必要的处理,以便将请求/响应对传递到容器管道的开始处进行处理。
postParseSuccess = postParseRequest(req, request, res, response);
if (postParseSuccess) {
//check valves if we support async
request.setAsyncSupported(
connector.getService().getContainer().getPipeline().isAsyncSupported());
// 调用容器:
// 通过连接器获得service可以获取容器(Engine只有一个),再获取调用链。在Engine(StandardEngine)启动后 pipeline也会启动,维护的是StandardEngineValve
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
}
if (request.isAsync()) {
async = true;
ReadListener readListener = req.getReadListener();
if (readListener != null && request.isFinished()) {
// 可能在service()方法期间读取了所有数据,因此需要在此处检查
ClassLoader oldCL = null;
try {
oldCL = request.getContext().bind(false, null);
if (req.sendAllDataReadEvent()) {
req.getReadListener().onAllDataRead();
}
} finally {
request.getContext().unbind(false, oldCL);
}
}

Throwable throwable =
(Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);

// 如果异步请求已启动,则在该容器线程完成并发生错误后不会结束,则触发异步错误进程
if (!request.isAsyncCompleting() && throwable != null) {
request.getAsyncContextInternal().setErrorState(throwable, true);
}
} else {
//请求响应完成
request.finishRequest();
response.finishResponse();
}

} catch (IOException e) {
// Ignore
} finally {
AtomicBoolean error = new AtomicBoolean(false);
res.action(ActionCode.IS_ERROR, error);

if (request.isAsyncCompleting() && error.get()) {
// 如果有字符,现在就将它们全部刷新到字节缓冲区,因为字节用于计算内容长度(当然,如果所有内容都适合字节缓冲区)。
res.action(ActionCode.ASYNC_POST_PROCESS, null);
async = false;
}

// Access log
if (!async && postParseSuccess) {
// 仅在调用处理时记录。如果postParseRequest()失败,则它已将其记录下来。
Context context = request.getContext();
Host host = request.getHost();
//如果上下文为空,则可能是终结点已关闭,此连接已关闭,请求已在其他线程中回收。
//该线程已经更新了访问日志,因此在这种情况下不更新访问日志是可以的。
//另一种可能是在 无法将处理和请求映射到上下文。
//在这种情况下,通过主机或引擎登录。
long time = System.currentTimeMillis() - req.getStartTime();
if (context != null) {
context.logAccess(request, response, time, false);
} else if (response.isError()) {
if (host != null) {
host.logAccess(request, response, time, false);
} else {
connector.getService().getContainer().logAccess(
request, response, time, false);
}
}
}

req.getRequestProcessor().setWorkerThreadName(null);

// 回收包装器请求和响应
if (!async) {
updateWrapperErrorCount(request, response);
request.recycle();
response.recycle();
}
}
}

connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);

通过连接器获得service可以获取容器(Engine只有一个),再获取调用链。在Engine(StandardEngine)启动后 pipeline也会启动,维护的是StandardEngineValve

invoke方法是StandardEngineValve中的invoke方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// 选择用于此请求的主机
Host host = request.getHost();
if (host == null) {
response.sendError
(HttpServletResponse.SC_BAD_REQUEST, sm.getString("standardEngine.noHost",request.getServerName()));
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}

// 请此主机处理此请求
host.getPipeline().getFirst().invoke(request, response);

}

StandardEngineValve中的invoke又会调用host中的StandardHostValve的invoke方法,依次StandardContextValve->StandardWrapperValve,最后到StandardWrapperValve中的invoke,这里处理servlet。

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
@Override
public final void invoke(Request request, Response response)
throws IOException, ServletException {

// Initialize local variables we may need
boolean unavailable = false;
Throwable throwable = null;
// This should be a Request attribute...
long t1=System.currentTimeMillis();
//增加请求次数
requestCount.incrementAndGet();
//获取StandardWrapper,每次都有StandardWrapper和value
StandardWrapper wrapper = (StandardWrapper) getContainer();
//获取Servlet
Servlet servlet = null;
Context context = (Context) wrapper.getParent();

// Check for the application being marked unavailable
if (!context.getState().isAvailable()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardContext.isUnavailable"));
unavailable = true;
}

// 检查是否获取到 包装头信息
if (!unavailable && wrapper.isUnavailable()) {
container.getLogger().info(sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
unavailable = true;
}

// 分配一个servlet实例来处理这个请求
try {
//没有获取到,就加载,Tomcat默认是按需加载。(延迟加载)
if (!unavailable) {
//实例化
servlet = wrapper.allocate();
}
} catch (UnavailableException e) {
container.getLogger().error(
sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
} catch (ServletException e) {
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), StandardWrapper.getRootCause(e));
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.allocateException",
wrapper.getName()), e);
throwable = e;
exception(request, response, e);
servlet = null;
}

MessageBytes requestPathMB = request.getRequestPathMB();
DispatcherType dispatcherType = DispatcherType.REQUEST;
if (request.getDispatcherType()==DispatcherType.ASYNC) dispatcherType = DispatcherType.ASYNC;
request.setAttribute(Globals.DISPATCHER_TYPE_ATTR,dispatcherType);
request.setAttribute(Globals.DISPATCHER_REQUEST_PATH_ATTR,
requestPathMB);
// 过滤链,层层通过过滤器,再层层返回。
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

// Call the filter chain for this request
// NOTE: This also calls the servlet's service() method
try {
if ((servlet != null) && (filterChain != null)) {
// Swallow output if needed
if (context.getSwallowOutput()) {
try {
SystemLogHandler.startCapture();
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
//执行过滤链
filterChain.doFilter(request.getRequest(),
response.getResponse());
}
} finally {
String log = SystemLogHandler.stopCapture();
if (log != null && log.length() > 0) {
context.getLogger().info(log);
}
}
} else {
if (request.isAsyncDispatching()) {
request.getAsyncContextInternal().doInternalDispatch();
} else {
filterChain.doFilter
(request.getRequest(), response.getResponse());
}
}

}
} catch (ClientAbortException | CloseNowException e) {
if (container.getLogger().isDebugEnabled()) {
container.getLogger().debug(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
}
throwable = e;
exception(request, response, e);
} catch (IOException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
} catch (UnavailableException e) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
// throwable = e;
// exception(request, response, e);
wrapper.unavailable(e);
long available = wrapper.getAvailable();
if ((available > 0L) && (available < Long.MAX_VALUE)) {
response.setDateHeader("Retry-After", available);
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
sm.getString("standardWrapper.isUnavailable",
wrapper.getName()));
} else if (available == Long.MAX_VALUE) {
response.sendError(HttpServletResponse.SC_NOT_FOUND,
sm.getString("standardWrapper.notFound",
wrapper.getName()));
}
// Do not save exception in 'throwable', because we
// do not want to do exception(request, response, e) processing
} catch (ServletException e) {
Throwable rootCause = StandardWrapper.getRootCause(e);
if (!(rootCause instanceof ClientAbortException)) {
container.getLogger().error(sm.getString(
"standardWrapper.serviceExceptionRoot",
wrapper.getName(), context.getName(), e.getMessage()),
rootCause);
}
throwable = e;
exception(request, response, e);
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString(
"standardWrapper.serviceException", wrapper.getName(),
context.getName()), e);
throwable = e;
exception(request, response, e);
} finally {
// 释放此请求的过滤器链
if (filterChain != null) {
filterChain.release();
}

// 回收到servlet池,取消分配分配的servlet实例
try {
if (servlet != null) {
wrapper.deallocate(servlet);
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.deallocateException",
wrapper.getName()), e);
if (throwable == null) {
throwable = e;
exception(request, response, e);
}
}

// If this servlet has been marked permanently unavailable,
// unload it and release this instance
try {
if ((servlet != null) &&
(wrapper.getAvailable() == Long.MAX_VALUE)) {
wrapper.unload();
}
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
container.getLogger().error(sm.getString("standardWrapper.unloadException",
wrapper.getName()), e);
if (throwable == null) {
exception(request, response, e);
}
}
long t2=System.currentTimeMillis();

long time=t2-t1;
processingTime += time;
if( time > maxTime) maxTime=time;
if( time < minTime) minTime=time;
}
}

一直到找到servlet

实例化servlet

allocate()方法:

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
@Override
public Servlet allocate() throws ServletException {

// If we are currently unloading this servlet, throw an exception
if (unloading) {
throw new ServletException(sm.getString("standardWrapper.unloading", getName()));
}

boolean newInstance = false;

// 如果不是单线程模型,则每次返回相同的实例
if (!singleThreadModel) {
// 必要时加载并初始化我们的实例
if (instance == null || !instanceInitialized) {
synchronized (this) {
if (instance == null) {
try {
if (log.isDebugEnabled()) {
log.debug("Allocating non-STM instance");
}

// 注意:在加载之前,我们不知道Servlet是否实现了SingleThreadModel。
instance = loadServlet();
newInstance = true;
if (!singleThreadModel) {
// For non-STM, increment here to prevent a race condition with unload. Bug 43683, test case
// #3
countAllocated.incrementAndGet();
}
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("standardWrapper.allocate"), e);
}
}
if (!instanceInitialized) {
initServlet(instance);
}
}
}

//单线程模式
if (singleThreadModel) {
if (newInstance) {
// 必须在上述同步之外执行此操作,以防止可能的死锁
synchronized (instancePool) {
//往实例池里放
instancePool.push(instance);
nInstances++;
}
}
} else {
if (log.isTraceEnabled()) {
log.trace(" Returning non-STM instance");
}
// 对于新实例,在创建时计数将增加
if (!newInstance) {
countAllocated.incrementAndGet();
}
return instance;
}
}

synchronized (instancePool) {
while (countAllocated.get() >= nInstances) {
// 如果可能,分配一个新实例,否则等待
if (nInstances < maxInstances) {
try {
instancePool.push(loadServlet());
nInstances++;
} catch (ServletException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("standardWrapper.allocate"), e);
}
} else {
try {
instancePool.wait();
} catch (InterruptedException e) {
// Ignore
}
}
}
if (log.isTraceEnabled()) {
log.trace(" Returning allocated STM instance");
}
countAllocated.incrementAndGet();
return instancePool.pop();
}
}

处理

上面最终通过连接器的适配器执行service:
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
最终可以找到servlet处理。

处理完成,层层返回:

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
//请求响应完成
request.finishRequest();
response.finishResponse();

public void finishResponse() throws IOException {
// Writing leftover bytes
outputBuffer.close();
}

public void close() throws IOException {

if (closed) {
return;
}
if (suspended) {
return;
}

// 如果有字符,就将它们全部刷新到字节缓冲区,计算内容长度(当然,如果所有内容都适合字节缓冲区)。
if (cb.remaining() > 0) {
//数据刷出 并进行字符转换
flushCharBuffer();
}

if ((!coyoteResponse.isCommitted()) && (coyoteResponse.getContentLengthLong() == -1)
&& !coyoteResponse.getRequest().method().equals("HEAD")) {
//如果这没有导致响应提交,则可以计算最终内容长度。只有当这不是头
//因为在这种情况下,不应该有任何机构
//在此处设置值为零将导致显式内容
//正在对响应设置零的长度。
if (!coyoteResponse.isCommitted()) {
coyoteResponse.setContentLength(bb.remaining());
}
}
//数据对象,刷出到outputbuffer
if (coyoteResponse.getStatus() == HttpServletResponse.SC_SWITCHING_PROTOCOLS) {
doFlush(true);
} else {
doFlush(false);
}
closed = true;

//请求应该在响应时已完全读取
//已关闭。进一步读取输入a)是没有意义的,b)真的
//混淆AJP(错误50189),关闭输入缓冲区以防止它们。
//转换为http对象
Request req = (Request) coyoteResponse.getRequest().getNote(CoyoteAdapter.ADAPTER_NOTES);
req.inputBuffer.close();

coyoteResponse.action(ActionCode.CLOSE, null);
}

请求时序图:
20200406144305

总结accept流程

Connector 启动以后会启动一组线程用于不同阶段的请求处理过程,Acceptor、Poller、worker 所在的线程组都维护在 NioEndpoint 中。

Acceptor线程组。用于接受新连接,并将新连接封装一下,选择一个 Poller 将新连接添加到 Poller 的事件队列中,Acceptor线程组是多个线程组成的线程组。
Poller 线程组。用于监听 Socket 事件,当 Socket 可读或可写等等时,将 Socket 封装一下添加到 worker 线程池的任务队列中,Poller线程组是多个线程组成的线程组。
worker 线程组。用于对请求进行处理,包括分析请求报文并创建 Request 对象,调用容器的 pipeline 进行处理,worker线程组是Executor创建的线程池。
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
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {

public void startInternal() throws Exception {
// 创建worker线程组
if ( getExecutor() == null ) {
createExecutor();
}

// Poller线程组由一堆线程组成
pollers = new Poller[getPollerThreadCount()];
for (int i=0; i<pollers.length; i++) {
pollers[i] = new Poller();
Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start();
}

startAcceptorThreads();
}
}
}

public abstract class AbstractEndpoint<S> {
// Acceptor线程组由一堆线程组成
protected final void startAcceptorThreads() {
int count = getAcceptorThreadCount();
acceptors = new Acceptor[count];

for (int i = 0; i < count; i++) {
acceptors[i] = createAcceptor();
String threadName = getName() + "-Acceptor-" + i;
acceptors[i].setThreadName(threadName);
Thread t = new Thread(acceptors[i], threadName);
t.setPriority(getAcceptorThreadPriority());
t.setDaemon(getDaemon());
t.start();
}
}

// worker的线程组由executor创建线程池组成
public void createExecutor() {
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
}
}

请求处理过程

Acceptor接收连接

Acceptor接受的新连接没有立即注册到selector当中,需要先封装成PollerEvent对象后保存至PollerEvent队列当中,Poller对象会消费PollerEvent队列,类似生产消费模型。

Acceptor 在启动后会阻塞在 ServerSocketChannel.accept(); 方法处,当有新连接到达时,该方法返回一个 SocketChannel。

setSocketOptions()方法将 Socket 封装到 NioChannel 中,并注册到 Poller。

一开始就启动了多个 Poller 线程,注册的时候采用轮询选择 Poller 。NioEndpoint 维护了一个 Poller 数组,当一个连接分配给 pollers[index] 时,下一个连接就会分配给 pollers[(index+1)%pollers.length]。

addEvent() 方法会将 Socket 添加到该 Poller 的 PollerEvent 队列中。到此 Acceptor 的任务就完成了。
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
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {

private volatile ServerSocketChannel serverSock = null;
protected class Acceptor extends AbstractEndpoint.Acceptor {

public void run() {

while (running) {
state = AcceptorState.RUNNING;
try {
SocketChannel socket = null;
try {
// 监听socket负责接收新连接
socket = serverSock.accept();
} catch (IOException ioe) {
}

if (running && !paused) {
// 处理接受到的socket对象
if (!setSocketOptions(socket)) {
closeSocket(socket);
}
}
} catch (Throwable t) {
}
}
state = AcceptorState.ENDED;
}
}


protected boolean setSocketOptions(SocketChannel socket) {
try {
socket.configureBlocking(false);
Socket sock = socket.socket();
socketProperties.setProperties(sock);

channel = new NioChannel(socket, bufhandler);
// 注册到Poller当中
getPoller0().register(channel);
} catch (Throwable t) {
}

return true;
}

public Poller getPoller0() {
int idx = Math.abs(pollerRotater.incrementAndGet()) % pollers.length;
return pollers[idx];
}


public class Poller implements Runnable {
public void register(final NioChannel socket) {
socket.setPoller(this);
NioSocketWrapper ka = new NioSocketWrapper(socket, NioEndpoint.this);
r = new PollerEvent(socket,ka,OP_REGISTER);

// 添加PollerEvent队列当中
addEvent(r);
}


private void addEvent(PollerEvent event) {
// 投入到PollerEvent队列当中
events.offer(event);
if ( wakeupCounter.incrementAndGet() == 0 ) selector.wakeup();
}
}

}

Poller处理请求

Poller会消费PollerEvent队列(由Acceptor进行投递),并注册到Selector当中。当注册到Selector的socket数据可读的时候将socket封装成SocketProcessor对象,投递到Executor实现的线程池进行处理。

selector.select(1000)。当 Poller 启动后因为 selector 中并没有已注册的 Channel,所以当执行到该方法时只能阻塞。所有的 Poller 共用一个 Selector,其实现类是 sun.nio.ch.SelectorImpl。

events() 方法通过 addEvent() 方法添加到事件队列中的 Socket 注册到SelectorImpl。这里指的socket是accept过来的请求的socket。

当 Socket 可读时,Poller 才对其进行处理,createSocketProcessor() 方法将 Socket 封装到 SocketProcessor 中,SocketProcessor 实现了 Runnable 接口。worker 线程通过调用其 run() 方法来对 Socket 进行处理。

execute(SocketProcessor) 方法将 SocketProcessor 提交到线程池,放入线程池的 workQueue 中。workQueue 是 BlockingQueue 的实例。
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
public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {

public static class PollerEvent implements Runnable {
private NioChannel socket;
private int interestOps;
private NioSocketWrapper socketWrapper;

public PollerEvent(NioChannel ch, NioSocketWrapper w, int intOps) {
reset(ch, w, intOps);
}

public void run() {
if (interestOps == OP_REGISTER) {
try {
socket.getIOChannel().register(
socket.getPoller().getSelector(), SelectionKey.OP_READ, socketWrapper);
} catch (Exception x) {

}
}
}
}


public class Poller implements Runnable {

public void run() {
while (true) {
// events()负责处理PollerEvent事件并注册到selector当中
hasEvents = events();
keyCount = selector.select(selectorTimeout);

// 处理新接受的socket的读写事件
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();

processKey(sk, attachment);
}
}
}

// 处理读写事件
protected void processKey(SelectionKey sk, NioSocketWrapper attachment) {
if (sk.isReadable()) {
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}

if (!closeSocket && sk.isWritable()) {
if (!processSocket(attachment, SocketEvent.OPEN_WRITE, true)) {
closeSocket = true;
}
}
}
}
}


public abstract class AbstractEndpoint<S> {
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
SocketEvent event, boolean dispatch) {

try {
sc = createSocketProcessor(socketWrapper, event);
Executor executor = getExecutor();
// 注册到Worker的线程池ThreadPoolExecutor。
if (dispatch && executor != null) {
executor.execute(sc);
}
} catch (RejectedExecutionException ree) {
}

return true;
}
}

Worker处理具体请求

当新任务添加到 workQueue(ThreadPoolExecutor)后,workQueue.take()方法会返回一个 Runnable,通常是 SocketProcessor,然后 worker 线程调用 SocketProcessor的run() -> doRun()方法对 Socket 进行处理。

createProcessor() 会创建一个Http11Processor, 它用来解析 Socket,将 Socket 中的内容封装到Request中。注意这个Request是临时使用的一个类,它的全类名是org.apache.coyote.Request。

CoyoteAdapter的postParseRequest()方法封装一下 Request,并处理一下映射关系(从 URL 映射到相应的 Host、Context、Wrapper)。

CoyoteAdapter将 Rquest 提交给 Container(StandardEngine) 处理之前,并将 org.apache.coyote.Request封装到 org.apache.catalina.connector.Request,传递给 Container处理的 Request 是 org.apache.catalina.connector.Request。

connector.getService().getMapper().map(),用来在Mapper中查询 URL 的映射关系。映射关系会保留到 org.apache.catalina.connector.Request 中,Container处理阶段 request.getHost()是使用的就是这个阶段查询到的映射主机,以此类推 request.getContext()、request.getWrapper()都是。

connector.getService().getContainer().getPipeline().getFirst().invoke()会将请求传递到 Container(StandardEngine)处理,至此进入了Engine->Host->Context->Wrapper的处理流程,当然了 Container处理也是在 Worker线程中执行的(也就是说Tomcat处理请求是通过ThreadPoolExecutor的线程池实现的),但是这是一个相对独立的模块,所以单独分出来一节。

Tomcat源码中涉及的设计模式

发表于 2018-09-04 | 分类于 Tomcat

模板方法模式

模板方法模式抽象出某个业务操作公共的流程,将流程分为几个步骤,其中有一些步骤是固定不变的,有一些步骤是变化的,固定不变的步骤通过一个基类来实现,而变化的部分通过钩子方法让子类去实现,这样就实现了对系统中流程的统一化规范化管理。

Tomcat中关于生命周期管理应用了模板方法模式,在一个组件的生命周期中都会涉及到init(初始化),start(启动),stop(停止),destory(销毁),而对于每一个生命周期阶段其实都有固定一些事情要做,比如判断前置状态,设置后置状态,以及通知状态变更事件的监听者等,而这些工作其实是可以固化的,所以Tomcat中就将每个生命周期阶段公共的部分固化,然后通过initInternal,startInternal,stopInternal,destoryInternal这几个钩子方法开放给子类去实现具体的逻辑。

20200412101412

tomcat的所有容器都实现了Lifecycle的生命周期管理接口

基类LifecycleBase实现了Lifecycle接口并实现init、start、stop、destroy等方法。

init()方法内部模板化初始化流程,抽象具体的实现initInternal()方法。
start()方法内部模板化启动流程,抽象具体的实现startInternal()方法。
stop()方法内部模板化停止流程,抽象具体的实现stopInternal()方法。
destroy()方法内部模板化释放流程,抽象具体的实现destroyInternal()方法。

Tomcat状态转移图

20200412103613

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface Lifecycle {

public static final String BEFORE_INIT_EVENT = "before_init";
public static final String AFTER_INIT_EVENT = "after_init";
public static final String START_EVENT = "start";
public static final String BEFORE_START_EVENT = "before_start";
public static final String AFTER_START_EVENT = "after_start";
public static final String STOP_EVENT = "stop";
public static final String BEFORE_STOP_EVENT = "before_stop";
public static final String AFTER_STOP_EVENT = "after_stop";
public static final String AFTER_DESTROY_EVENT = "after_destroy";
public static final String BEFORE_DESTROY_EVENT = "before_destroy";
public static final String PERIODIC_EVENT = "periodic";
public static final String CONFIGURE_START_EVENT = "configure_start";
public static final String CONFIGURE_STOP_EVENT = "configure_stop";

public void addLifecycleListener(LifecycleListener listener);
public LifecycleListener[] findLifecycleListeners();
public void removeLifecycleListener(LifecycleListener listener);
public void init() throws LifecycleException;
public void start() throws LifecycleException;
public void stop() throws LifecycleException;
public void destroy() throws LifecycleException;
}

LifecycleBase基实现类:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
public abstract class LifecycleBase implements Lifecycle {
private static final Log log = LogFactory.getLog(LifecycleBase.class);
private static final StringManager sm = StringManager.getManager(LifecycleBase.class);

/**
* 事件通知的注册生命周期侦听器列表。
*/
private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
/**
* 源组件的当前状态。
*/
private volatile LifecycleState state = LifecycleState.NEW;
//捕获Tomcat启动时的错误,当这个属性为真时,就会抛出LifecycleException错误
private boolean throwOnFailure = true;
public boolean getThrowOnFailure() {
return throwOnFailure;
}
public void setThrowOnFailure(boolean throwOnFailure) {
this.throwOnFailure = throwOnFailure;
}

@Override
public void addLifecycleListener(LifecycleListener listener) {
lifecycleListeners.add(listener);
}

@Override
public LifecycleListener[] findLifecycleListeners() {
return lifecycleListeners.toArray(new LifecycleListener[0]);
}

@Override
public void removeLifecycleListener(LifecycleListener listener) {
lifecycleListeners.remove(listener);
}


/**
* 允许子类触发{@link Lifecycle}事件。
*/
protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}


//初始化
@Override
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}

try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
//真正的初始化方法
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}


/**
* 子类实现此方法以执行所需的任何实例初始化。
*/
protected abstract void initInternal() throws LifecycleException;

@Override
public final synchronized void start() throws LifecycleException {

if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {

if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}

return;
}

if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}

try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
//最终启动
startInternal();
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}


/**
* 子类必须确保在执行此方法期间状态更改为*{@link LifecycleState}STARTING}。
* 更改状态将触发{@link Lifecycle#START_EVENT}事件。
*
* 如果一个组件启动失败,它可能抛出一个{@link LifecycleException},这将导致它的父组件启动失败*,
* 或者它将自己置于错误状态,在这种情况下,将对失败的组件调用{@link#stop()}*,但父组件*将继续正常启动。
*/
protected abstract void startInternal() throws LifecycleException;

@Override
public final synchronized void stop() throws LifecycleException {

if (LifecycleState.STOPPING_PREP.equals(state) || LifecycleState.STOPPING.equals(state) ||
LifecycleState.STOPPED.equals(state)) {

if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStopped", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStopped", toString()));
}

return;
}

if (state.equals(LifecycleState.NEW)) {
state = LifecycleState.STOPPED;
return;
}

if (!state.equals(LifecycleState.STARTED) && !state.equals(LifecycleState.FAILED)) {
invalidTransition(Lifecycle.BEFORE_STOP_EVENT);
}

try {
if (state.equals(LifecycleState.FAILED)) {
// Don't transition to STOPPING_PREP as that would briefly mark the
// component as available but do ensure the BEFORE_STOP_EVENT is
// fired
fireLifecycleEvent(BEFORE_STOP_EVENT, null);
} else {
setStateInternal(LifecycleState.STOPPING_PREP, null, false);
}

stopInternal();

// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
if (!state.equals(LifecycleState.STOPPING) && !state.equals(LifecycleState.FAILED)) {
invalidTransition(Lifecycle.AFTER_STOP_EVENT);
}

setStateInternal(LifecycleState.STOPPED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.stopFail", toString());
} finally {
if (this instanceof Lifecycle.SingleUse) {
// Complete stop process first
setStateInternal(LifecycleState.STOPPED, null, false);
destroy();
}
}
}


/**
* *子类必须确保在执行此方法期间将状态更改为*{@link LifecycleState#STOPPING}。*更改状态将触发{@link Lifecycle#STOP_EVENT}事件。
*/
protected abstract void stopInternal() throws LifecycleException;


@Override
public final synchronized void destroy() throws LifecycleException {
if (LifecycleState.FAILED.equals(state)) {
try {
// Triggers clean-up
stop();
} catch (LifecycleException e) {
// Just log. Still want to destroy.
log.error(sm.getString("lifecycleBase.destroyStopFail", toString()), e);
}
}

if (LifecycleState.DESTROYING.equals(state) || LifecycleState.DESTROYED.equals(state)) {
if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyDestroyed", toString()), e);
} else if (log.isInfoEnabled() && !(this instanceof Lifecycle.SingleUse)) {
// Rather than have every component that might need to call
// destroy() check for SingleUse, don't log an info message if
// multiple calls are made to destroy()
log.info(sm.getString("lifecycleBase.alreadyDestroyed", toString()));
}

return;
}

if (!state.equals(LifecycleState.STOPPED) && !state.equals(LifecycleState.FAILED) &&
!state.equals(LifecycleState.NEW) && !state.equals(LifecycleState.INITIALIZED)) {
invalidTransition(Lifecycle.BEFORE_DESTROY_EVENT);
}

try {
setStateInternal(LifecycleState.DESTROYING, null, false);
destroyInternal();
setStateInternal(LifecycleState.DESTROYED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.destroyFail", toString());
}
}

/**
* 子类实现此方法以执行所需的任何实例销毁。
*/
protected abstract void destroyInternal() throws LifecycleException;

@Override
public LifecycleState getState() {
return state;
}

@Override
public String getStateName() {
return getState().toString();
}


protected synchronized void setState(LifecycleState state) throws LifecycleException {
setStateInternal(state, null, true);
}

protected synchronized void setState(LifecycleState state, Object data)
throws LifecycleException {
setStateInternal(state, data, true);
}

private synchronized void setStateInternal(LifecycleState state, Object data, boolean check)
throws LifecycleException {

if (log.isDebugEnabled()) {
log.debug(sm.getString("lifecycleBase.setState", this, state));
}

if (check) {
// 必须是由一个抽象方法触发的(假设这个类中的代码是正确的)空永远都不是有效的状态
if (state == null) {
invalidTransition("null");
// 无法访问的代码-这里是为了阻止eclipse抱怨方法后面可能有NPE
return;
}

// 任何方法都可以转换为failed startInternal()允许启动准备到启动
// stopInternal()允许停止准备到停止,并且无法停止
if (!(state == LifecycleState.FAILED ||
(this.state == LifecycleState.STARTING_PREP &&
state == LifecycleState.STARTING) ||
(this.state == LifecycleState.STOPPING_PREP &&
state == LifecycleState.STOPPING) ||
(this.state == LifecycleState.FAILED &&
state == LifecycleState.STOPPING))) {
// No other transition permitted
invalidTransition(state.name());
}
}
this.state = state;
String lifecycleEvent = state.getLifecycleEvent();
if (lifecycleEvent != null) {
fireLifecycleEvent(lifecycleEvent, data);
}
}

private void invalidTransition(String type) throws LifecycleException {
String msg = sm.getString("lifecycleBase.invalidTransition", type, toString(), state);
throw new LifecycleException(msg);
}

private void handleSubClassException(Throwable t, String key, Object... args) throws LifecycleException {
setStateInternal(LifecycleState.FAILED, null, false);
ExceptionUtils.handleThrowable(t);
String msg = sm.getString(key, args);
if (getThrowOnFailure()) {
if (!(t instanceof LifecycleException)) {
t = new LifecycleException(msg, t);
}
throw (LifecycleException) t;
} else {
log.error(msg, t);
}
}
}

工厂模式

在Tomcat启动的过程中,调用Connector的startInternal()启动中,启动endpoint后bind的方法,初始化endpoint就是使用了工厂模式创建socket

观察者模式

观察者模式的定义,有多个对象在关注着一个对象,如果这个对象的状态发生了改变,其它依赖(关注)它的对象就会收到通知,然后在接收到通知以后各个对象做出相应的动作。

观察者

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
/** 观察者的事件对象继承了EventObject类
* 用于通知侦听器实现生命周期接口的组件上的事件。
*/
public final class LifecycleEvent extends EventObject {

private static final long serialVersionUID = 1L;

public LifecycleEvent(Lifecycle lifecycle, String type, Object data) {
super(lifecycle);
this.type = type;
this.data = data;
}


/**
* 与此事件关联的事件数据
*/
private final Object data;

private final String type;

public Object getData() {
return data;
}

public Lifecycle getLifecycle() {
return (Lifecycle) getSource();
}

public String getType() {
return this.type;
}
}

/**
* 观察者实现统一的接口LifecycleListener,实现具体的方法lifecycleEvent。
*/
public interface LifecycleListener {
public void lifecycleEvent(LifecycleEvent event);
}

//具体的lifecycleEvent方法
//具体实现以ContextConfig为例,实现了具体的lifecycleEvent方法。
public class ContextConfig implements LifecycleListener {

/**
* 处理关联上下文的事件。
*/
@Override
public void lifecycleEvent(LifecycleEvent event) {

// Identify the context we are associated with
try {
context = (Context) event.getLifecycle();
} catch (ClassCastException e) {
log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
return;
}

// Process the event that has occurred
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
configureStart();
} else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
beforeStart();
} else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
// Restore docBase for management tools
if (originalDocBase != null) {
context.setDocBase(originalDocBase);
}
} else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
configureStop();
} else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
init();
} else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
destroy();
}

}
}

被观察者

被观察者实现接口Lifecycle,实现addLifecycleListener和removeLifecycleListener方法。
容器的基类LifecycleBase实现了被观察者功能,提供List<LifecycleListener> lifecycleListeners保存被观察者。
容器的具体实现当中都是继承LifecycleBase类,所以包含了被观察者的功能。

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
public interface Lifecycle {
public void addLifecycleListener(LifecycleListener listener);
public LifecycleListener[] findLifecycleListeners();
public void removeLifecycleListener(LifecycleListener listener);
}

public abstract class LifecycleBase implements Lifecycle {

private final List<LifecycleListener> lifecycleListeners = new CopyOnWriteArrayList<>();
private volatile LifecycleState state = LifecycleState.NEW;

public void addLifecycleListener(LifecycleListener listener) {
lifecycleListeners.add(listener);
}

public LifecycleListener[] findLifecycleListeners() {
return lifecycleListeners.toArray(new LifecycleListener[0]);
}

public void removeLifecycleListener(LifecycleListener listener) {
lifecycleListeners.remove(listener);
}

protected void fireLifecycleEvent(String type, Object data) {
LifecycleEvent event = new LifecycleEvent(this, type, data);
for (LifecycleListener listener : lifecycleListeners) {
listener.lifecycleEvent(event);
}
}
}

责任链模式

责任链,顾名思义,就是用来处理相关事务责任的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

在tomcat中容器之间,当一个请求过来的时候首先是engine容器接受请求,然后engine容器会把请求传到host容器,host容器又会传到context容器,context容器传到wrapper容器,最后wrapper容器使用适配请求的servlet处理请求。

20200412111304

20200412110748

20200412113158

组成:

  • Valve Valve作为职责链上的每个节点,主要用于处理流到该节点的request对象。

20200412111252

  • Pipeline Pipeline作为职责链对象,主要维护Valve职责链节点对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface Valve {
public Valve getNext();
public void setNext(Valve valve);
public void backgroundProcess();
public void invoke(Request request, Response response)
throws IOException, ServletException;
public boolean isAsyncSupported();
}

public interface Pipeline {
public Valve getBasic();
public void setBasic(Valve valve);
public void addValve(Valve valve);
public Valve[] getValves();
public void removeValve(Valve valve);
public Valve getFirst();
public boolean isAsyncSupported();
public Container getContainer();
public void setContainer(Container container);
public void findNonAsyncValves(Set<String> result);
}

一个Pipeline上面可以有很多Valve,这些Valve存放的方式并非统一存放在Pipeline中,而是像一个链表一个接着一个。当你获取到一个Valve实例的时候,调用getNext()方法即可获取在这个Pipeline上的下个Valve实例。

Pipeline中很多的方法都是操作Valve的,包括获取,设置,移除Valve,getFirst()返回的是Pipeline上的第一个Valve,而getBasic(),setBasic()则是获取/设置基础阀,我们都知道在Pipeline中,每个pipeline至少都有一个阀门,叫做基础阀,而getBasic(),setBasic()则是操作基础阀的。

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
/**
* StandardPipeline标准实现类中我们看到了对Pipeline接口的实现
*/
public class StandardPipeline extends LifecycleBase
implements Pipeline, Contained {

private static final Log log = LogFactory.getLog(StandardPipeline.class);
private static final StringManager sm = StringManager.getManager(Constants.Package);

// ----------------------------------------------------------- Constructors


/**
* Construct a new StandardPipeline instance with no associated Container.
*/
public StandardPipeline() {

this(null);

}


/**
* Construct a new StandardPipeline instance that is associated with the
* specified Container.
*
* @param container The container we should be associated with
*/
public StandardPipeline(Container container) {

super();
setContainer(container);

}


// ----------------------------------------------------- Instance Variables


/**
* The basic Valve (if any) associated with this Pipeline.
*/
protected Valve basic = null;


/**
* The Container with which this Pipeline is associated.
*/
protected Container container = null;


/**
* The first valve associated with this Pipeline.
*/
protected Valve first = null;


// --------------------------------------------------------- Public Methods

@Override
public boolean isAsyncSupported() {
Valve valve = (first!=null)?first:basic;
boolean supported = true;
while (supported && valve!=null) {
supported = supported & valve.isAsyncSupported();
valve = valve.getNext();
}
return supported;
}


@Override
public void findNonAsyncValves(Set<String> result) {
Valve valve = (first!=null) ? first : basic;
while (valve != null) {
if (!valve.isAsyncSupported()) {
result.add(valve.getClass().getName());
}
valve = valve.getNext();
}
}


// ------------------------------------------------------ Contained Methods
@Override
public Container getContainer() {
return this.container;
}


/**
* 设置与此管道关联的容器。
*/
@Override
public void setContainer(Container container) {
this.container = container;
}


@Override
protected void initInternal() {
// NOOP
}


/**
* 组件的start()方法,将first(第一个阀门)赋值给current变量,如果current为空,就将basic(也就是基础阀)赋值给current,
* 接下来如果一个标准的遍历单向链表,调用每个对象的start()方法,最后将组件(pipeline)状态设置为STARTING(启动中)。
*/
@Override
protected synchronized void startInternal() throws LifecycleException {

// Start the Valves in our pipeline (including the basic), if any
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
if (current instanceof Lifecycle)
((Lifecycle) current).start();
current = current.getNext();
}

setState(LifecycleState.STARTING);
}

@Override
protected synchronized void stopInternal() throws LifecycleException {

setState(LifecycleState.STOPPING);

// Stop the Valves in our pipeline (including the basic), if any
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
if (current instanceof Lifecycle)
((Lifecycle) current).stop();
current = current.getNext();
}
}


@Override
protected void destroyInternal() {
Valve[] valves = getValves();
for (Valve valve : valves) {
removeValve(valve);
}
}


/**
* Return a String representation of this component.
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Pipeline[");
sb.append(container);
sb.append(']');
return sb.toString();
}


// ------------------------------------------------------- Pipeline Methods


/**
* <p>Return the Valve instance that has been distinguished as the basic
* Valve for this Pipeline (if any).
*/
@Override
public Valve getBasic() {
return this.basic;
}


/**
* 设置基础阀的方法,这个方法在每个容器的构造函数中调用,
*
* 阀门链表的遍历。
*/
@Override
public void setBasic(Valve valve) {

// 如果已经有基础阀(basic已经有值并且跟要设置的值一样)那么直接return
Valve oldBasic = this.basic;
if (oldBasic == valve)
return;

// 旧的基础阀非空 那么调用其stop方法取消和对应container的关联。(销毁旧的基础阀)
if (oldBasic != null) {
if (getState().isAvailable() && (oldBasic instanceof Lifecycle)) {
try {
((Lifecycle) oldBasic).stop();
} catch (LifecycleException e) {
log.error(sm.getString("standardPipeline.basic.stop"), e);
}
}
if (oldBasic instanceof Contained) {
try {
((Contained) oldBasic).setContainer(null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
}
}
}

// Start the new component if necessary
if (valve == null)
return;
//和Container进行关联
if (valve instanceof Contained) {
((Contained) valve).setContainer(this.container);
}
//启动新的阀门
if (getState().isAvailable() && valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error(sm.getString("standardPipeline.basic.start"), e);
return;
}
}

//遍历阀门链表将新的阀门取代旧的阀门
Valve current = first;
while (current != null) {
if (current.getNext() == oldBasic) {
current.setNext(valve);
break;
}
current = current.getNext();
}
//将基础阀设置为新的阀门
this.basic = valve;

}


/**
* 向容器中添加Valve,在server.xml解析的时候也会调用该方法
*/
@Override
public void addValve(Valve valve) {

// 验证Valve 关联Container
if (valve instanceof Contained)
((Contained) valve).setContainer(this.container);

// 验证组件状态,如果对的话 启动需要添加的Valve,调用start方法。
if (getState().isAvailable()) {
if (valve instanceof Lifecycle) {
try {
((Lifecycle) valve).start();
} catch (LifecycleException e) {
log.error(sm.getString("standardPipeline.valve.start"), e);
}
}
}

//如果 first变量为空,将valve赋值给first变量,并且设置 valve的下一个阀门为基础阀
//之所以这样是因为,如果first为空说明这个容器只有一个基础阀,所以此次添加的阀门肯定是第一个非基础阀阀门
if (first == null) {
first = valve;
valve.setNext(basic);
} else {
//否则 遍历阀门链表,将要被添加的阀门设置在 基础阀之前。
Valve current = first;
while (current != null) {
if (current.getNext() == basic) {
current.setNext(valve);
valve.setNext(basic);
break;
}
current = current.getNext();
}
}
//container触发添加阀门事件
container.fireContainerEvent(Container.ADD_VALVE_EVENT, valve);
}

@Override
public Valve[] getValves() {

List<Valve> valveList = new ArrayList<>();
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
valveList.add(current);
current = current.getNext();
}

return valveList.toArray(new Valve[0]);

}

public ObjectName[] getValveObjectNames() {

List<ObjectName> valveList = new ArrayList<>();
Valve current = first;
if (current == null) {
current = basic;
}
while (current != null) {
if (current instanceof JmxEnabled) {
valveList.add(((JmxEnabled) current).getObjectName());
}
current = current.getNext();
}

return valveList.toArray(new ObjectName[0]);

}

@Override
public void removeValve(Valve valve) {
//如果first 是需要被移除的valve 那么将first的下一个阀门赋值给first,并且current 赋值null,否则current 赋值first
Valve current;
if(first == valve) {
first = first.getNext();
current = null;
} else {
current = first;
}
////遍历阀门链表 查找需要被移除的阀门 如果之前first是被移除的话 current = null是不会进入该循环
while (current != null) {
if (current.getNext() == valve) {
current.setNext(valve.getNext());
break;
}
current = current.getNext();
}
//如果first(此时已经指向下一个阀门)此时 == 基础阀,那么first置空
//first指的是第一个阀门,即使整个container只有一个基础阀门也不会指向基础阀。
//first严格定义是 除了基础阀的第一个阀门。
if (first == basic) first = null;

if (valve instanceof Contained)
//验证需要被移除的阀门 取消container关联
((Contained) valve).setContainer(null);

if (valve instanceof Lifecycle) {
//验证需要被移除的阀门 取消container关联
if (getState().isAvailable()) {
try {
((Lifecycle) valve).stop();
} catch (LifecycleException e) {
log.error(sm.getString("standardPipeline.valve.stop"), e);
}
}
try {
((Lifecycle) valve).destroy();
} catch (LifecycleException e) {
log.error(sm.getString("standardPipeline.valve.destroy"), e);
}
}
//触发container的移除valve事件。
container.fireContainerEvent(Container.REMOVE_VALVE_EVENT, valve);
}


@Override
public Valve getFirst() {
if (first != null) {
return first;
}

return basic;
}
}

每个容器包含职责链对象Pipeline。

每个职责对象Valve的具体实现当中会包含下一个容器对象,相当于Valve对象内部会访问下一个容器,进而实现职责链传递。

整个Http请求被处理的流程:

  • 请求被Connector组件接收,创建Request和Response对象。
  • Connector将Request和Response交给Container,先通过Engine的pipeline组件流经内部的每个Valve。
  • 请求流转到Host的pipeline组件中,并且经过内部Valve的过滤。
  • 请求流转到Context的pipeline组件中,并且经过内部的Valve的过滤。
  • 请求流转到Wrapper的pipeline组件中,并且经过内部的Valve的过滤。
  • Wrapper内部的WrapperValve创建FilterChain实例,调用指定的Servlet实例处理请求。
  • 返回

门面模式

门面模式是对象的结构模式,外部与一个子系统的通信必须通过一个统一的门面对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。

Tomcat中门面模式的例子:

20200412144024

RequestFacade作为Request的门面,内部包含Request对象。

这样的例子有:

ResponseFacade作为Response的门面,内部包含Response对象。
StandardSessionFacade作为HttpSession的门面,内部包含HttpSession对象。
ApplicationContextFacade作为ApplicationContext的门面,内部包含ApplicaitonContext对象。

如Request源码:

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
public class RequestFacade implements HttpServletRequest {

public RequestFacade(Request request) {
this.request = request;
}

protected Request request = null;

public Object getAttribute(String name) {

if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}

return request.getAttribute(name);
}

public Enumeration<String> getAttributeNames() {

if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}

if (Globals.IS_SECURITY_ENABLED){
return AccessController.doPrivileged(
new GetAttributePrivilegedAction());
} else {
return request.getAttributeNames();
}
}

public int getContentLength() {

if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}

return request.getContentLength();
}
}

public class Request implements org.apache.catalina.servlet4preview.http.HttpServletRequest {

private HttpServletRequest applicationRequest = null;

protected RequestFacade facade = null;

public HttpServletRequest getRequest() {
if (facade == null) {
facade = new RequestFacade(this);
}
if (applicationRequest == null) {
applicationRequest = facade;
}

return applicationRequest;
}
}

Tomcat源码分析-加载和启动过程

发表于 2018-09-04 | 分类于 Tomcat

Tomcat启动过程

Tomcat启动类入口是org.apache.catalina.startup.Bootstrap类,里面有main方法,还有静态代码块,初始化运行环境变量:

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
static {
// 获取系统变量
String userDir = System.getProperty("user.dir");


String home = System.getProperty(Globals.CATALINA_HOME_PROP);
File homeFile = null;
// 获取安装目录
if (home != null) {
File f = new File(home);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}

if (homeFile == null) {
// First fall-back. See if current directory is a bin directory
// in a normal Tomcat install
File bootstrapJar = new File(userDir, "bootstrap.jar");

if (bootstrapJar.exists()) {
File f = new File(userDir, "..");
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
}

if (homeFile == null) {
// Second fall-back. Use current directory
File f = new File(userDir);
try {
homeFile = f.getCanonicalFile();
} catch (IOException ioe) {
homeFile = f.getAbsoluteFile();
}
}
//设置安装目录
catalinaHomeFile = homeFile;
System.setProperty(
Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

// 设置工作目录
String base = System.getProperty(Globals.CATALINA_BASE_PROP);
if (base == null) {
catalinaBaseFile = catalinaHomeFile;
} else {
File baseFile = new File(base);
try {
baseFile = baseFile.getCanonicalFile();
} catch (IOException ioe) {
baseFile = baseFile.getAbsoluteFile();
}
catalinaBaseFile = baseFile;
}
System.setProperty(
Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
}

Main方法:

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
public static void main(String args[]) {
synchronized (daemonLock) {
//创建主类 daemon是守护线程
if (daemon == null) {
// Don't set daemon until init() has completed
//Bootstrap实例化
Bootstrap bootstrap = new Bootstrap();
try {
bootstrap.init();
} catch (Throwable t) {
handleThrowable(t);
t.printStackTrace();
return;
}
daemon = bootstrap;
} else {
// When running as a service the call to stop will be on a new
// thread so make sure the correct class loader is used to
// prevent a range of class not found exceptions.
Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
}
}

try {
String command = "start";
if (args.length > 0) {
command = args[args.length - 1];
}

if (command.equals("startd")) {
args[args.length - 1] = "start";
daemon.load(args);
daemon.start();
} else if (command.equals("stopd")) {
args[args.length - 1] = "stop";
daemon.stop();
} else if (command.equals("start")) {
daemon.setAwait(true);
daemon.load(args);
daemon.start();
if (null == daemon.getServer()) {
System.exit(1);
}
} else if (command.equals("stop")) {
daemon.stopServer(args);
} else if (command.equals("configtest")) {
daemon.load(args);
if (null == daemon.getServer()) {
System.exit(1);
}
System.exit(0);
} else {
log.warn("Bootstrap: command \"" + command + "\" does not exist.");
}
} catch (Throwable t) {
// Unwrap the Exception for clearer error reporting
if (t instanceof InvocationTargetException &&
t.getCause() != null) {
t = t.getCause();
}
handleThrowable(t);
t.printStackTrace();
System.exit(1);
}
}

初始化整个环境

bootstrap.init();

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
public void init() throws Exception {
//初始化类加载器
initClassLoaders();
//设置上下文加载器
Thread.currentThread().setContextClassLoader(catalinaLoader);
//设置安全加载器
SecurityClassLoad.securityClassLoad(catalinaLoader);

// Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
//反射的方式加载org.apache.catalina.startup.Catalina
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();

// 设置共享扩展类加载器
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
//设置父加载器
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
//把实例赋予catalinaDaemon
catalinaDaemon = startupInstance;
}
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
private void load(String[] arguments) throws Exception {

// Call the load() method
String methodName = "load";
Object param[];
Class<?> paramTypes[];
if (arguments==null || arguments.length==0) {
paramTypes = null;
param = null;
} else {
paramTypes = new Class[1];
paramTypes[0] = arguments.getClass();
param = new Object[1];
param[0] = arguments;
}
//反射执行了catalina的methodName(参数传过来)方法
Method method =
catalinaDaemon.getClass().getMethod(methodName, paramTypes);
if (log.isDebugEnabled()) {
log.debug("Calling startup class " + method);
}
method.invoke(catalinaDaemon, param);
}

/**
* load新服务器信息
*/
public void load() {

//看是否load过
if (loaded) {
return;
}
loaded = true;

long t1 = System.nanoTime();
//初始化目录
initDirs();

// Before digester - it may be needed
//初始化命名空间
initNaming();

// 加载 解析 Server.xml文件,生成组件对象。
Digester digester = createStartDigester();

InputSource inputSource = null;
InputStream inputStream = null;
File file = null;
try {
try {
file = configFile();
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail", file), e);
}
}
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream(getConfigFile());
inputSource = new InputSource
(getClass().getClassLoader()
.getResource(getConfigFile()).toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
getConfigFile()), e);
}
}
}

// This should be included in catalina.jar
// Alternative: don't bother with xml, just create it manually.
if (inputStream == null) {
try {
inputStream = getClass().getClassLoader()
.getResourceAsStream("server-embed.xml");
inputSource = new InputSource
(getClass().getClassLoader()
.getResource("server-embed.xml").toString());
} catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("catalina.configFail",
"server-embed.xml"), e);
}
}
}


if (inputStream == null || inputSource == null) {
if (file == null) {
log.warn(sm.getString("catalina.configFail",
getConfigFile() + "] or [server-embed.xml]"));
} else {
log.warn(sm.getString("catalina.configFail",
file.getAbsolutePath()));
if (file.exists() && !file.canRead()) {
log.warn("Permissions incorrect, read permission is not allowed on the file.");
}
}
return;
}

try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
} catch (SAXParseException spe) {
log.warn("Catalina.start using " + getConfigFile() + ": " +
spe.getMessage());
return;
} catch (Exception e) {
log.warn("Catalina.start using " + getConfigFile() + ": " , e);
return;
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
// Ignore
}
}
}
//设置了工作目录和服务
getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

// Stream redirection
initStreams();

// Start the new server
try {
//初始化服务器
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}

long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
}
}

调用LifecyleBase方法初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final synchronized void init() throws LifecycleException {
if (!state.equals(LifecycleState.NEW)) {
invalidTransition(Lifecycle.BEFORE_INIT_EVENT);
}

try {
setStateInternal(LifecycleState.INITIALIZING, null, false);
//真正的初始化容器方法
initInternal();
setStateInternal(LifecycleState.INITIALIZED, null, false);
} catch (Throwable t) {
handleSubClassException(t, "lifecycleBase.initFail", toString());
}
}

initInternal();每个容器继承LifecycleMBeanBase。实现自己的初始化。

20200405112329

初始化各个容器组件

Server->Service->(Engine/Executor/Connector->ProtocolHandler->EndPoint->绑定协议)

这里没有一直初始化Context下去,到Engine就停止,后面的子容器都是在启动的时候初始化的。

初始化Server

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
protected void initInternal() throws LifecycleException {

super.initInternal();
onameStringCache = register(new StringCache(), "type=StringCache");

// Register the MBeanFactory 注册jMX 放到管理器
MBeanFactory factory = new MBeanFactory();
factory.setContainer(this);
onameMBeanFactory = register(factory, "type=MBeanFactory");

// Register the naming resources
globalNamingResources.init();
if (getCatalina() != null) {
ClassLoader cl = getCatalina().getParentClassLoader();
// Walk the class loader hierarchy. Stop at the system class loader.
// This will add the shared (if present) and common class loaders
while (cl != null && cl != ClassLoader.getSystemClassLoader()) {
if (cl instanceof URLClassLoader) {
URL[] urls = ((URLClassLoader) cl).getURLs();
for (URL url : urls) {
if (url.getProtocol().equals("file")) {
try {
File f = new File (url.toURI());
if (f.isFile() &&
f.getName().endsWith(".jar")) {
ExtensionValidator.addSystemResource(f);
}
} catch (URISyntaxException e) {
// Ignore
} catch (IOException e) {
// Ignore
}
}
}
}
cl = cl.getParent();
}
}
// 初始化我们定义的服务 可能有多个。
for (int i = 0; i < services.length; i++) {
services[i].init();
}
}

这个方法又会初始化service

初始化Service

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
@Override
protected void initInternal() throws LifecycleException {

super.initInternal();

//初始化 engine
if (engine != null) {
engine.init();
}

// Initialize any Executors 初始化线程池
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}

// 初始化映射程序侦听器
mapperListener.init();

// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
//初始化连接器
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);

if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}

初始化Service时,也会初始化线程池,Engine和Connector。

初始化Engine

1
2
3
4
5
6
7
@Override
protected void initInternal() throws LifecycleException {
// Ensure that a Realm is present before any attempt is made to start one. This will create the default NullRealm if necessary.
getRealm();
//这里个之前初始化容器不同,前面都是父容器初始化子容器,这里不同,不再初始化后面的容器,后面容器在启动中初始化。
super.initInternal();
}

直接调用父类ContainerBase的方法

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void initInternal() throws LifecycleException {
BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
startStopExecutor = new ThreadPoolExecutor(
getStartStopThreadsInternal(),
getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
startStopQueue,
new StartStopThreadFactory(getName() + "-startStop-"));
startStopExecutor.allowCoreThreadTimeOut(true);
super.initInternal();
}

初始化Connector

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
//连接器初始化
@Override
protected void initInternal() throws LifecycleException {

super.initInternal();

// 初始化适配器
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);

// Make sure parseBodyMethodsSet has a default
if (null == parseBodyMethodsSet) {
setParseBodyMethods(getParseBodyMethods());
}

if (protocolHandler.isAprRequired() && !AprLifecycleListener.isInstanceCreated()) {
throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprListener",
getProtocolHandlerClassName()));
}
if (protocolHandler.isAprRequired() && !AprLifecycleListener.isAprAvailable()) {
throw new LifecycleException(sm.getString("coyoteConnector.protocolHandlerNoAprLibrary",
getProtocolHandlerClassName()));
}
if (AprLifecycleListener.isAprAvailable() && AprLifecycleListener.getUseOpenSSL() &&
protocolHandler instanceof AbstractHttp11JsseProtocol) {
AbstractHttp11JsseProtocol<?> jsseProtocolHandler =
(AbstractHttp11JsseProtocol<?>) protocolHandler;
if (jsseProtocolHandler.isSSLEnabled() &&
jsseProtocolHandler.getSslImplementationName() == null) {
// OpenSSL is compatible with the JSSE configuration, so use it if APR is available
jsseProtocolHandler.setSslImplementationName(OpenSSLImplementation.class.getName());
}
}

try {
//protocolHandler初始化
protocolHandler.init();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
}
}

这里同时初始化了适配器CoyoteAdapter和AbstractProtocol.ProtocolHandler

而ProtocolHandler又会初始化EndPoint监听器

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
@Override
public void init() throws Exception {
if (getLog().isInfoEnabled()) {
getLog().info(sm.getString("abstractProtocolHandler.init", getName()));
}

if (oname == null) {
// Component not pre-registered so register it
oname = createObjectName();
if (oname != null) {
Registry.getRegistry(null, null).registerComponent(this, oname, null);
}
}

if (this.domain != null) {
rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
Registry.getRegistry(null, null).registerComponent(
getHandler().getGlobal(), rgOname, null);
}

String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length()-1));
endpoint.setDomain(domain);

//监听器初始化 来监听protocolHandler
endpoint.init();
}

后面就是绑定协议:

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
public abstract void bind() throws Exception;
public abstract void unbind() throws Exception;
public abstract void startInternal() throws Exception;
public abstract void stopInternal() throws Exception;

public void init() throws Exception {
if (bindOnInit) {
bind();
bindState = BindState.BOUND_ON_INIT;
}
if (this.domain != null) {
// Register endpoint (as ThreadPool - historical name)
oname = new ObjectName(domain + ":type=ThreadPool,name=\"" + getName() + "\"");
Registry.getRegistry(null, null).registerComponent(this, oname, null);

ObjectName socketPropertiesOname = new ObjectName(domain +
":type=ThreadPool,name=\"" + getName() + "\",subType=SocketProperties");
socketProperties.setObjectName(socketPropertiesOname);
Registry.getRegistry(null, null).registerComponent(socketProperties, socketPropertiesOname, null);

for (SSLHostConfig sslHostConfig : findSslHostConfigs()) {
registerJmx(sslHostConfig);
}
}
}

20200405112440

之后就是实例化ServerSocketl了。

启动服务

main方法中,启动daemon.start();其实是启动Catalina的start方法,之后启动服务:

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
public void start() throws Exception {
if (catalinaDaemon == null) {
init();
}
//启动Catalina守护进程。
Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
method.invoke(catalinaDaemon, (Object [])null);
}

//其实就是启动Catalina的start方法:
/**
* Start a new server instance.
* 启动服务器
*/
public void start() {

if (getServer() == null) {
load();
}

if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}

long t1 = System.nanoTime();

// Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}

long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}

// Register shutdown hook
//为了安全关闭
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);

// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}

if (await) {
await();
stop();
}
}

服务启动,会调用LifecycleBean的start方法

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
public final synchronized void start() throws LifecycleException {
//启动后生命周期的不同状态,又有不同的操作。
if (LifecycleState.STARTING_PREP.equals(state) || LifecycleState.STARTING.equals(state) ||
LifecycleState.STARTED.equals(state)) {

if (log.isDebugEnabled()) {
Exception e = new LifecycleException();
log.debug(sm.getString("lifecycleBase.alreadyStarted", toString()), e);
} else if (log.isInfoEnabled()) {
log.info(sm.getString("lifecycleBase.alreadyStarted", toString()));
}

return;
}

if (state.equals(LifecycleState.NEW)) {
init();
} else if (state.equals(LifecycleState.FAILED)) {
stop();
} else if (!state.equals(LifecycleState.INITIALIZED) &&
!state.equals(LifecycleState.STOPPED)) {
invalidTransition(Lifecycle.BEFORE_START_EVENT);
}

try {
setStateInternal(LifecycleState.STARTING_PREP, null, false);
//最终启动的方法。不同的实现,分别启动实现。
startInternal();
if (state.equals(LifecycleState.FAILED)) {
// This is a 'controlled' failure. The component put itself into the
// FAILED state so call stop() to complete the clean-up.
stop();
} else if (!state.equals(LifecycleState.STARTING)) {
// Shouldn't be necessary but acts as a check that sub-classes are
// doing what they are supposed to.
invalidTransition(Lifecycle.AFTER_START_EVENT);
} else {
setStateInternal(LifecycleState.STARTED, null, false);
}
} catch (Throwable t) {
// This is an 'uncontrolled' failure so put the component into the
// FAILED state and throw an exception.
handleSubClassException(t, "lifecycleBase.startFail", toString());
}
}

启动子组件

启动server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
protected void startInternal() throws LifecycleException {
//监听器做相应的处理
fireLifecycleEvent(CONFIGURE_START_EVENT, null);
//设置状态
setState(LifecycleState.STARTING);

globalNamingResources.start();

// Start our defined Services
synchronized (servicesLock) {
for (int i = 0; i < services.length; i++) {
services[i].start();
}
}
}

启动service

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
protected void startInternal() throws LifecycleException {

if(log.isInfoEnabled())
log.info(sm.getString("standardService.start.name", this.name));
setState(LifecycleState.STARTING);

// engine启动
if (engine != null) {
synchronized (engine) {
engine.start();
}
}

//启动线程池
synchronized (executors) {
for (Executor executor: executors) {
executor.start();
}
}

//启动mapperListener监听器 主要用于处理映射关系
mapperListener.start();

// 启动连接器 对应是在server.xml中
synchronized (connectorsLock) {
for (Connector connector: connectors) {
try {
// If it has already failed, don't try and start it
if (connector.getState() != LifecycleState.FAILED) {
connector.start();
}
} catch (Exception e) {
log.error(sm.getString(
"standardService.connector.startFailed",
connector), e);
}
}
}
}

启动Engine

1
2
3
4
5
6
7
8
9
10
@Override
protected synchronized void startInternal() throws LifecycleException {

// Log our server identification information
if(log.isInfoEnabled())
log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo());

// Standard container startup
super.startInternal();
}

这里就不在启动子容器,而是执行父类方法。

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
protected synchronized void startInternal() throws LifecycleException {

// Start our subordinate components, if any
logger = null;
getLogger();
//有集群启动集群
Cluster cluster = getClusterInternal();
if (cluster instanceof Lifecycle) {
((Lifecycle) cluster).start();
}
Realm realm = getRealmInternal();
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}

// 启动子容器 使用Future同步执行
Container children[] = findChildren();
List<Future<Void>> results = new ArrayList<>();
for (int i = 0; i < children.length; i++) {
results.add(startStopExecutor.submit(new StartChild(children[i])));
}

MultiThrowable multiThrowable = null;

for (Future<Void> result : results) {
try {
result.get();
} catch (Throwable e) {
log.error(sm.getString("containerBase.threadedStartFailed"), e);
if (multiThrowable == null) {
multiThrowable = new MultiThrowable();
}
multiThrowable.add(e);
}

}
if (multiThrowable != null) {
throw new LifecycleException(sm.getString("containerBase.threadedStartFailed"),
multiThrowable.getThrowable());
}
//直到所有的子组件启动完成

// 启动我们管道中的阀门(包括基本阀门)pipeline
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}

//设置生命周期
setState(LifecycleState.STARTING);

// Start our thread
threadStart();
}

启动Connector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Connector(String protocol) {
setProtocol(protocol);
// 实例化协议处理程序
ProtocolHandler p = null;
try {
//org.apache.coyote.http11.Http11NioProtocol
Class<?> clazz = Class.forName(protocolHandlerClassName);
p = (ProtocolHandler) clazz.getConstructor().newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
} finally {
this.protocolHandler = p;
}

if (Globals.STRICT_SERVLET_COMPLIANCE) {
uriCharset = StandardCharsets.ISO_8859_1;
} else {
uriCharset = StandardCharsets.UTF_8;
}
}

连接器构造方法中实例化了一个ProtocolHandler协议处理器,用来处理请求。然后在启动方法中启动协议处理器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void startInternal() throws LifecycleException {

// Validate settings before starting
if (getPort() < 0) {
throw new LifecycleException(sm.getString(
"coyoteConnector.invalidPort", Integer.valueOf(getPort())));
}

setState(LifecycleState.STARTING);

try {
protocolHandler.start();
} catch (Exception e) {
throw new LifecycleException(
sm.getString("coyoteConnector.protocolHandlerStartFailed"), e);
}
}

这些容器启动流程如下:
20200405225231

启动子容器

Host

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
@Override
protected synchronized void startInternal() throws LifecycleException {

// 错误处理
String errorValve = getErrorReportValveClass();
if ((errorValve != null) && (!errorValve.equals(""))) {
try {
boolean found = false;
// Pipeline职责链
Valve[] valves = getPipeline().getValves();
for (Valve valve : valves) {
if (errorValve.equals(valve.getClass().getName())) {
found = true;
break;
}
}
if(!found) {
Valve valve =
(Valve) Class.forName(errorValve).getConstructor().newInstance();
getPipeline().addValve(valve);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString(
"standardHost.invalidErrorReportValveClass",
errorValve), t);
}
}
super.startInternal();
}

继续调用父类的startInternal

分层调用,子容器再掉父类startInternal,直到Context->Wrapper->没有子容器。结束

1
2
//设置生命周期 后会激发监听器。
setState(LifecycleState.STARTING);

激活监听器后,回执行HostConfig中执行start

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
public void start() {

if (log.isDebugEnabled())
log.debug(sm.getString("hostConfig.start"));

//注册事件
try {
ObjectName hostON = host.getObjectName();
oname = new ObjectName
(hostON.getDomain() + ":type=Deployer,host=" + host.getName());
Registry.getRegistry(null, null).registerComponent
(this, oname, this.getClass().getName());
} catch (Exception e) {
log.warn(sm.getString("hostConfig.jmx.register", oname), e);
}
//主机处理
if (!host.getAppBaseFile().isDirectory()) {
log.error(sm.getString("hostConfig.appBase", host.getName(),
host.getAppBaseFile().getPath()));
host.setDeployOnStartup(false);
host.setAutoDeploy(false);
}

//部署
if (host.getDeployOnStartup())
deployApps();

}

//为“应用程序根”目录中的任何目录或WAR文件部署应用程序。
protected void deployApps() {

File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
//3中部署方式
// 读取xml文件中Host <Context>
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);

}
//
protected void deployDirectories(File appBase, String[] files) {

if (files == null)
return;
//启动一个线程池
ExecutorService es = host.getStartStopExecutor();
//运用同步的方式去部署。
List<Future<?>> results = new ArrayList<>();

for (int i = 0; i < files.length; i++) {

if (files[i].equalsIgnoreCase("META-INF"))
continue;
if (files[i].equalsIgnoreCase("WEB-INF"))
continue;
File dir = new File(appBase, files[i]);
if (dir.isDirectory()) {
ContextName cn = new ContextName(files[i], false);

if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
continue;

results.add(es.submit(new DeployDirectory(this, cn, dir)));
}
}

for (Future<?> result : results) {
try {
result.get();
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDir.threaded.error"), e);
}
}
}

//使用一个内部类来部署应用程序
private static class DeployDirectory implements Runnable {

private HostConfig config;
private ContextName cn;
private File dir;

public DeployDirectory(HostConfig config, ContextName cn, File dir) {
this.config = config;
this.cn = cn;
this.dir = dir;
}

@Override
public void run() {
config.deployDirectory(cn, dir);
}
}

//部署启动应用
protected void deployDirectory(ContextName cn, File dir) {
long startTime = 0;
// Deploy the application in this directory
if( log.isInfoEnabled() ) {
startTime = System.currentTimeMillis();
log.info(sm.getString("hostConfig.deployDir",
dir.getAbsolutePath()));
}

Context context = null;
File xml = new File(dir, Constants.ApplicationContextXml);
File xmlCopy =
new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");


DeployedApplication deployedApp;
boolean copyThisXml = isCopyXML();
boolean deployThisXML = isDeployThisXML(dir, cn);
//拿到Context 开始部署。
try {
//实例化StandardContext
if (deployThisXML && xml.exists()) {
///解析这个context节点 就是解析xml
synchronized (digesterLock) {
try {
context = (Context) digester.parse(xml);
} catch (Exception e) {
log.error(sm.getString(
"hostConfig.deployDescriptor.error",
xml), e);
context = new FailedContext();
} finally {
digester.reset();
if (context == null) {
context = new FailedContext();
}
}
}

if (copyThisXml == false && context instanceof StandardContext) {
// Host is using default value. Context may override it.
copyThisXml = ((StandardContext) context).getCopyXML();
}

if (copyThisXml) {
Files.copy(xml.toPath(), xmlCopy.toPath());
context.setConfigFile(xmlCopy.toURI().toURL());
} else {
context.setConfigFile(xml.toURI().toURL());
}
} else if (!deployThisXML && xml.exists()) {
// Block deployment as META-INF/context.xml may contain security
// configuration necessary for a secure deployment.
log.error(sm.getString("hostConfig.deployDescriptor.blocked",
cn.getPath(), xml, xmlCopy));
context = new FailedContext();
} else {
context = (Context) Class.forName(contextClass).getConstructor().newInstance();
}

Class<?> clazz = Class.forName(host.getConfigClass());
LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
context.addLifecycleListener(listener);

context.setName(cn.getName());
context.setPath(cn.getPath());
context.setWebappVersion(cn.getVersion());
context.setDocBase(cn.getBaseName());
//解析完成添加到Host中,进而把Context启动
host.addChild(context);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("hostConfig.deployDir.error",
dir.getAbsolutePath()), t);
} finally {
deployedApp = new DeployedApplication(cn.getName(),
xml.exists() && deployThisXML && copyThisXml);

// Fake re-deploy resource to detect if a WAR is added at a later
// point
deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war",
Long.valueOf(0));
deployedApp.redeployResources.put(dir.getAbsolutePath(),
Long.valueOf(dir.lastModified()));
if (deployThisXML && xml.exists()) {
if (copyThisXml) {
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(xmlCopy.lastModified()));
} else {
deployedApp.redeployResources.put(
xml.getAbsolutePath(),
Long.valueOf(xml.lastModified()));
// Fake re-deploy resource to detect if a context.xml file is
// added at a later point
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(0));
}
} else {
// Fake re-deploy resource to detect if a context.xml file is
// added at a later point
deployedApp.redeployResources.put(
xmlCopy.getAbsolutePath(),
Long.valueOf(0));
if (!xml.exists()) {
deployedApp.redeployResources.put(
xml.getAbsolutePath(),
Long.valueOf(0));
}
}
addWatchedResources(deployedApp, dir.getAbsolutePath(), context);
// Add the global redeploy resources (which are never deleted) at
// the end so they don't interfere with the deletion process
addGlobalRedeployResources(deployedApp);
}

deployed.put(cn.getName(), deployedApp);

if( log.isInfoEnabled() ) {
log.info(sm.getString("hostConfig.deployDir.finished",
dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
}
}

Context

前面context部署完成后启动,StandardContext类中的启动方法:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
@Override
protected synchronized void startInternal() throws LifecycleException {

if(log.isDebugEnabled())
log.debug("Starting " + getBaseName());

// Send j2ee.state.starting notification
//发布启动状态
if (this.getObjectName() != null) {
Notification notification = new Notification("j2ee.state.starting",
this.getObjectName(), sequenceNumber.getAndIncrement());
broadcaster.sendNotification(notification);
}

setConfigured(false);
boolean ok = true;

// Currently this is effectively a NO-OP but needs to be called to ensure the NamingResources follows the correct lifecycle
//启动命名空间资源
if (namingResources != null) {
namingResources.start();
}

// Post work directory 工作目录
postWorkDirectory();

// 添加资源组件
/**
* 资源有4种:
* Context.xml中的 <PrePresource></PrePresource>
* WEB-INF/lib
* Jar:<JarResource></JarResource>
* post:<PostResource></PostResource>
*/
if (getResources() == null) { // (1) Required by Loader
if (log.isDebugEnabled())
log.debug("Configuring default Resources");

try {
setResources(new StandardRoot(this));
} catch (IllegalArgumentException e) {
log.error(sm.getString("standardContext.resourcesInit"), e);
ok = false;
}
}
if (ok) {
//启动资源
resourcesStart();
}

//加载webapp 加载器 父类是shareClassLoader
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}

// An explicit cookie processor hasn't been specified; use the default
if (cookieProcessor == null) {
//初始化cookie
cookieProcessor = new Rfc6265CookieProcessor();
}

// 初始化字符集映射器
getCharsetMapper();

// Validate required extensions
boolean dependencyCheck = true;
try {
dependencyCheck = ExtensionValidator.validateApplication
(getResources(), this);
} catch (IOException ioe) {
log.error(sm.getString("standardContext.extensionValidationError"), ioe);
dependencyCheck = false;
}

//检查依赖
if (!dependencyCheck) {
// do not make application available if dependency check fails
ok = false;
}

// 读取“catalina.useNaming”环境变量
String useNamingProperty = System.getProperty("catalina.useNaming");
if ((useNamingProperty != null)
&& (useNamingProperty.equals("false"))) {
useNaming = false;
}

if (ok && isUseNaming()) {
if (getNamingContextListener() == null) {
NamingContextListener ncl = new NamingContextListener();
ncl.setName(getNamingContextName());
ncl.setExceptionOnFailedWrite(getJndiExceptionOnFailedWrite());
addLifecycleListener(ncl);
setNamingContextListener(ncl);
}
}

// 标准容器启动
if (log.isDebugEnabled())
log.debug("Processing standard container startup");


// Binding thread
ClassLoader oldCCL = bindThread();

try {
if (ok) {
// 启动我们的下属组件(如果有)
Loader loader = getLoader();
if (loader instanceof Lifecycle) {
((Lifecycle) loader).start();
}

// since the loader just started, the webapp classloader is now
// created.
//类加载器属性设置
setClassLoaderProperty("clearReferencesRmiTargets",
getClearReferencesRmiTargets());
setClassLoaderProperty("clearReferencesStopThreads",
getClearReferencesStopThreads());
setClassLoaderProperty("clearReferencesStopTimerThreads",
getClearReferencesStopTimerThreads());
setClassLoaderProperty("clearReferencesHttpClientKeepAliveThread",
getClearReferencesHttpClientKeepAliveThread());
setClassLoaderProperty("clearReferencesObjectStreamClassCaches",
getClearReferencesObjectStreamClassCaches());
setClassLoaderProperty("clearReferencesThreadLocals",
getClearReferencesThreadLocals());

// By calling unbindThread and bindThread in a row, we setup the
// current Thread CCL to be the webapp classloader
unbindThread(oldCCL);
oldCCL = bindThread();

// Initialize logger again. Other components might have used it too early, so it should be reset.
logger = null;
getLogger();
//域设置
Realm realm = getRealmInternal();
if(null != realm) {
if (realm instanceof Lifecycle) {
((Lifecycle) realm).start();
}

// Place the CredentialHandler into the ServletContext so
// applications can have access to it. Wrap it in a "safe"
// handler so application's can't modify it.
CredentialHandler safeHandler = new CredentialHandler() {
@Override
public boolean matches(String inputCredentials, String storedCredentials) {
return getRealmInternal().getCredentialHandler().matches(inputCredentials, storedCredentials);
}

@Override
public String mutate(String inputCredentials) {
return getRealmInternal().getCredentialHandler().mutate(inputCredentials);
}
};
context.setAttribute(Globals.CREDENTIAL_HANDLER, safeHandler);
}

// 发送一个生命周期事件,之后,ContextConfig就会启动了 和之前Host套路一致,,调用ContextConfig的webConfig方法
fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

// 启动子容器
for (Container child : findChildren()) {
if (!child.getState().isAvailable()) {
child.start();
}
}

// 启动我们管道中的阀门(包括基本阀门),
if (pipeline instanceof Lifecycle) {
((Lifecycle) pipeline).start();
}

// 获取群集管理器
Manager contextManager = null;
Manager manager = getManager();
if (manager == null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.cluster.noManager",
Boolean.valueOf((getCluster() != null)),
Boolean.valueOf(distributable)));
}
if ((getCluster() != null) && distributable) {
try {
contextManager = getCluster().createManager(getName());
} catch (Exception ex) {
log.error("standardContext.clusterFail", ex);
ok = false;
}
} else {
contextManager = new StandardManager();
}
}

// Configure default manager if none was specified
if (contextManager != null) {
if (log.isDebugEnabled()) {
log.debug(sm.getString("standardContext.manager",
contextManager.getClass().getName()));
}
setManager(contextManager);
}

if (manager!=null && (getCluster() != null) && distributable) {
//let the cluster know that there is a context that is distributable
//and that it has its own manager
getCluster().registerManager(manager);
}
}

if (!getConfigured()) {
log.error(sm.getString("standardContext.configurationFail"));
ok = false;
}

// 获取资源,并且加入到上下文中
if (ok) {
getServletContext().setAttribute
(Globals.RESOURCES_ATTR, getResources());

if (getInstanceManager() == null) {
javax.naming.Context context = null;
if (isUseNaming() && getNamingContextListener() != null) {
context = getNamingContextListener().getEnvContext();
}
Map<String, Map<String, String>> injectionMap = buildInjectionMap(
getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources());
setInstanceManager(new DefaultInstanceManager(context,
injectionMap, this, this.getClass().getClassLoader()));
}
getServletContext().setAttribute(
InstanceManager.class.getName(), getInstanceManager());
InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());

// 创建所需的上下文属性
getServletContext().setAttribute(
JarScanner.class.getName(), getJarScanner());

// Make the version info available
getServletContext().setAttribute(Globals.WEBAPP_VERSION, getWebappVersion());
}

// Set up the context init params
mergeParameters();

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
//创建application context
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}

// Configure and call application event listeners
if (ok) {
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
}
}

// Check constraints for uncovered HTTP methods
// Needs to be after SCIs and listeners as they may programmatically
// change constraints
if (ok) {
checkConstraintsForUncoveredMethods(findConstraints());
}

try {
// Start manager
Manager manager = getManager();
if (manager instanceof Lifecycle) {
((Lifecycle) manager).start();
}
} catch(Exception e) {
log.error(sm.getString("standardContext.managerFail"), e);
ok = false;
}

// Configure and call application filters
if (ok) {
if (!filterStart()) {
log.error(sm.getString("standardContext.filterFail"));
ok = false;
}
}

// Load and initialize all "load on startup" servlets
if (ok) {
if (!loadOnStartup(findChildren())){
log.error(sm.getString("standardContext.servletFail"));
ok = false;
}
}

// Start ContainerBackgroundProcessor thread
super.threadStart();
} finally {
// Unbinding thread
unbindThread(oldCCL);
}

// Set available status depending upon startup success
if (ok) {
if (log.isDebugEnabled())
log.debug("Starting completed");
} else {
log.error(sm.getString("standardContext.startFailed", getName()));
}

startTime=System.currentTimeMillis();

// Send j2ee.state.running notification
if (ok && (this.getObjectName() != null)) {
Notification notification =
new Notification("j2ee.state.running", this.getObjectName(),
sequenceNumber.getAndIncrement());
broadcaster.sendNotification(notification);
}
getResources().gc();

// Reinitializing if something went wrong
if (!ok) {
setState(LifecycleState.FAILED);
} else {
setState(LifecycleState.STARTING);
}
}

上面设置生命周期状态后,启动监听器,会执行ContextConfig中的webConfig()方法:

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
protected void webConfig() {
//解析每个项目的web.xml web3.0后有注解方式,所以有两种,有web.xml的情况,解析它,还要解析注解。
WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
context.getXmlValidation(), context.getXmlBlockExternal());

Set<WebXml> defaults = new HashSet<>();
defaults.add(getDefaultWebXmlFragment(webXmlParser));

WebXml webXml = createWebXml();

// 解析 web.xml
InputSource contextWebXml = getContextWebXmlSource();
if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
ok = false;
}

ServletContext sContext = context.getServletContext();

// 排序

// Step 1. Identify all the JARs packaged with the application and those provided by the container. If any of the application JARs have a web-fragment.xml
// it will be parsed at this point. web-fragment.xml files are ignored for container provided JARs.
Map<String,WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

// Step 2. Order the fragments.
Set<WebXml> orderedFragments = null;
orderedFragments =
WebXml.orderWebFragments(webXml, fragments, sContext);

// Step 3. Look for ServletContainerInitializer implementations
// 锁定servlet容器
if (ok) {
processServletContainerInitializers();
}

if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
// Steps 4 & 5. 把web资源加载进来
processClasses(webXml, orderedFragments);
}

if (!webXml.isMetadataComplete()) {
// Step 6. Merge web-fragment.xml files into the main web.xml
//将web-fragment.xml文件合并到主web.xml中
// file.
if (ok) {
ok = webXml.merge(orderedFragments);
}

// Step 7. Apply global defaults Have to merge defaults before JSP conversion since defaults provide JSP servlet definition.
webXml.merge(defaults);

// Step 8. Convert explicitly mentioned jsps to servlets
if (ok) {
convertJsps(webXml);
}

// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);
}
} else {
webXml.merge(defaults);
convertJsps(webXml);
configureContext(webXml);
}

if (context.getLogEffectiveWebXml()) {
log.info("web.xml:\n" + webXml.toXml());
}

// Always need to look for static resources
//查找打包在jar中的静态资源
// Step 10. Look for static resources packaged in JARs
if (ok) {
// Spec does not define an order. Use ordered JARs followed by remaining JARs
Set<WebXml> resourceJars = new LinkedHashSet<>();
for (WebXml fragment : orderedFragments) {
resourceJars.add(fragment);
}
for (WebXml fragment : fragments.values()) {
if (!resourceJars.contains(fragment)) {
resourceJars.add(fragment);
}
}
processResourceJARs(resourceJars);
// See also StandardContext.resourcesStart() for
// WEB-INF/classes/META-INF/resources configuration
}

// Step 11. Apply the ServletContainerInitializer config to the
// context
if (ok) {
for (Map.Entry<ServletContainerInitializer,
Set<Class<?>>> entry :
initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(
entry.getKey(), null);
} else {
context.addServletContainerInitializer(
entry.getKey(), entry.getValue());
}
}
}
}


protected void processClasses(WebXml webXml, Set<WebXml> orderedFragments) {
// Step 4. Process /WEB-INF/classes for annotations and
// @HandlesTypes matches
Map<String, JavaClassCacheEntry> javaClassCache = new HashMap<>();

if (ok) {
///WEB-INF/classes下所有资源
WebResource[] webResources =
context.getResources().listResources("/WEB-INF/classes");

for (WebResource webResource : webResources) {
// Skip the META-INF directory from any JARs that have been
// expanded in to WEB-INF/classes (sometimes IDEs do this).
if ("META-INF".equals(webResource.getName())) {
continue;
}
//处理注解的资源
processAnnotationsWebResource(webResource, webXml,
webXml.isMetadataComplete(), javaClassCache);
}
}

// Step 5. Process JARs for annotations and
// @HandlesTypes matches - only need to process those fragments we
// are going to use (remember orderedFragments includes any
// container fragments)
if (ok) {
processAnnotations(
orderedFragments, webXml.isMetadataComplete(), javaClassCache);
}

// Cache, if used, is no longer required so clear it
javaClassCache.clear();
}

Wrapper

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
protected synchronized void startInternal() throws LifecycleException {

// Send j2ee.state.starting notification 发送监听通知
if (this.getObjectName() != null) {
Notification notification = new Notification("j2ee.state.starting",
this.getObjectName(),
sequenceNumber++);
broadcaster.sendNotification(notification);
}

// 执行父类方法
super.startInternal();

setAvailable(0L);

// Send j2ee.state.running notification
if (this.getObjectName() != null) {
Notification notification =
new Notification("j2ee.state.running", this.getObjectName(),
sequenceNumber++);
broadcaster.sendNotification(notification);
}

}

public synchronized void load() throws ServletException {
instance = loadServlet();

if (!instanceInitialized) {
initServlet(instance);
}

if (isJspServlet) {
StringBuilder oname = new StringBuilder(getDomain());

oname.append(":type=JspMonitor");

oname.append(getWebModuleKeyProperties());

oname.append(",name=");
oname.append(getName());

oname.append(getJ2EEKeyProperties());

try {
jspMonitorON = new ObjectName(oname.toString());
Registry.getRegistry(null, null).registerComponent(instance, jspMonitorON, null);
} catch (Exception ex) {
log.warn("Error registering JSP monitoring with jmx " + instance);
}
}
}

启动子容器流程如下:
20200405213810

Tomcat源码环境idea配置

发表于 2018-09-01 | 分类于 Tomcat

下载

https://tomcat.apache.org/download-80.cgi 下载源码,解压。

配置环境

新建一个目录,把解压好的文件目录复制进去,新建一个文件,主要把源代码里面的conf复制,其他随意,结果是这个样子:(也可以不用分离,分离开好管理)

20200404225435

在外层目录新建pom文件,让源代码目录成为idea的模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<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>com.huf</groupId>
<artifactId>apache-tomcat</artifactId>
<name>apache-tomcat-source</name>
<version>1.0</version>
<packaging>pom</packaging>
<modules>
<module>apache-tomcat-8.5.53-src</module>
</modules>
</project>

在源码中新建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
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
<?xml version="1.0" encoding="UTF-8"?>
<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>org.apache.tomcat</groupId>
<artifactId>Tomcat8.5.53</artifactId>
<name>Tomcat8.5.53</name>
<version>8.5</version>

<build>
<finalName>Tomcat8.5</finalName>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>

<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.5</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.6.1</version>
</dependency>
</dependencies>
</project>

构建

在idea中打开,自动状态maven项目。

20200404232607

catalina目录是运行环境,构建完成后配置运行环境。

新建配置

20200404230428

配置如下:

20200404232546

1
2
3
4
5
6
7
-Dcatalina.home=catalina
-Dcatalina.base=catalina
-Djava.endorsed.dirs=catalina/endorsed
-Djava.io.tmpdir=catalina/temp
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file=catalina/conf/logging.properties
-Dfile.encoding=UTF-8

启动,开始构建,构建过程如果test报错,把pom中的test移除。

启动成功后,浏览,报错HTTP Status 500 – Internal Server Error

这是因为8以后版本,JSP引擎没有初始化进去,手动增加代码初始化:

org/apache/catalina/startup/ContextConfig.java

1
2
3
webConfig();
//添加初始化JasperInitializer
context.addServletContainerInitializer(new JasperInitializer(), null);

20200404232406
重启后,即可。
20200404232341

上一页1…111213…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4