Spring源码分析专题 —— IOC容器启动过程(中篇)

声明
1.建议先阅读《Spring源码分析专题 —— 阅读指引》
2.强烈建议阅读过程中要参照调用过程图,每篇都有对应的调用过程图
3.写文不易,转载请标明出处

前言

在上文《Spring源码分析专题 —— IOC容器启动过程(上篇)》中我们已经寻找到 Spring IOC 启动过程的核心方法 refresh() ,本篇我们将详细讲解启动流程 「定位 -> 加载 -> 注册 -> 实例化」 中的加载与注册环节。
(定位的作用是获取到配置文件,通常我们spring的配置文件是 application.xml 或自定义的 spring-xxx.xml ,定位过程的细节不少,而对我们的主流程影响不大,所以关于定位过程将放在下篇补充讲解)

本篇继续使用上篇中的调用过程图☞ IOC容器启动调用过程图.jpg

加载与注册

我们再看一眼 refresh() 方法

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
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
/**
* obtainFreshBeanFactory方法中会调用loadBeanDefinition方法,用于加载bean的定义
*/
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
/** 初始化所有非lazy-init的bean **/
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

在这个 refresh() 方法中,我们首先关注的是 obtainFreshBeanFactory()

1
2
3
4
5
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
/** 实际是调用子类AbstractRefreshableApplicationContext中的refreshBeanFactory方法 **/
refreshBeanFactory();
return getBeanFactory();
}

obtainFreshBeanFactory() 方法中调用了 refreshBeanFactory() ,而这个方法是在 AbstractApplicationContext 的子类 AbstractRefreshableApplicationContext 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
customizeBeanFactory(beanFactory);
/** [note-by-leapmie] 调用子类XmlWebApplicationContext的loadBeanDefinitions方法 **/
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

refreshBeanFactory方法中调用了 loadBeanDefinitions 方法,路线又回到了 XmlWebApplicationContext 容器,loadBeanDefinitions 方法是在 XmlWebApplicationContext 类中实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);

// Configure the bean definition reader with this context's
// resource loading environment.
beanDefinitionReader.setEnvironment(getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
initBeanDefinitionReader(beanDefinitionReader);
/** [note-by-leapmie] 注意传入的beanDefinitionReader是XmlBeanDefinitionReader **/
loadBeanDefinitions(beanDefinitionReader);
}

方法最后是调用了重载的 loadBeanDefinitions 方法,传入的参数是 XmlBeanDefinitionReader 的对象,我们先看一看重载的 loadBeanDefinitions 方法

1
2
3
4
5
6
7
8
9
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
String[] configLocations = getConfigLocations();
if (configLocations != null) {
for (String configLocation : configLocations) {
/** 实际是调用XmlBeanDefinitionReader的loadBeanDefinitions方法 **/
reader.loadBeanDefinitions(configLocation);
}
}
}

可以看到,其实最后是调用 reader 的 loadBeanDefinitions 方法,此处 reader 的类型是 XmlBeanDefinitionReader ,所以我们查看 XmlBeanDefinitionReader 中的 loadBeanDefinitions 方法

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
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
.
.
.
try {
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
/** do开头的方法表示是正真执行处理操作的方法 **/
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}

此处我们关注的是 doLoadBeanDefinitions 方法,有个技巧,在Spring中,以do开头的方法都是最终实际执行逻辑处理的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {

try {
/** [note-by-leapmie] 读取加载配置文件 **/
Document doc = doLoadDocument(inputSource, resource);
/** [note-by-leapmie] 注册Bean **/
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
.
.
.
}

在这里有两个方法值得我们关注,一个是 doLoadDocument ,负责把配置文件读取为 Document 对象,这个方法中包含了「定位」过程的处理逻辑,关于定位过程我们在下篇再详细分析;第二个是 registerBeanDefinitions 方法,这个方法包含了「加载」和「注册」逻辑。

1
2
3
4
5
6
7
8
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
/** [note-by-leapmie] 获取BeanDefinitionDocumentReader,此处获得的对象实际类型为DefaultBeanDefinitionDocumentReader **/
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
/** [note-by-leapmie] 调用Reader的registerBeanDefinitions方法 */
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}

这里关键有两步,第一步是获取 documentReader ,第二步是调用 documentReader 的 registerBeanDefinitions 方法。稍微跟踪一下可知 documentReader 的实际类型是 DefaultBeanDefinitionDocumentReader,所以我们进入到 DefaultBeanDefinitionDocumentReader 的 registerBeanDefinitions 方法

1
2
3
4
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}

按照惯例,干活的是do开头的方法

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
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);

if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
// We cannot use Profiles.of(...) since profile expressions are not supported
// in XML config. See SPR-12458 for details.
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isDebugEnabled()) {
logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}

preProcessXml(root);
/** 解析转换为BeanDefinitions **/
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);

this.delegate = parent;
}

然后是 parseBeanDefinitions 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
/** [note-by-leapmie] 判断元素是否属于默认的Namespace(当标签为<beans>时判断条件为真) **/
if (delegate.isDefaultNamespace(ele)) {
/** [note-by-leapmie] 处理默认的Element **/
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}

isDefaultNamespaces 方法是判断元素是否属于默认的 Namespace ,通过跟踪可知,这个默认的 Namespace 是指 <beans> 标签, 我们知道在spring的配置文件中,对bean的定义是放在 <beans> 标签里边的,所以接下来的 parseDefaultElement 方法则是用于解析 bean 定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
/** 处理<import>标签 **/
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
/** 处理<alias>标签 **/
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
/** 处理<bean>标签 **/
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
/** 处理嵌套的<beans>标签 **/
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}

