SpringApplication initialize 方法:
1 | private void initialize(Object[] sources) { |
deduceMainApplicationClass 方法即用于查找 main 方法类,并实例化:
1 | private Class<?> deduceMainApplicationClass() { |
SpringApplication initialize 方法:
1 | private void initialize(Object[] sources) { |
deduceMainApplicationClass 方法即用于查找 main 方法类,并实例化:
1 | private Class<?> deduceMainApplicationClass() { |
SpringApplication initialize 方法:
1 | private void initialize(Object[] sources) { |
setListeners 方法即找到 ApplicationListener 类并实例化
ApplicationListener 是 Spring 框架中事件监听器接口,即 SpringApplicationRunListener 发布通知事件时,由 ApplicationListener 负责接收。具体可以参考 Spring 事件监听机制。
1 | public interface ApplicationListener<E extends ApplicationEvent> extends EventListener { |
ApplicationListener 在 SpringBoot 中包含两块:
spring-boot-x.x.x.RELEASE.jar/META-INF/spring.factories中:1
2
3
4
5
6
7
8
9
10
11# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener
spring-boot-autoconfigure-x.x.x.RELEASE.jar/META-INF/spring.factories中:1
2
3# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
SpringApplication initialize 方法:
1 | private void initialize(Object[] sources) { |
setInitializers 方法即找到 ApplicationContextInitializer 类并实例化
ApplicationContextInitializer 是 Spring 框架中的接口,其作用可以理解为在 ApplicationContext 执行 refresh 之前,调用 initialize() 方法,对 ApplicationContext 做进一步的设置和处理:1
2
3public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
void initialize(C var1);
}
ApplicationContextInitializer 在 SpringBoot 中包含两块:
spring-boot-x.x.x.RELEASE.jar/META-INF/spring.factories中:1
2
3
4
5
6# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.embedded.ServerPortInfoApplicationContextInitializer
spring-boot-autoconfigure-x.x.x.RELEASE.jar/META-INF/spring.factories中:1
2
3
4# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer
SpringApplication 类中含有如下方法:
1 | private boolean deduceWebEnvironment() { |
其中 WEB_ENVIRONMENT_CLASSES 是 SpringApplication 持有的静态属性:1
2private static final String[] WEB_ENVIRONMENT_CLASSES =
new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
SpringApplication 判断是否是 Web 程序的逻辑非常简单,即判断当前类路径下是否包含 Servlet 和 ConfigurableWebApplicationContext 类:1
2
3
4
5
6
7
8public static boolean isPresent(String className, ClassLoader classLoader) {
try {
forName(className, classLoader);
return true;
} catch (Throwable var3) {
return false;
}
}
1 | public static Class<?> forName(String name, ClassLoader classLoader) throws ClassNotFoundException, LinkageError { |
以 WEB_ENVIRONMENT_CLASSES 参数来说,ClassUtils.isPresent(className, (ClassLoader)null) 方法将始终返回true,即默认就是 Web 程序
SpringBoot 启动类示例:1
2
3
4
5
6
7
8
9
10import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
通过 SpringApplication 静态方法 run 执行启动:
1 | public static ConfigurableApplicationContext run(Object source, String... args) { |
调用构造函数:1
2
3
4
5
6
7
8
9public SpringApplication(Object... sources) {
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.initialize(sources);
}
核心方法 initalize :1
2
3
4
5
6
7
8
9
10private void initialize(Object[] sources) {
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
this.webEnvironment = this.deduceWebEnvironment();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
首先:把 DemoApplication 类对象设置到 SpringApplication 持有的 sources 属性中,是个 Set 集合
其次:判断是否是 Web 程序环境
第三:设置初始化器
第四:设置事件监听器
第五:找出程序入口
JIRA 版本:7.3.8
JKD 版本:1.8
MySQL 版本:5.6
JIRA 需要依赖 JDK 和 外部数据库,本文使用 JDK 8 和 MySQL 5.6。
关于这两个的安装方法可参考其他文章,本文具体不详述。
CentOS 6.5 64bit 安装 JDK 7
CentOS 6.5 64bit 安装 MySQL 5.6
1 | CREATE DATABASE jiradb CHARACTER SET utf8 COLLATE utf8_bin; |
1 | GRANT SELECT,INSERT,UPDATE,DELETE,CREATE,DROP,ALTER,INDEX on jiradb.* to 'mysql'@'%' identified by '123456'; |
以 root 用户执行安装。
使安装程序可执行:1
[root@localhost ~]# chmod a+x atlassian-jira-software-7.3.8-x64.bin
运行安装程序:1
2
3
4
5
6
7
8[root@localhost ~]# ./atlassian-jira-software-7.3.8-x64.bin
Unpacking JRE ...
Starting Installer ...
Jan 01, 2018 7:35:35 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
This will install JIRA Software 7.3.8 on your computer.
OK [o, Enter], Cancel [c]
依据步骤提示选择:
首先,选择安装类型:1
2
3Choose the appropriate installation or upgrade option.
Please choose one of the following:
Express Install (use default settings) [1], Custom Install (recommended for advanced users) [2, Enter], Upgrade an existing JIRA installation [3]
这里选择 2。
其次,选择安装目录:1
2Where should JIRA Software be installed?
[/opt/atlassian/jira]
这里选择默认安装目录。
第三,选择数据目录:1
2Default location for JIRA Software data
[/var/atlassian/application-data/jira]
这里选择默认数据目录。
第四步,选择 TCP 端口:1
2
3
4
5Configure which ports JIRA Software will use.
JIRA requires two TCP ports that are not being used by any other
applications on this machine. The HTTP port is where you will access JIRA
through your browser. The Control port is used to startup and shutdown JIRA.
Use default ports (HTTP: 8080, Control: 8005) - Recommended [1, Enter], Set custom value for HTTP and Control ports [2]
这里选择默认的端口。
第五步,安装服务:1
2
3
4
5JIRA can be run in the background.
You may choose to run JIRA as a service, which means it will start
automatically whenever the computer restarts.
Install JIRA as Service?
Yes [y, Enter], No [n]
1 | Please wait a few moments while JIRA Software is configured. |
特别注意:防火墙需要开启 8080 和 8005 端口
将 mysql-connector-java-5.1.27-bin.jar 驱动程序拷贝到 JIRA 的 lib 目录下:1
[root@localhost ~]# cp mysql-connector-java-5.1.27-bin.jar /opt/atlassian/jira/lib/
重启 JIRA 服务:1
[root@localhost ~]# cd /opt/atlassian/jira/bin
JIRA bin 目录下又启用和停用的脚本
而且 JIRA 默认支持中文,可以通过 Language 中进行选择
选择 我将设置它自己:
数据库设置:
设置应用程序属性:
输入 License:
生成使用 License :
设置管理员账户:
设置电子邮件通知:
将破解文件 atlassian-extras-3.2.jar 替换 /opt/atlassian/jira/atlassian-jira/WEB-INF/lib 原 atlassian-extras-3.2.jar
今天是2018年第一天,又开始了新的一年。
暂且设定如下年度小目标:
操作系统:CentOS 6.5 64bit
Sonatype Nexus 版本:nexus-2.14.5-02-bundle
JDK 版本:JDK 1.8
执行 rpm 安装命令:1
[root@localhost ~]# rpm -ivh jdk-8u151-linux-x64.rpm
默认的 JDK 安装目录:1
/usr/java/jdk1.8.0_151
配置 JDK 环境变量:1
[root@localhost ~]# vim /etc/profile
添加如下内容:1
2
3
4#set jdk environment
export JAVA_HOME=/usr/java/jdk1.8.0_151
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
启用配置文件:1
[root@localhost ~]# source /etc/profile
验证 JDK:1
2
3
4[root@localhost ~]# java -version
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
准备安装目录:1
2
3[root@localhost ~]# cd /user/local
[root@localhost local]# mkdir nexus
[root@localhost local]# cd nexus
解压 Sonatype Nexus:1
[root@localhost nexus]# tar –zxvf nexus-2.14.5-02-bundle.tar.gz
解压之后将出现两个目录:1
2[root@localhost nexus]# ls
nexus-2.14.5-02 sonatype-work
前者是 nexus 服务目录,后者是私库目录
Sonatype Nexus 的启动脚本目录位于:1
2[root@localhost bin]# pwd
/usr/local/nexus/nexus-2.14.5-02/bin
Sonatype Nexus 默认不推荐使用 root 用户启动:1
2
3
4
5
6
7[root@localhost bin]# ls
jsw nexus nexus.bat
[root@localhost bin]# ./nexus start
****************************************
WARNING - NOT RECOMMENDED TO RUN AS ROOT
****************************************
If you insist running as root, then set the environment variable RUN_AS_USER=root before running this script.
若想通过 root 用户启动 Sonatype Nexus 需要修改启动脚本 nexus:1
[root@localhost bin]# vim nexus
设置启动脚本中 RUN_AS_USER 内容:1
RUN_AS_USER=root
重新启动 Sonatype Nexus:1
2
3
4
5
6[root@localhost bin]# ./nexus start
****************************************
WARNING - NOT RECOMMENDED TO RUN AS ROOT
****************************************
Starting Nexus OSS...
Started Nexus OSS.
停止 Sonatype Nexus:1
2
3
4
5
6[root@localhost bin]# ./nexus stop
****************************************
WARNING - NOT RECOMMENDED TO RUN AS ROOT
****************************************
Stopping Nexus OSS...
Stopped Nexus OSS.
1 | [root@localhost bin]# ln -s /usr/local/nexus/nexus-2.14.5-02/bin/nexus /etc/init.d/nexus |
以系统服务方式启动 Sonatype Nexus:1
2
3
4
5
6[root@localhost conf]# service nexus start
****************************************
WARNING - NOT RECOMMENDED TO RUN AS ROOT
****************************************
Starting Nexus OSS...
Started Nexus OSS.
Sonatype Nexus 默认使用 Jetty,其配置文件目录:1
/usr/local/nexus/nexus-2.14.5-02/conf
查看配置文件:1
[root@localhost conf]# vim nexus.properties
1 | # Jetty section |
默认使用 8081 端口。
在防火墙中添加 8081 端口:1
[root@localhost ~]# vim /etc/sysconfig/iptables
添加一条规则:1
-A INPUT -m state --state NEW -m tcp -p tcp --dport 8081 -j ACCEPT
重启防火墙服务:1
[root@localhost ~]# service iptables restart
默认的访问地址:1
http://IP:8081/nexus
默认的用户名和密码:1
admin/admin123
本文转自 Edison Xu CQRS 和 Event Sourcing 系列(二):基本概念
微服务架构已经热了有两年了,而且目测会越来越热,除非有更高级的架构出现。相关解释和说明,网上一搜一大堆,我这里就不重复了。一句话概括:
微服务将原来的N个模块,或者说服务,按照适当的边界,从单节点划分成一整个分布式系统中的若干节点上。
原来服务间的交互直接代码级调用,现在则需要通过以下几种方式调用:
前面两种就比较类似,都属于直接调用,好处明显,缺点是请求者必须知道被请求方的地址。现在一般会提供额外的机制,如服务注册、发现等,来提供动态地址,实现负载和动态路由。目前大多数微服务框架都走的这条路子,如当下十分火热的SpringCloud等。
事件驱动的方式,把请求者与被请求者的绑定关系解耦了,但是需要额外提供一个消息队列,请求者直接把消息发送到队列,被请求者监听队列,在获取到与自己有关系的事件时进行处理。主要缺点主要有二:
但无论是哪种方式,都使得传统架构下的事务无法再起到原先的作用了。
事务的作用主要有二:
在传统架构下,无论是DB还是框架所提供的事务操作,都是基于同线/进程的。在微服务所处的分布式框架下,业务操作变成跨进程、跨节点,只能自行实现,而由于节点通信状态的不确定性、节点间生命周期的不统一等,把实现分布式事务的难度提高了很多。
这就是微服务中的一个大难题。
聚合。这个词或许听起来有点陌生,用集合或者组合就好理解点。
1 | A DDD aggregate is a cluster of domain objects that can be treated as a single unit. —— Martin Fowler |
以下图为例:
车、轮子、轮胎构成了一个聚合。其中车是聚合根(AggregateRoot)
Aggregate有两大特征:
具体来说,Aggregate存在于两种形式:
这里Customer不能直接访问Car下面的Tire,只能通过聚合根Car来访问。
不保存对象的最新状态,而是保存对象产生的所有事件。
通过事件回溯(Event Sourcing, ES)得到对象最新的状态
以前我们是在每次对象参与完一个业务动作后把对象的最新状态持久化保存到数据库中,也就是说我们的数据库中的数据是反映了对象的当前最新的状态。而事件溯源则相反,不是保存对象的最新状态,而是保存这个对象所经历的每个事件,所有的由对象产生的事件会按照时间先后顺序有序的存放在数据库中。当我们需要这个对象的最新状态时,只要先创建一个空的对象,然后把和改对象相关的所有事件按照发生的先后顺序从先到后全部应用一遍即可。这个过程就是事件回溯。
因为一个事件就是表示一个事实,事实是不能被磨灭或修改的,所以ES中的事件本身是不可修改的(Immutable),不会有DELETE或UPDATE操作。
ES很明显先天就会有个问题——由于不停的记录Event,回溯获得对象最新状态所需花的时间会与事件的数量成正比,当数据量大了以后,获取最新状态的时间也相对的比较长。
而在很多的逻辑操作中,进行“写”前一般会需要“读”来做校验,所以ES架构的系统中一般会在内存中维护一份对象的最新状态,在启动时进行”预热”,读取所有持久化的事件进行回溯。这样在读对象——也就是Aggregate的最新状态时,就不会因为慢影响性能。
同时,也可以根据一些策略,把一部分的Event合集所产生的状态作为一个snapshot,下次直接从该snapshot开始回溯。
既然需要读,就不可避免的遇到并发问题。
EventSourcing要求对回溯的操作必须是原子性的,具体实现可参照Actor模型。
ActorModel的核心思想是与对象的交互不会直接调用,而是通过发消息。如下图:
每一个Actor都有一个Mailbox,它收到的所有的消息都会先放入Mailbox中,然后Actor内部单线程处理Mailbox中的消息。从而保证对同一个Actor的任何消息的处理,都是线性的,无并发冲突。整个系统中,有很多的Actor,每个Actor都在处理自己Mailbox中的消息,Actor之间通过发消息来通信。
Akka框架就是实现Actor模型的并行开发框架。Actor作为DDD聚合根,最新状态是在内存中。Actor的状态修改是由事件驱动的,事件被持久化起来,然后通过Event Sourcing的技术,还原特定Actor的最新状态到内存。
另外,还有Eventuate,两者的作者是同一人.
CQRS 架构全称是Command Query Responsibility Segregation,即命令查询职责分离,名词本身最早应该是Greg Young提出来的,但是概念却很早就有了。
本质上,CQRS也是一种读写分离的机制,架构图如下:
CQRS把整个系统划分成两块:
接收外部所有的Insert、Update、Delete命令,转化为Command,每一个Command修改一个Aggregate的状态。Command Side的命令通常不需要返回数据。注意:这种“写”操作过程中,可能会涉及“读”,因为要做校验,这时可直接在这一边进行读操作,而不需要再到Query Side去。
接受所有查询请求,直接返回数据。
由于C端与Q端的分离,两端各有一个自己的Repository,可根据不同的特性选取不同的产品,比如C端用RMDB,而Q端选用读取速度更快的NoSQL产品。
使用了CQRS架构,由于读写之间会有延迟,就意味着系统的一致性模型为最终一致性(Eventual Consistency),所以CQRS架构一般用于读比写大很多的场景。
注意:
CQRS并不像SOA、EDA(EventDrivenArchitecture)属于顶级架构,它有自己的局限性,并不适合于一切场景。有些天然适合于CRUD的系统,在评估CQRS所带来的好处与坏处后,认为利大于弊再选取CQRS。所以,通常CQRS只作为一个大系统中某部分功能实现时使用。
从前面的介绍,应该可以发现两者其实并没有直接的关系,但是EventSourcing天然适合CQRS架构的C端的实现。
CQRS/ES整合在一起的架构,优缺点如下:
我们先把实现微服务事务中的主要难点列出来,然后看用CQRS/ES是怎么一一解决的。
必须自己实现事务的统一commit和rollback;
这个是无论哪一种方式,都必须面对的问题。完全逃不掉。在DDD中有一个叫Saga的概念,专门用于统理这种复杂交互业务的,CQRS/ES架构下,由于本身就是最终一致性,所以都实现了Saga,可以使用该机制来做微服务下的transaction治理。
请求幂等
请求发送后,由于各种原因,未能收到正确响应,而被请求端已经正确执行了操作。如果这时重发请求,则会造成重复操作。
CQRS/ES架构下通过AggregateRootId、Version、CommandId三种标识来识别相同command,目前的开源框架都实现了幂等支持。
并发
单点上,CQRS/ES中按事件的先来后到严格执行,内存中Aggregate的状态由单一线程原子操作进行改变。
多节点上,通过EventStore的broker机制,毫秒级将事件复制到其他节点,保证同步性,同时支持版本回退。(Eventuate)
结合的方式很简单,就是把合适的服务变成CQRS/ES架构,然后提供一个统一的分布式消息队列。
每个服务自己内部用的C或Q的Storage完全可以不同,但C端的Storage尽量使用同一个,例如MongoDB、Cansandra这种本身就是HA的,以保证可用性。同时也可以避免大数据分析导数据时需要从不同的库导。
目前,相对成熟的CQRS/ES可用框架有:
| 名称 | 地址 | 语言 | 文档 |
| AxonFramework | http://www.axonframework.org | Java | 比较全,更新及时 |
| Akka Persistence | http://akka.io | Scala .Net | 文档全 |
| Eventuate | http://eventuate.io | Scala | 文档少 |
| ENode | http://github.com/tangxuehua/enode | C# | 博客 |
| Confluent | http://www.confluent.io | Scala | 文档较少 |
url : 请求的地址
responseType : 响应体 body 的包装类型
urlVariables : 参数数组,用于替换 url 中占位符对用的参数
1 | @RequestMapping(value = "/get-for-entity-01") |
而使用参数数组的方式需要在url中指定占位符,占位符中的顺序即参数在数组中的索引:1
2
3
4
5
6
7
8
9@RequestMapping(value = "/get-for-entity-02")
public String getForEntity02() {
ResponseEntity<String> responseEntity =
restTemplate.getForEntity(
"http://DEMO-REST-TEMPLATE-PROVIDER-EUREKA-CLIENT/user/name?userId={1}",
String.class,
"1");
return responseEntity.getBody();
}
支持字符串方式返回响应对象体:1
2
3
4
5
6
7
8
9@RequestMapping(value = "/get-for-entity-03")
public String getForEntity03() {
ResponseEntity<String> responseEntity =
restTemplate.getForEntity(
"http://DEMO-REST-TEMPLATE-PROVIDER-EUREKA-CLIENT/user?userId={1}",
String.class,
"1");
return responseEntity.getBody();
}
支持对象方式返回响应体(SpringMVC 会将对象序列化为JSON):1
2
3
4
5
6
7
8
9@RequestMapping(value = "/get-for-entity-04")
public User getForEntity04() {
ResponseEntity<User> responseEntity =
restTemplate.getForEntity(
"http://DEMO-REST-TEMPLATE-PROVIDER-EUREKA-CLIENT/user?userId={1}",
User.class,
"1");
return responseEntity.getBody();
}
该方式是将请求参数封装进 Map :1
2
3
4
5
6
7
8
9
10
11@RequestMapping(value = "/get-for-entity-05")
public String getForEntity05() {
Map<String, String> params = new HashMap<>(8);
params.put("userId", "1");
ResponseEntity<String> responseEntity =
restTemplate.getForEntity(
"http://DEMO-REST-TEMPLATE-PROVIDER-EUREKA-CLIENT/user/name?userId={userId}",
String.class,
params);
return responseEntity.getBody();
}
该方式是以构建URI的方式传递请求参数:1
2
3
4
5
6
7
8
9
10
11@RequestMapping(value = "/get-for-entity-06")
public String getForEntity06() {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"http://DEMO-REST-TEMPLATE-PROVIDER-EUREKA-CLIENT/user/name?userId={userId}")
.build()
.expand("1")
.encode();
URI uri = uriComponents.toUri();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
return responseEntity.getBody();
}
getForObject 是对 getForEntity 的进一步封装,自动对请求响应 body 内容进行对象转换。
1 | @RequestMapping(value = "/get-for-object-01") |
1 | @RequestMapping(value = "/get-for-object-02") |
1 | @RequestMapping(value = "/get-for-object-03") |
一般直接在 url 中组合参数或者使用 getForObject(String url, Class responseType, Map urlVariables)
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true