到这里我们就一目了然了,这很显然就是针对 <beans> 标签中的各种元素进行解析,对于其他标签我们不深究,直接看处理 <bean> 标签的 processBeanDefinition 方法

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
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
/**
* [note-by-leapmie]
* 调用BeanDefinitionParserDelegate的parseBeanDefinitionElement方法
* 返回一个包含BeanDefinition信息的BeanDefinitionHolder实例
* **/
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
/**
* [note-by-leapmie]
* 注册BeanDefinition
* 传入的参数是刚刚获取到的BeanDefinitionHolder对象,再加上DefaultListableBeanFactory对象
* DefaultListableBeanFactory对象的由来需要追溯到AbstractRefreshableApplicationContext的refreshBeanFactory()方法中
* **/
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}

这里有两个非常关键的方法:

  • 一个是 parseBeanDefinitionElement ,这个方法最终会返回一个持有 BeanDefinition 的 BeanDefinitionHolder 实例,我们在上篇开头的结论中已经说了,加载的过程其实就是把bean的定义转换成一个 BeanDefinition 对象,所以 parseBeanDefinitionElement 对应的便是 「加载」 过程;
  • 另一个则是 registerBeanDefinition ,这个方法对应的便是「注册」过程。

接下来我们将分两部分分别讲解 parseBeanDefinitionElement 和 registerBeanDefinition 的内容

1. 加载 (parseBeanDefinitionElement)

bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); 中的 delegate 是 BeanDefinitionParserDelegate 的实例,我们查看 BeanDefinitionParserDelegate 中的 decorateBeanDefinitionIfRequired 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {

.
.
.

/** [note-by-leapmie] 此处调用的parseBeanDefinitionElement方法返回的是BeanDefinition **/
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
.
.
.
/** [note-by-leapmie] 把beanDefinition注入BeanDefinitionHolder中 **/
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}

return null;
}

这个方法先调用另一个重载的 parseBeanDefinitionElement 方法,然后把获得 beanDefinition 传入 BeanDefinitionHolder 。我们看一看重载的 parseBeanDefinitionElement 方法

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
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, @Nullable BeanDefinition containingBean) {

.
.
.

try {
AbstractBeanDefinition bd = createBeanDefinition(className, parent);

/** [note-by-leapmie] 解析bean定义的属性 **/
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));

parseMetaElements(ele, bd);
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());

parseConstructorArgElements(ele, bd);
parsePropertyElements(ele, bd);
parseQualifierElements(ele, bd);

bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));

return bd;
}
.
.
.
return null;
}

这里的parse开头的方法都是对bean定义的属性标签进行解析,例如「name」、「singleton」、「lazy-init」等,大家可以自行深入了解每一个parse方法是如何解析各个属性的,在本文中就不再占用篇幅逐一讲解了。至此我们已经获取到了BeanDefinition的信息,下一步就到「注册」了。

2. 注册 (registerBeanDefinition)

所谓的注册,其实就是把BeanDefintion存储到IOC容器中,我们进入到 registerBeanDefinition 中看一看是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {

// Register bean definition under primary name.
String beanName = definitionHolder.getBeanName();
/** [note-by-leapmie] 此处实际是调用DefaultListableBeanFactory的registerBeanDefinition方法 **/
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

// Register aliases for bean name, if any.
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}

这里最终调用的是 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()) 其中 registry 是 DefaultListableBeanFactory 的实例。

(为什么是 DefaultListableBeanFactory ?感兴趣的可以去追溯一下,给出一点提示,在 AbstractRefreshableApplicationContext 的 refreshBeanFactory() 方法中会创建DefaultListableBeanFactory的实例,并在之后的所有关键方法中都会作为参数传入该实例,保证后续的调用流程中都能获取到该实例)

DefaultListableBeanFactory 的 registerBeanDefinition 方法如下

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 void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {

.
.
.
if (existingDefinition != null) {
.
.
.
}
else {
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// Still in startup registration phase
/**
* 把BeanDefinitionc添加到beanDefinitionMap中
* Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
* 由此可知,IOC的启动过程是先把Bean的定义解析转换为BeanDefiniton,
* 最后存储于IOC容器(DefaultListableBeanFactory是一个IOC容器)的一个Map变量中。
* */
this.beanDefinitionMap.put(beanName, beanDefinition);
/**
* 把所有的Bean名存储于beanDefinitionNames列表中
*/
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}

if (existingDefinition != null || containsSingleton(beanName)) {
resetBeanDefinition(beanName);
}
}

this.beanDefinitionMap.put(beanName, beanDefinition); 这一行是重点,字面意思已经很明显,就是把 beanName 和 beanDefinition 以 key-value 的形式存储于 beanDefinitionMap 中, beanDefinitionMap 的定义如下。

1
2
/** Map of bean definition objects, keyed by bean name. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);

跟踪到这一步我们便可以得出结论:「 BeanDefinition 是存储在 DefaultListableBeanFactory 的一个 Map 数据结构中 」


[目录]
[上一篇]Spring源码分析专题 —— IOC容器启动过程(上篇)
[下一篇]Spring源码分析专题 —— IOC容器启动过程(下篇)



Spring源码分析专题 —— IOC容器启动过程(上篇)

声明
1.建议先阅读《Spring源码分析专题 —— 阅读指引》
2.强烈建议阅读过程中要参照调用过程图,每篇都有对应的调用过程图
3.写文不易,转载请标明出处

前言

关于 IOC 容器启动的内容很多,我将分上中下三篇讲解,其中上篇相对简单,中篇最为复杂,请大家耐心阅读。

  • 上篇 - 主要是相关基础说明和找到分析入口
  • 中篇 - 讲解定位、加载、注册的过程(实例化在依赖注入的章节再讲)
  • 下篇 - 细节补充

调用过程图

由于篇幅问题,此处我只放个缩略图,高清大图请点击链接☞ IOC容器启动调用过程图.jpg
请务必一边对照图片一边阅读文章。

先放结论

此处先放结论,大家稍微记一记,后边将展开详解

  • Spring 的启动流程主要是定位 -> 加载 -> 注册 -> 实例化
    • 定位 - 获取配置文件路径
    • 加载 - 把配置文件读取成 BeanDefinition
    • 注册 - 存储 BeanDefinition
    • 实例化 - 根据 BeanDefinition 创建实例
  • 所谓的IOC容器其实就是 BeanFactory , BeanFactory 是一个接口,有很多对应的实现类
  • IOC容器的关键入口方法是 refresh()
  • Web 应用中使用的容器是 XmlWebApplicationContext ,其类图如下,可以看出最终是一个实现了 BeanFactory 的类

IOC容器源码的入口

我们知道 Spring 框架不仅仅是面向 Web 应用,所以 Spring 中对应不同场景有许多 IOC 容器的实现类,其中有简单的也有复杂的,在此我们跳过简单容器的讲解,直接以我们最熟悉、也是最感兴趣的 Java Web 项目下手,寻找其对应的 IOC 容器实现类,同时一口气寻找到 IOC 容器的关键入口方法 refresh() 。

1. 寻找IOC容器实现类

以下是我们熟知的 SpringMVC 项目中 web.xml 的基础配置,其关键是要配置一个 ContextLoaderListener 和一个 DispatcherServlet

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
<web-app>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

<!-- ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- DispatcherServlet -->
<servlet>
<description>spring mvc servlet</description>
<servlet-name>springMvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<description>spring mvc</description>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMvc</servlet-name>
<url-pattern>*.do</url-pattern>
</servlet-mapping>
</web-app>

我们知道在 Java Web 容器中相关组件的启动顺序是 ServletContext -> listener -> filter -> servlet , listener 是优于 servlet 启动的,所以我们先看一看 ContextLoaderListener 的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext());
}

public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}

根据 Java Web 容器的规范可知,当 Listener 启动时会调用 contextInitialized 方法,而 ContextLoaderListener 中该方法的内容是继续调用 initWebApplicationContext 方法,于是我们再跟踪 initWebApplicationContext
( ContextLoaderListener 是 ContextLoader 的子类,所以其实是调用了父类的 initWebApplicationContext 方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException(
"Cannot initialize context because there is already " +
"a root application context present - check whether " +
"you have multiple ContextLoader* definitions in your web.xml!");
} else {
Log logger = LogFactory.getLog(ContextLoader.class);
servletContext.log("Initializing Spring root WebApplicationContext");
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}

long startTime = System.currentTimeMillis();

try {
if (this.context == null) {
this.context = this.createWebApplicationContext(servletContext);
}
.
.
.
}

此处我们关心的是 createWebApplicationContext 方法

1
2
3
4
5
6
7
8
9
10
11
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {

/** [note-by-leapmie] determineContextClass方法中获取contextClass **/
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
}
/** [note-by-leapmie] 根据contextClass返回实例 */
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

从代码可知,方法中的逻辑主要是调用 determineContextClass 获取 contextClass ,然后根据 contextClass 创建 IOC 容器实例。所以, contextClass 的值将是关键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> determineContextClass(ServletContext servletContext) {
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
.
.
.
}
else {
/**
* [note-by-leapmie]
* defaultStrategies的值是在本类中的static方法中注入的
* 即该类加载过程中defaultStrategies已经被赋值
* 本类的开始部分有static代码块
* **/
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

可以看到, contextClassName 是从 defaultStrategies 中获取的,而关于 defaultStrategies 的赋值需要追溯到 ContextLoader 类中的静态代码块

1
2
3
4
5
6
7
8
9
10
11
12
13
static {
try {
/**
* [note-by-leapmie]
* DEFAULT_STRATEGIES_PATH的值是ContextLoader.properties
*/
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
}
}

defaultStrategies 是从 resource 中获取的参数,而 resource 又是从 DEFAULT_STRATEGIES_PATH 中获取,查看可知 DEFAULT_STRATEGIES_PATH 的值是 ContextLoader.properties ,通过全局查找到ContextLoader.properties文件,其中内容如下

1
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

由此可知, SpringMVC 项目中使用到的 IOC 容器类型是 XmlWebApplicationContext。

2. 寻找关键入口方法refresh()

我们回到 ContextLoader 的 initWebApplicationContext 方法,前边我们说到调用 createWebApplicationContext 方法创建容器,容器创建后我们关注的下一个方法是 configureAndRefreshWebApplicationContext

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 WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
.
.
.
try {
// Store context in local instance variable, to guarantee that
// it is available on ServletContext shutdown.
if (this.context == null) {
/** [note-by-leapmie] 获取SpringIOC容器类型 **/
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
/** [note-by-leapmie] 配置和刷新容器 **/
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
.
.
.
}

configureAndRefreshWebApplicationContext的代码如下

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
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}

wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}

// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

customizeContext(sc, wac);
/** [note-by-leapmie] 调用容器的refresh()方法,此处wac对应的类是XmlWebApplicationContext **/
wac.refresh();
}

在这里我们要关注的是最后一行 wac.refresh() ,意思是调用容器的 refresh() 方法,此处我们的容器是XmlWebApplicationContext,对应的 refresh() 在其父类 AbstractApplicationContext

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
@Override
/** 核心过程 **/
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
/**
* obtainFreshBeanFactory方法中会调用loadBeanDefinition方法,用于加载bean的定义
*/
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
/** 初始化所有非lazy-init的bean **/
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

至此我们已经找到了关键的入口 refresh() ,我们看一下在调用过程图中我们所处的位置

refresh 方法是 Spring IOC 容器启动过程的核心方法,方法中按顺序调用了好几个命名清晰的方法,其对应的都是 IOC 容器启动过程的关键步骤,更多的细节我们将在下一节继续讲解。

话痨一下
大家可能会觉得,在源码分析过程中一个方法中调用了很多方法,例如先执行方法 a() ,再执行方法 b() ,为什么我们直接看方法 b() 而跳过了方法 a() ?
在这里我想说的是,Spring的源码量很庞大,如果每个细节都去了解可能一年过去了都看不完,我们应该先关注大流程,其他的细枝末节可以在了解了大流程后再慢慢深入了解。
至于为什么是看方法 b() 而跳过方法 a() ,这些都是前人总结的经验与心血,在学习过程中我也是跟着别人的步伐在源码中探索,中间有些缺失的路线我也花费大量时间去踩坑,最后绘制了每一份调用过程图。在本专题中我能确保的是,只要跟着我的步伐,你们不会在源码分析的路上迷路。


[目录]
[上一篇]Spring源码分析专题 —— 阅读指引
[下一篇]Spring源码分析专题 —— IOC容器启动过程(中篇)



Spring源码分析专题 —— 阅读指引

声明:本博客中除声明转载的,其余都是原创文章。本人宗旨是不抄袭,不生产垃圾网文,写下的都是个人经验与心血总结,如转载请注明出处。若文中有任何纰漏错误,希望大家直接指出,以免个人愚见误导他人,感激!

阅读源码的意义

  1. 更深入理解框架原理,印象更深刻
  2. 学习优秀的编程风格、编程技巧、设计思想
  3. 解决实际问题,如修复框架中的bug,或是参考框架源码,结合实际业务需求编写一个独有的框架

阅读源码的方法

  1. 首先是要有一定的编程经验。如果连业务代码都写得不流畅那是不建议阅读源码的,因为基础不好的情况下一是阅读困难,二是无法静下心理解,每看两行就会纠结花大量时间在源码上是否值得,感觉不如写多两行业务代码来得有价值。
  2. 要有耐心。一篇关于源码的文章可能需要阅读两三小时以上才能读完,如果没有这个觉悟,可能看到一半就转头去干别的事了。
  3. 先读文档,理解概念,而不是一上来就扎在代码中。要从代码中反推出设计逻辑是非常痛苦的,也是最没意义的,除非实在是没有相关文档,例如研究竞争对手的源码。(文档可以是官方文档也可以是网上的优秀博客文章,主要用于理解设计概念)

本专题阅读指引

  • 本专题涵盖Spring IOC容器、SpringMVC、AOP、事务四大方面,内容较多,要较长时间消化
  • 可下载带有我注释笔记的Spring源码,Spring版本是5.1.2.BUILD-SNAPSHOT,下载地址https://github.com/leapmie/spring-framework-master-note
  • 每个专题都会配有我整理的调用过程图,可以理解为源码追踪地图,建议是一边看调用过程图一边跟踪源码,因为Spring的源码非常复杂,调用层次很深,非常容易在源码中迷路,看到一半就分不清自己身在何处了。

[目录]
[下一篇]Spring源码分析专题 —— IOC容器启动过程(上篇)



Spring源码分析专题——目录

EJB到底是什么

声明:本文转载自https://www.cnblogs.com/strugglion/p/6027318.html

EJB到底是什么?

1. 我们不禁要问,什么是”服务集群”?什么是”企业级开发”?

既然说了EJB 是为了”服务集群”和”企业级开发”,那么,总得说说什么是所谓的”服务集群”和”企业级开发”吧!这个问题其实挺关键的,因为J2EE 中并没有说明白,也没有具体的指标或者事例告诉广大程序员什么时候用EJB 什么时候不用。于是大家都产生一些联想,认为EJB”分布式运算”指得是”负载均衡”提高系统的运行效率。然而,估计很多人都搞错了,这个”服务群集”和”分布式运算”并没有根本解决运行负载的问题,尤其是针对数据库的应用系统。
为什么?

我们先把EJB 打回原形给大家来慢慢分析。

2. 把EJB 掰开了揉碎了

我们把EJB 的概念好好的分析一下,看看能发现些什么蛛丝马迹。

3.1 EJB 概念的剖析

我们先看一下,EJB 的官方解释:
商务软件的核心部分是它的业务逻辑。业务逻辑抽象了整个商务过程的流程,并使用计算机语言将他们实现。

J2EE 对于这个问题的处理方法是将业务逻辑从客户端软件中抽取出来,封装在一个组件中。这个组件运行在一个独立的服务器上,客户端软件通过网络调用组件提供的服务以实现业务逻辑,而客户端软件的功能单纯到只负责发送调用请求和显示处理结果。在J2EE 中,这个运行在一个独立的服务器上,并封装了业务逻辑的组件就是EJB(Enterprise Java Bean)组件。这其中我们主要关注这么几点,我们来逐条剖析:

剖析1:所谓:”业务逻辑”
我们注意到在EJB 的概念中主要提到的就是”业务逻辑”的封装,而这个业务逻辑到底是什么?说的那么悬乎,其实这个所谓的”业务逻辑”我们完全可以理解成执行特定任务的”类”。

剖析2:所谓:”将业务逻辑从客户端软件中抽取出来,封装在组件中……运行在一个服务器上”
既然我们知道了”业务逻辑”的概念就是执行特定任务的”类”,那么,什么叫”从客户端软件中抽取出来”?其实,这个就是把原来放到客户端的”类”,拿出来不放到客户端了,放到一个组件中,并将这个组件放到一个服务器上去运行。

3.2 把EJB 这个概念变成大白话

变成大白话就是,”把你编写的软件中那些需要执行制定的任务的类,不放到客户端软件上了,而是给他打成包放到一个服务器上了”。

3.3 发现问题了

不管是用”八股文”说,还是用大白话说这个EJB 概念都提到了一个词–”客户端软件”。
“客户端软件”?难道EJB 的概念中说的是C/S 软件?
是的,没错!
EJB 就是将那些”类”放到一个服务器上,用C/S 形式的软件客户端对服务器上的”类”进行调用。
快崩溃了吧!
EJB 和JSP 有什么关系?EJB 和JSP 有关系,但是关系还真不怎么大,至多是在JSP 的服务器端调用远端服务上的EJB 类,仅此而已。

4.1 EJB 的最底层究竟是什么

我们揭开了EJB”八股”概念的真谛,那么,再来分析EJB 的底层实现技术,通过底层实
现技术来分析EJB 的工作方式。

4.2 EJB 的实现技术

EJB 是运行在独立服务器上的组件,客户端是通过网络对EJB 对象进行调用的。在Java中,能够实现远程对象调用的技术是RMI,而EJB 技术基础正是RMI。通过RMI 技术,J2EE将EJB 组件创建为远程对象,客户端就可以通过网络调用EJB 对象了。

4.3 看看RMI 是什么东东

在说RMI 之前,需要理解两个名词:
对象的序列化
分布式计算与RPC

名词1:对象的序列化
对象的序列化概念:对象的序列化过程就是将对象状态转换成字节流和从字节流恢复对象。将对象状态转换成字节流之后,可以用java.io 包中的各种字节流类将其保存到文件中,或者通过网络连接将对象数据发送到另一个主机。
上面的说法有点”八股”,我们不妨再用白话解释一下:对象的序列化就是将你程序中实例化的某个类的对象,比如,你自定一个类MyClass,或者任何一个类的对象,将它转换成字节数组,也就是说可以放到一个byte 数组中,这时候,你既然已经把一个对象放到了byte数组中,那么你当然就可以随便处置了它了,用得最多的就是把他发送到网络上远程的计算机上了。如图2 11所示。


名词2:分布式计算与RPC
RPC 并不是一个纯粹的Java 概念,因为在Java 诞生之前就已经有了RPC 的这个概念,RPC是”Remote Procedure Call”的缩写,也就是”远程过程调用”。在Java 之前的大多数编程语言,如,Fortran、C、COBOL 等等,都是过程性的语言,而不是面向对象的。所以,这些编程语言很自然地用过程表示工作,如,函数或子程序,让其在网络上另一台机器上执行。说白了,就是本地计算机调用远程计算机上的一个函数。如图2 12所示。


名词3:二者结合就是RMI
RMI 英文全称是”Remote Method Invocation”,它的中文名称是”远程方法调用”,它就是利用Java 对象序列化的机制实现分布式计算,实现远程类对象的实例化以及调用的方法。说的更清楚些,就是利用对象序列化来实现远程调用,也就是上面两个概念的结合体,利用这个方法来调用远程的类的时候,就不需要编写Socket 程序了,也不需要把对象进行序列化操作,直接调用就行了非常方便。
远程方法调用是一种计算机之间对象互相调用对方函数,启动对方进程的一种机制,使用这种机制,某一台计算机上的对象在调用另外一台计算机上的方法时,使用的程序语法规则和在本地机上对象间的方法调用的语法规则一样。如图2 13所示。

4.4 优点

这种机制给分布计算的系统设计、编程都带来了极大的方便。只要按照RMI 规则设计程序,可以不必再过问在RMI 之下的网络细节了,如:TCP 和Socket 等等。任意两台计算机之间的通讯完全由RMI 负责。调用远程计算机上的对象就像本地对象一样方便。RMI 可将完整的对象作为参数和返回值进行传递,而不仅仅是预定义的数据类型。也就是说,可以将类似Java 哈西表这样的复杂类型作为一个参数进行传递。

4.5 缺点

如果是较为简单的方法调用,其执行效率也许会比本地执行慢很多,即使和远程Socket机制的简单数据返回的应用相比,也会慢一些,原因是,其在网络间需要传递的信息不仅仅包含该函数的返回值信息,还会包含该对象序列化后的字节内容。

4.6 EJB 是以RMI 为基础的

通过RMI 技术,J2EE 将EJB 组件创建为远程对象,EJB 虽然用了RMI 技术,但是却只需要定义远程接口而无需生成他们的实现类,这样就将RMI 技术中的一些细节问题屏蔽了。但不管怎么说,EJB 的基础仍然是RMI,所以,如果你想了解EJB 的原理,只要把RMI的原理搞清楚就行了。你也就弄清楚了什么时候用EJB 什么时候不需要用EJB 了。

5. EJB 中所谓的”服务群集”

既然已经知道了,RMI 是将各种任务与功能的类放到不同的服务器上,然后通过各个服务器间建立的调用规则实现分布式的运算,也就明白EJB 所谓的”服务群集”的概念。就是将原来在一个计算机上运算的几个类,分别放到其他计算机上去运行,以便分担运行这几个类所需要占用的CPU 和内存资源。同时,也可以将不同的软件功能模块放到不同的服务器上,当需要修改某些功能的时候直接修改这些服务器上的类就行了,修改以后所有客户端的软件都被修改了。如图2 14所示。

6. 这种部署难道是无懈可击

图2 14所示的这个”服务群集”看似”无懈可击”,其实是它这个图没有画完整,我们来把这个图画完整,再来看看有什么问题没有。

6.1 瓶颈在数据库端

仔细观察之后,发现这种配置是有瓶颈的,如图2 15所示。


我们看看图2 15的结构图,现在如果想实现各个服务器针对同一个数据库的查询,那么,不管你部署多少个功能服务器,都需要针对一个数据库服务器进行查询操作。也就是说,不管你的”计算”有多么”分布”也同样需要从一台服务器中取得数据。虽然,看起来将各个功能模块分布在不同的服务器上从而分担了各个主计算机的CPU 资源,然而,真正的瓶颈并不在这里,而是,数据库服务器那里。数据库服务器都会非常忙的应付各个服务器的查询及操作请求。
因此,通过这个结构图使我们了解到了EJB 根本不能完全解决负载的问题,因为,瓶颈并不在功能模块的所在位置,而是在数据库服务器这里。

6.2 假如分开数据库,数据共享怎么办

有的读者一定会想到下面的这个应用结构,如图2 16所示。


就是把每一个功能服务器后面都部署一个数据库,这样不就解决了上节所说的问题了吗?是的解决了数据库查询负载的问题,然而又出现了新的问题,就是”数据共享”的问题就又不容易解决了。

6.3 网络面临较大压力,让你的应用慢如老牛

我们再向前翻看看如图2 15所示的这种架构中存在两个网络,一个是”A 网”一个是”B网”,这两个网络是不同的。”B 网”往往是局域网,一般带宽是10M/100M,速度较快,因此到还好说,然而,”A 网”往往是互联网或者是利用电信网络互联VPN 网或称广域网。”A 网”的特点是带宽一般较窄,如ADSL 的网络仅仅有512K-2M 的带宽,由于广域网互联的成本较高,所以一般不会有较高的带宽。而在这个网络上恰恰跑的是功能模块和客户端软件之间交换的数据,而这部分数据恰恰优势非常占用带宽的。因此,这个应用架构其运行速度可以想见是多么的慢了。说句不夸张的话,有点想老牛拉破车一样的慢。一个如老牛的系统:目前在中国互联网做运营商网络管理系统的一个大公司,它的一个早期的网管软件就是采用了这种架构来做的C/S 结构的应用系统。有一次,我作为评估者来对其应用系统进行评估,将其部署到一个非运营商大型的网络中的时候,便出现了我们上述描述的情况,速度已经到了难以忍受的地步,打开一个流量图,有时候需要用15分钟的时间才能呈现完整。然而,该系统在开发阶段并没有发现这个问题,为什么呢?因为,他们没有考虑到应用的实际用户连接网络的复杂性,从而给该公司造成较大损失,以至于,这个开发架构被最终遗弃。

7. EJB 活学活用,J2EE 不是必须使用EJB

通过上面小节的讲解似乎好像EJB 和开发Web 应用的B/S 结构的系统关系并不大,其实倒也不然。我们如果把”客户端程序”理解成某一台服务器,这样也是可以被应用的,而且,如果是服务器互相之间做EJB 的调用的话,也就不存在广域网带宽限制的问题了。
但是,如下情况尽量就不要使用EJB 了:
1、较为简单的纯Web 应用开发,不需要用EJB。
2、需要与其他服务程序配合使用的应用,但调用或返回的自定义的网络协议可以解决的应用程序,不需要使用EJB。
3、较多人并发访问的C/S 结构的应用程序,尽量不要使用EJB。

总结

a.EJB实现原理: 就是把原来放到客户端实现的代码放到服务器端,并依靠RMI进行通信。

b.RMI实现原理 :就是通过Java对象可序列化机制实现分布计算。

c.服务器集群: 就是通过RMI的通信,连接不同功能模块的服务器,以实现一个完整的功能。


再次声明:本文转载自https://www.cnblogs.com/strugglion/p/6027318.html


个人见解

技术总是在一个轮回中不断行进,现金的EJB被大家贴上的标签就是已经淘汰的落后技术,只有老旧的银行、企业系统仍在使用,但所谓落后的EJB只是变成了现在流行的所谓微服务。技术会不断的迭代升华,名字会不断的变更,但核心技术是永久留存的,如RMI的概念在分布式互联网应用占有重要地位,我们应该踏踏实实把底层技术学好。


IDEA设置注释模板最佳实践

效果

在方法上输入/**,然后按tab键,生成的效果如下
1536055768001.jpg

配置步骤

1. 在Live Templates中添加模板组,命名随意,主要是为了存放自定义的模板,方便管理

tapd_20988451_base64_1536053182_18.png

2. 在模板组下添加一个模板,具体配置如下

1. Abbreviation 填写 *

(注:曾经我填写的是/** ,发现拦截/**会导致某些情况下无法获取对应参数的)
1536054663460.jpg

2. Template text 填写

1
2
3
4
5
6
7
* 
*
*
$param$
* @author XxxYyyZzz
* @date $date$
*/

3. 点击Edit variables,date字段选择date(),param中填写以下内容

1
groovyScript("def result=''; def params=\"${_1}\".replaceAll('[\\\\[|\\\\]|\\\\s]', '').split(',').toList(); for(i = 0; i < params.size(); i++) {result+='* @param ' + params[i] + ((i < params.size() - 1) ? '\\n ' : '')};return result", methodParameters())

1536054939594.jpg

4. 在Expand with中选择tab,然后点击Apply即可

(也可选择其他按键,表示触发的按键,个人比较喜欢用tab,避免与原有按键冲突)
1536055074276.jpg


精品汇总
weblogic虚拟路径配置

前言

weblogic的虚拟路径配置有两种:

约定

详细配置

  1. 在/var/upload目录下创建WEB-INF目录,然后添加

    1. web.xml 内容如下
      1
      2
      3
          <web-app></web-app>
      ```
      2. `weblogic.xml` 内容如下
      /upload
      1
      2. 修改domain下的config/config.xml,增加如下内容,然后启动服务即可
      upload yourserver war /var/upload/ 100 DDOnly nostage false ```
  2. 配置详解

    1. name 部署名称,无特殊作用
    2. target 填写为实际部署的服务名称
    3. source-path 需要映射的物理路径
    4. staging-mode 重点!!!此处必需设为nostage,表示不复制到webloigc的stage目录下
    5. 其他配置项均为常规配置项
  3. 其他注意事项

    1. (如果是在界面上配置部署,生成的默认配置是<staging-mode xsi:nil="true"></staging-mode>,要修改为nostage时务必去掉xsi:nil="true",因为xsi:nil="true"的意思是表示标签中的内容为空)

附 关于stage

weblogic有三种部署模式

  1. nostage
    不把项目复制到stage下,服务直接读取指定路径作为部署内容
  2. stage
    把部署项目复制到服务的stage目录下
  3. External_Stage
    借助第三方工具部署项目

参考

https://blog.csdn.net/wolf863292/article/details/7615323

https://blog.csdn.net/ahhsxy/article/details/6873542


weblogic安装(无界面静默安装)

一、环境准备

1. 用户准备

Generic通用版weblogic不能用ROOT用户安装,如无其他用户需先创建用户,创建用户步骤此处略过

2. 下载weblogic

在官网下载weblogic,将下载好的jar包上传到linux服务器上

3. JDK环境

如果原来已配置好JDK环境可跳过该步骤
步骤

  1. 编辑器打开.bash_profile文件
    1
    vi ~/.bash_profile
  2. 填写jdk相关环境变量
    1
    2
    3
    export JAVA_HOME=/usr/local/jdk1.7.0_80
    export CLASSPATH=.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
    export PATH=$JAVA_HOME/bin:$PATH
  3. 使配置生效
    source ~/.bash_profile

    二、安装

    0. 约定

  4. 本例中安装路径为/usr/local/weblogic12c,可根据自己情况修改
    最终目录结构为
    1
    2
    3
    4
    /usr/local/weblogic12
    |--bea // 程序目录
    |--user_projects // 域集合
    |--xxxDomain // 域
    注:weblogic下可以创建多个域,很多教程创建域时目录命名为domain,与bea同级,如此不利于多个域的管理,所以把域归集到user_projects目录下,一个项目对应一个域
  5. 用户 weblogic
    用户组 weblgoicgroup

    1. 创建oraInst.loc文件

    注:文件路径无所谓,注意修改inst_group为实际的组名
    1
    vi oraInst.loc
    1
    2
    inventory_loc=/home/weblogic/oraInventory
    inst_group=weblgoicgroup

    安装程序使用Oracle清单目录来跟踪安装在计算机上的所有Oracle产品。清单目录中存放的文件名为orainst.loc。如果该文件不存在于您的系统上,则必须在启动静默安装之前创建该文件。安装程序使用此文件。
    用你想要安装程序创建清单目录的完整路径目录替换oui_inventory_directory。然后,用其成员有这个目录的写权限的组的名称替换oui_install_group。

    2. 创建响应文件wls.rsp

    注:文件路径无所谓,注意修改ORACLE_HOME
    1
    vi wls.rsp
    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
    [ENGINE]
    #DO NOT CHANGE THIS.
    Response File Version=1.0.0.0.0
    [GENERIC]
    #The oracle home location. This can be an existing Oracle Home or a new Oracle Home
    ORACLE_HOME=/usr/local/weblogic12c/bea
    #Set this variable value to the Installation Type selected. e.g. WebLogic Server, Coherence, Complete with Examples.
    INSTALL_TYPE=WebLogic Server
    #Provide the My Oracle Support Username. If you wish to ignore Oracle Configuration Manager configuration provide empty string for user name.
    MYORACLESUPPORT_USERNAME=
    #Provide the My Oracle Support Password
    MYORACLESUPPORT_PASSWORD=<SECURE VALUE>
    #Set this to true if you wish to decline the security updates. Setting this to true and providing empty string for My Oracle Support username will ignore the Oracle Configuration Manager configuration
    DECLINE_SECURITY_UPDATES=true
    #Set this to true if My Oracle Support Password is specified
    SECURITY_UPDATES_VIA_MYORACLESUPPORT=false
    #Provide the Proxy Host
    PROXY_HOST=
    #Provide the Proxy Port
    PROXY_PORT=
    #Provide the Proxy Username
    PROXY_USER=
    #Provide the Proxy Password
    PROXY_PWD=<SECURE VALUE>
    #Type String (URL format) Indicates the OCM Repeater URL which should be of the format [scheme[Http/Https]]://[repeater host]:[repeater port]
    COLLECTOR_SUPPORTHUB_URL=
  6. 安装
    注:注意指定wls.rsp和oraInst.loc路径
    1
    java -jar fmw_12.1.3.0.0_wls.jar -silent -responseFile /usr/local/weblogic12c/wls.rsp -invPtrLoc /usr/local/weblogic12c/oraInst.loc
    安装过程比较久,耐心等待即可

    三、创建域

    0. 准备

  7. 由于Linux下的Java生成随机数规则与Windows的机制不一样,需要修改securerandom.source为/dev/./urandom (原值为/dev/random),否则会出现创建、启动域的时候耗时非常长的问题。(共性问题,请放心更改,如不放心可百度权威解释)
    1
    2
    3
    4
    5
    cd $JAVA_HOME/jre/lib/security
    vi java.security
    securerandom.source=file:/dev/random
    # 修改为
    securerandom.source=file:/dev/./urandom
  8. 配置环境变量
    1
    vi ~/.bash_profile
    1
    export MW_HOME=/usr/local/weblogic12c/bea
    1
    source ~/.bash_profile

    1. 创建域

  9. 创建目录
    1
    mkdir /usr/local/weblogic12c/user_projects/xxxDomain
  10. 执行运行文件
    1
    2
    3
    cd $MW_HOME/wlserver/common/bin
    ./commEnv.sh
    ./wlst.sh
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    wls:/offline> 
    wls:/offline> readTemplate('/usr/local/weblogic12c/bea/wlserver/common/templates/wls/wls.jar')
    wls:/offline/base_domain>cd('Servers/AdminServer')
    wls:/offline/base_domain/Server/AdminServer>set('ListenAddress','')
    wls:/offline/base_domain/Server/AdminServer>set('ListenPort', 7001)
    wls:/offline/base_domain/Server/AdminServer>cd('/')
    wls:/offline/base_domain>cd('Security/base_domain/User/weblogic')
    wls:/offline/base_domain/Security/base_domain/User/weblogic>cmo.setPassword('YourPassword')
    wls:/offline/base_domain/Security/base_domain/User/weblogic>setOption('OverwriteDomain', 'true')
    wls:/offline/base_domain/Security/base_domain/User/weblogic>writeDomain('/usr/local/weblogic12c/user_projects/xxxDomain')
    wls:/offline/domain/Security/domain/User/weblogic>closeTemplate()
    wls:/offline>exit()
    注:注意根据需要修改端口、密码、域路径

    四、启动weblogic

    1
    2
    3
    cd /usr/local/weblogic12c/user_projects/xxxDomain/bin/ -- 进入创建的域目录bin下
    ./startWebLogic.sh -- 后台启动使用nohup ./startWebLogic.sh &
    ./stopWeblogic.sh -- 关闭weblogic
    启动浏览器访问weblogic控制台http://IP:7001/console,用户名默认是weblogic,密码是创建域时设置的。

《Maven实战》笔记
  • (P47)需求用例模板

  • (P67)在 项目B中可以使用元素指定X、Y模块是可选依赖,此时X、Y模块只对当前项目B影响,项目A依赖于项目B的时候X、Y模块将不会被传递;当项目A依赖于项目B的时候,如果实际使用中需要使用X模块则需要在项目A中显式声明X模块依赖,选择使用Y模块时同理。(请留意下一点,不建议使用可选依赖)
    f21944ea-d6a0-41f8-bfc6-e3d15da8bb0e.png

  • (P68)在理想的情况下,是不应该使用可选依赖的,使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,应遵循单一职责性原则。在上例中,更好的做法是为X和Y分别创建一个Maven项目,基于同样的groupId分配不同的artifactId,如com.juvenxu.mvnbook:project-b-x和com.juvenxu.mvnbook:project-b-y。

  • (P79)修改M2_HOME/conf/settings.xml文件可以修改maven的全局配置项,但是建议把该文件复制到/.m2目录下再进行修改,因为如果直接改maven安装目录下的settings.xml文件将会对本机的所有用户影响,而修改/.m2/settings.xml则可以控制只对当前用户生效,避免对其他用户造成不必要的干扰。(个人认为windows系统比较少存在多用户的情况,直接修改M2_HOME/conf/settings.xml也无妨)

  • (P83)【远程仓库调用】大部分远程仓库无需认证就可以访问,但有时候出于安全方面的考虑,我们需要提供认证信息才能访问一些远程数据库。配置认证信息和配置仓库信息不同,仓库信息可以直接配置在POM文件中,但是认证信息必须匹配置在settings.xml文件中。这是因为POM往往被提交到代码仓库中供所有成员访问,而settings.xml一般只放在本机,相对更安全。
    setttings.xml中通过配置认证信息,其中必须与POM中repository元素的id完全一致。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <settings>
    ...
    <servers>
    <id>my-proj</id>
    <username>repo-user</username>
    <password>repo-pwd</password>
    <servers>
    ...
    </settings>
  • (P86)Maven的快照机制

  • (P90)由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven仍将无法访问被镜像仓库,因而无法下载构件

  • (P117)一般来说,一个项目的子模块都应该使用同样的groupId,如果它们一起开发和发布,换应该使用同样的version,此外,它们的artifactId还应该使用一致的前缀,以方便同其他项目区分。

  • (P118)例:account-persist的Java代码位于默认的src/mainjava目录,包含Account.java、AccountPersistServiceImpl.java和AccountPersistException.java四个文件,它们的包名都是com.juvenxu.mvnbook.accoount.persist,该包名与account-persist的groupId com.juvenxu.mvnbook.account及artifactId account-persist对应。

  • (P126)聚合模块与其他模块的目录结构并非一定要是夫子关系。

    • 聚合模块的父子目录结构
      ae480e4e-0a24-4cb3-a9ef-b87f1acfafaa.png
    • 聚合模块的平行目录结构
      65853ead-8f80-43d2-ac51-4220871e3245.png
      如果使用平行目录结构,聚合模块的POM也需要做响应的修改,以指向正确的模块目录:
      1
      2
      3
      4
      <modules>
      <module>../account-email</module>
      <module>../account-persist</module>
      </modules>
  • (P128)作为父模块,其打包类型必须是pom
    由于父模块只为了帮助消除配置的重复,因此它本身不包含除POM之外的项目文件,也就不需要src/main/java/之类的文件夹了

  • (P131)dependencyManagement元素的使用

  • (P137)多模块Maven项目中的聚合与继承其实是两个概念,其目的完全是不同的。前者主要是为了方便快速构建项目,后者主要是为了消除重复配置。
    在现有的实际项目中,往往会出现一个POM既是聚合POM,又是父POM,这么做主要是为了方便。

  • (P145)模块间的依赖关系会将反应堆构成一个有向非循环图,各个模块是该图的节点,依赖关系构成有向边。这个图不允许出现循环,因此,当出现模块A依赖于模块B,而B又依赖于A的情况时,Maven就会报错。

  • (P145)裁剪反应堆

  • (P153)Nexus的仓库列表

  • (P195)Hudson(jenkins)的使用

  • (P231)web项目依赖于servlet-api和jsp-api这两个包,它们为servlet和jsp的编译提供支持。需要注意,这两个依赖的范围是provided,表示它们最终不会被打包至war文件中,这是因为几乎所有Web容器都会提供这两个类库,如果war包中重复出现,就会导致潜在的依赖冲突问题。

  • (P231)配置可以指定war包名称

  • (P246)Maven的版本号定义约定如下:
    <主版本>.<次版本>.<增量版本>-<里程碑版本>
    如1.3.4-beta-2,1表示该版本是第一个重大版本;3表示这是基于重大版本的第三个次要版本;4表示该次要版本的第四个增量;beta-2表示该增量的某一次里程碑

  • (P263)profile