ClassPathXmlApplicationContext的注册方式

源码分析基于Spring4.3

从ClassPathXmlApplicationContext入口,最终都会调用到

/*
* 使用给定父级创建新的ClassPathXmlApplicationContext,从给定的XML文件加载定义信息。
* 加载所有的bean 定义信息并且创建所有的单例
* 或者,在进一步配置上下文之后手动调用刷新。
*/
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException { super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}

上述注释的解释如是说:在容器的启动过程中,初始化过程中所有的bean都是单例存在的

自动刷新

ApplicationContext context = new ClassPathXmlApplicationContext("xxx.xml");

就等同于

手动刷新

ApplicationContext context = new ClassPathXmlApplicationContext();
context.register("xxx.xml");
context.refresh();

上述一共有三条链路,下面来一一分析

加载父子容器

  • 首先是加载并初始化父容器的方法

  1. 第一个出场的是ClassPathXmlApplicationContext,它是一个独立的应用程序上下文,从类路径获取上下文定义文件,能够将普通路径解析为包含包路径的类路径资源名称。它可以支持Ant-Style(路径匹配原则),它是一站式应用程序的上下文,考虑使用GenericApplicationContext类结合XmlBeanDefinitionReader来设置更灵活的上下文配置。

Ant-Style 路径匹配原则,例如 "mypackages/application-context.xml" 可以用"mypackages/*-context.xml" 来替换。

⚠️注意: 如果有多个上下文配置,那么之后的bean定义将覆盖之前加载的文件。这可以用来通过额外的XML文件故意覆盖某些bean定义

  1. 随后不紧不慢走过来的不是一个完整的somebody,AbstractXmlApplicationContext, 它是为了方便ApplicationContext的实现而出现的(抽象类一个很重要的思想就是适配)。AbstractXmlApplicationContext 的最主要作用就是通过创建一个XML阅读器解析ClassPathXmlApplicationContext 注册的配置文件。它有两个最主要的方法 loadBeanDefinitions(DefaultListableBeanFactory beanFactory) 和 loadBeanDefinitions(XmlBeanDefinitionReader reader)

  2. 下一个缓缓出场的是 AbstractRefreshableConfigApplicationContext ,它就像是中间人的角色,并不作多少工作,很像古代丞相的奏折要呈递给皇上,它的作用就相当于是拿奏折的角色。它用作XML应用程序上下文实现的基类,例如ClassPathXmlApplicationContext、FileSystemXmlApplicationContext和XmlWebApplicationContext

  3. 当老板的一般都比较听小秘的,那么AbstractRefreshableApplicationContext就扮演了小秘的角色,它是ApplicationContext的基类,支持多次调用refresh()方法,每次都会创建一个新的内部bean factory实例。继承 AbstractRefreshableApplicationContext 需要唯一实现的方法就是loadBeanDefinitions,在每一次调用刷新方法的时候。一个具体的实现是加载bean定义信息的DefaultListableBeanFactory。

  4. 但是只有小秘给老板递交请辞不行,中间还要有技术leader 来纵览大局,向上与老板探讨公司发展计划,在下领导新人做项目打硬仗(这种男人真的很有魅力哈哈哈),但是技术leader也不能干完所有的工作,他还需要交给手下的程序员去帮他完成具体的工作,程序员接到一项工作,看看有没有可复用的项目和开源类库,发现有可用的,直接把"引用"链接过去就可以了。这就是容器的初始化工作,但是这一步的流程还没有结束,你还得时刻记住你是给boss干活的。

public AbstractApplicationContext(@Nullable ApplicationContext parent) {
// 交给其他程序员去完成的工作
this();
// 明确自己的老板是谁
setParent(parent);
} public AbstractApplicationContext() {
this.resourcePatternResolver = getResourcePatternResolver();
} // 返回 ResourcePatternResolver 去解析资源实例中的匹配模式,默认的是 PathMatchingResourcePatternResolver 支持 Ant-Style 模式。
protected ResourcePatternResolver getResourcePatternResolver() {
return new PathMatchingResourcePatternResolver(this);
} // 此时的resourceLoader 就是ClassPathXmlApplicationContext 对象。
public PathMatchingResourcePatternResolver(ResourceLoader resourceLoader) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
this.resourceLoader = resourceLoader;
}

你需要一些程序员帮你做具体的编码工作,也需要明确你是公司的员工,需要听从老板的,所以你需要明确老板是谁

@Override
public void setParent(@Nullable ApplicationContext parent) {
this.parent = parent;
if (parent != null) {
Environment parentEnvironment = parent.getEnvironment();
if (parentEnvironment instanceof ConfigurableEnvironment) {
getEnvironment().merge((ConfigurableEnvironment) parentEnvironment);
}
}
}

但是这个时候老板出差了,不在了(因为传过来的parent 是 null),所以你需要自己做一些decision。至此,第一条线路就分析完成了。

配置路径解析

  • 第二条线路,ApplicationContext中的 setConfigLocations(configLocations)
// 参数传过来的是可变参数,可变参数是一个数组,也就是说,你可以传递多个配置文件,用","分隔起来。
public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
// configlocations 是一个可为空的String数组,可以为null,为null可以进行手动注册。
this.configLocations = new String[locations.length];
// 解析数组中的每一个配置文件的路径。
for (int i = 0; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
// 默认是直接创建了一个 ClassPathXmlApplicationContext 的无参数的构造函数,采用手动注册的方式。
else {
this.configLocations = null;
}
}

关键点:路径解析方法 : AbstractRefreshableConfigApplicationContext 中的 resolvePath(locations[i]).trim(); 来看看是如何进行路径解析的

// 解析给定的路径,必要时用相应的环境属性值替换占位符。应用于路径配置。
protected String resolvePath(String path) {
return getEnvironment().resolveRequiredPlaceholders(path);
}

涉及两个方法,AbstractRefreshableConfigApplicationContext 中的getEnvironment() 和 validateRequiredProperties(),先来看第一个

  • getEnvironment()

    // 以配置的形式返回此应用程序上下文的Environment,来进一步自定义
    // 如果没有指定,则通过初始化默认的环境。
    @Override
    public ConfigurableEnvironment getEnvironment() {
    if (this.environment == null) {
    // 使用默认的环境配置
    this.environment = createEnvironment();
    }
    return this.environment;
    }
    • 下面来看一下createEnvironment()如何初始化默认的环境:

      // 创建并返回一个 StandardEnvironment,子类重写这个方法为了提供
      // 一个自定义的 ConfigurableEnvironment 实现。
      protected ConfigurableEnvironment createEnvironment() {
      // StandardEnvironment 继承AbstractEnvironment,而AbstractEnvironment
      // 实现了ConfigurableEnvironment
      return new StandardEnvironment();
      }

      其实很简单,也只是new 了一个StandardEnvironment() 的构造器而已。StandardEnvironment是什么?非web应用程序的Environment 的标准实现。他实现了AbstractEnvironment 抽象类,下面是具体的继承树:

StandardEnvironment是AbstractEnvironment的具体实现,而AbstractEnvironment又是继承了ConfigurableEnvironment接口,提供了某些方法的具体实现,ConnfigurableEnvironment 继承了Environment,而Environment 和 ConfigurablePropertyResolver 同时继承了PropertyResolver

下面来看一下StandardEnvironment() 的源码:

public class StandardEnvironment extends AbstractEnvironment {

  // 系统属性资源名称
public static final String SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME = "systemEnvironment"; // JVM系统属性资源名:
public static final String SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME = "systemProperties"; //为标准的Java 环境 自定义合适的属性文件
@Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
} } 现在读者就会产生疑问,不是说new出来一个标准的StandardEnvironment 实现吗,但是StandardEnvironment并没有默认的构造方法啊?这是什么回事呢? 其实StandardEnvironment 的构造方法是 AbstractEnvironment: public AbstractEnvironment() {
// 实现自定义属性资源的方法,也就是StandardEnvironment中customizePropertySources()
customizePropertySources(this.propertySources);
if (logger.isDebugEnabled()) {
logger.debug("Initialized " + getClass().getSimpleName() + " with PropertySources " + this.propertySources);
}
} 上述的`customizePropertySources` 由`StandardEnvironment` 来实现,具体如下 @Override
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
} 由于容器在刚起步的时候 propertySources 是null,所以添加完系统环境(systemEnvironment)和系统属性(systemProperties) 之后,会变成下图所示
    • 如何获取系统属性和如何获取系统环境没有往下跟,有兴趣的读者可以继续沿用。

      大致截一个图,里面大概的属性是这样

      systemProperties

      systemEnvironment

  • 另外一个是 resolveRequiredPlaceholders,它是由 PropertyResolver 超顶级接口定义的方法

// 在给定的text 参数中解析${} 占位符,将其替换为getProperty 解析的相应属性值。
// 没有默认值的无法解析的占位符将导致抛出IllegalArgumentException。
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
    • 由 AbstractPropertyResolver 子类来实现,且看AbstractPropertyResolver 的继承树

    -

    具体实现的方法如下:

    // 传递进来的文本就是解析过的 配置文件 SimpleName
    @Override
    public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
    if (this.strictHelper == null) {
    this.strictHelper = createPlaceholderHelper(false);
    }
    return doResolvePlaceholders(text, this.strictHelper);
    } // 调用createPlaceholderHelper
    private PropertyPlaceholderHelper createPlaceholderHelper(boolean ignoreUnresolvablePlaceholders) {
    return new PropertyPlaceholderHelper(this.placeholderPrefix, this.placeholderSuffix,
    this.valueSeparator, ignoreUnresolvablePlaceholders);
    } ----------------------------PropertyPlaceholderHelper------------------------------- // PropertyPlaceholderHelper加载的时候会把下面的特殊字符放进去
    static {
    wellKnownSimplePrefixes.put("}", "{");
    wellKnownSimplePrefixes.put("]", "[");
    wellKnownSimplePrefixes.put(")", "(");
    } /*
    创建一个新的 PropertyPlaceholderHelper 使用提供的前缀 和 后缀
    * 参数解释:placeholderPrefix 占位符开头的前缀
    * placeholderSuffix 占位符结尾的后缀
    * valueSeparator 占位符变量和关联的默认值 之间的分隔符
    * ignoreUnresolvablePlaceholders 指示是否应忽略不可解析的占位符。
    */ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix,
    @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { Assert.notNull(placeholderPrefix, "'placeholderPrefix' must not be null");
    Assert.notNull(placeholderSuffix, "'placeholderSuffix' must not be null");
    this.placeholderPrefix = placeholderPrefix;
    this.placeholderSuffix = placeholderSuffix;
    String simplePrefixForSuffix = wellKnownSimplePrefixes.get(this.placeholderSuffix);
    if (simplePrefixForSuffix != null && this.placeholderPrefix.endsWith(simplePrefixForSuffix)) {
    this.simplePrefix = simplePrefixForSuffix;
    }
    else {
    this.simplePrefix = this.placeholderPrefix;
    }
    this.valueSeparator = valueSeparator;
    this.ignoreUnresolvablePlaceholders = ignoreUnresolvablePlaceholders;
    }

    解析完成占位符之后,需要做真正的解析,调用AbstractPropertyResolver中的doResolvePlaceholders 方法。

    private String doResolvePlaceholders(String text, PropertyPlaceholderHelper helper) {
    return helper.replacePlaceholders(text, new PropertyPlaceholderHelper.PlaceholderResolver() {
    @Override
    public String resolvePlaceholder(String placeholderName) {
    return getPropertyAsRawString(placeholderName);
    }
    });
    }

    PlaceholderResolver 是 PropertyPlaceholderHelper类的内部类,这是一种匿名内部类的写法,它真正调用的就是 PropertyPlaceholderHelper中的 replacePlaceholders 方法,具体如下:

    // 将格式为 ${name} 的占位符替换为从提供 PlaceholderResolver 返回的值。
    public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
    Assert.notNull(value, "'value' must not be null");
    return parseStringValue(value, placeholderResolver, new HashSet<String>());
    } protected String parseStringValue(
    String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { StringBuilder result = new StringBuilder(value); int startIndex = value.indexOf(this.placeholderPrefix); // 判断指定的占位符有无 ${ 存在,没有的话直接返回
    while (startIndex != -1) {
    int endIndex = findPlaceholderEndIndex(result, startIndex);
    if (endIndex != -1) {
    String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
    String originalPlaceholder = placeholder;
    if (!visitedPlaceholders.add(originalPlaceholder)) {
    throw new IllegalArgumentException(
    "Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
    }
    // Recursive invocation, parsing placeholders contained in the placeholder key.
    placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
    // Now obtain the value for the fully resolved key...
    String propVal = placeholderResolver.resolvePlaceholder(placeholder);
    if (propVal == null && this.valueSeparator != null) {
    int separatorIndex = placeholder.indexOf(this.valueSeparator);
    if (separatorIndex != -1) {
    String actualPlaceholder = placeholder.substring(0, separatorIndex);
    String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
    propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
    if (propVal == null) {
    propVal = defaultValue;
    }
    }
    }
    if (propVal != null) {
    // Recursive invocation, parsing placeholders contained in the
    // previously resolved placeholder value.
    propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
    result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
    if (logger.isTraceEnabled()) {
    logger.trace("Resolved placeholder '" + placeholder + "'");
    }
    startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
    }
    else if (this.ignoreUnresolvablePlaceholders) {
    // Proceed with unprocessed value.
    startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
    }
    else {
    throw new IllegalArgumentException("Could not resolve placeholder '" +
    placeholder + "'" + " in value \"" + value + "\"");
    }
    visitedPlaceholders.remove(originalPlaceholder);
    }
    else {
    startIndex = -1;
    }
    } return result.toString();
    }

    直白一点,上述过程就是用来判断有没有 ${ 这个占位符,如果有的话就进入下面的判断逻辑,把${}

    中的值替换为 PlaceholderResolver 返回的值,如果没有的话,就直接返回。

容器刷新

​ 在经过上述的准备工作完成后,接下来就是整个IOC,DI和AOP的核心步骤了,也是Spring框架的灵魂。由于源码太多,设计范围太广,本篇只分析刷新预处理应该做的事:我们都知道,无论你加载的是哪一种上下文环境,最终都会调用 AbstractApplicationContext 的refresh()方法,此方法是一切加载、解析、注册、销毁的核心方法,采用了工厂的设计思想。

// 完成IoC容器的创建及初始化工作
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) { // 1: 刷新前的准备工作。
prepareRefresh(); // 告诉子类刷新内部bean 工厂。
// 2:创建IoC容器(DefaultListableBeanFactory),加载解析XML文件(最终存储到Document对象中)
// 读取Document对象,并完成BeanDefinition的加载和注册工作
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 3: 对IoC容器进行一些预处理(设置一些公共属性)
prepareBeanFactory(beanFactory); try { // 4: 允许在上下文子类中对bean工厂进行后处理。
postProcessBeanFactory(beanFactory); // 5: 调用BeanFactoryPostProcessor后置处理器对BeanDefinition处理
invokeBeanFactoryPostProcessors(beanFactory); // 6: 注册BeanPostProcessor后置处理器
registerBeanPostProcessors(beanFactory); // 7: 初始化一些消息源(比如处理国际化的i18n等消息源)
initMessageSource(); // 8: 初始化应用事件多播器
initApplicationEventMulticaster(); // 9: 初始化一些特殊的bean
onRefresh(); // 10: 注册一些监听器
registerListeners(); // 11: 实例化剩余的单例bean(非懒加载方式)
// 注意事项:Bean的IoC、DI和AOP都是发生在此步骤
finishBeanFactoryInitialization(beanFactory); // 12: 完成刷新时,需要发布对应的事件
finishRefresh();
} catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
} // 销毁已经创建的单例避免占用资源
destroyBeans(); // 重置'active' 标签。
cancelRefresh(ex); // 传播异常给调用者
throw ex;
} finally { // 重置Spring核心中的常见内省缓存,因为我们可能不再需要单例bean的元数据了...
resetCommonCaches();
}
}
}

刷新容器之刷新预处理

​ 此步骤的主要作用在于:准备刷新的上下文,设置启动的时间和active的标志作为扮演属性资源初始化的角色。

protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true); if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
} // 初始化environment 上下文中的占位符属性资源
initPropertySources();
// 验证标记为必需的所有属性是否可解析
getEnvironment().validateRequiredProperties(); // 允许收集早期的ApplicationEvents
this.earlyApplicationEvents = new LinkedHashSet<>();
}

这里面有两处代码需要说明:initPropertySources这个方法是需要子类进行实现的,默认是不会做任何事情的;getEnvironment() 这个方法由于上述的源码分析过程中,已经默认创建了 createEnvironment,所以这段代码是直接返回的

@Override
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
this.environment = createEnvironment();
}
return this.environment;
}

下面只剩下了validateRequiredProperties()的分析,不着急,看源码不能着急,要怀着这个世界很美好的心情去看。

首先在 ConfigurablePropertyResolver 接口中定义了 validateRequiredProperties 方法

// 验证每一个被setRequiredProperties 设置的属性存在并且解析非空值,会抛出
// MissingRequiredPropertiesException 异常如果任何一个需要的属性没有被解析。
void validateRequiredProperties() throws MissingRequiredPropertiesException;

在抽象子类AbstractPropertyResolver 中被重写

@Override
public void validateRequiredProperties() {
// 属性找不到抛出异常的对象
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
ex.addMissingRequiredProperty(key);
}
}
if (!ex.getMissingRequiredProperties().isEmpty()) {
throw ex;
}
}

因为在我们的源码分析中,没有看到任何操作是在对 requiredProperties 进行添加操作,也就是如下:

@Override
public void setRequiredProperties(String... requiredProperties) {
if (requiredProperties != null) {
for (String key : requiredProperties) {
this.requiredProperties.add(key);
}
}
}

所以,此时的 requiredProperties 这个set集合是null, 也就不存在没有解析的元素了。

本篇到此就结束了,下一篇文章会进行源码分析的下一个步骤: 创建IOC容器以及Bean的解析。

Spring IOC源码分析之-刷新前的准备工作的更多相关文章

  1. Spring Ioc源码分析系列--Ioc源码入口分析

    Spring Ioc源码分析系列--Ioc源码入口分析 本系列文章代码基于Spring Framework 5.2.x 前言 上一篇文章Spring Ioc源码分析系列--Ioc的基础知识准备介绍了I ...

  2. Spring IOC 源码分析

    Spring 最重要的概念是 IOC 和 AOP,本篇文章其实就是要带领大家来分析下 Spring 的 IOC 容器.既然大家平时都要用到 Spring,怎么可以不好好了解 Spring 呢?阅读本文 ...

  3. Spring Ioc源码分析系列--Ioc容器BeanFactoryPostProcessor后置处理器分析

    Spring Ioc源码分析系列--Ioc容器BeanFactoryPostProcessor后置处理器分析 前言 上一篇文章Spring Ioc源码分析系列--Ioc源码入口分析已经介绍到Ioc容器 ...

  4. Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理

    Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理 前言 上一篇分析了BeanFactoryPostProcessor的作用,那么这一篇继续 ...

  5. Spring Ioc源码分析系列--Bean实例化过程(一)

    Spring Ioc源码分析系列--Bean实例化过程(一) 前言 上一篇文章Spring Ioc源码分析系列--Ioc容器注册BeanPostProcessor后置处理器以及事件消息处理已经完成了对 ...

  6. Spring Ioc源码分析系列--Bean实例化过程(二)

    Spring Ioc源码分析系列--Bean实例化过程(二) 前言 上篇文章Spring Ioc源码分析系列--Bean实例化过程(一)简单分析了getBean()方法,还记得分析了什么吗?不记得了才 ...

  7. Spring Ioc源码分析系列--@Autowired注解的实现原理

    Spring Ioc源码分析系列--@Autowired注解的实现原理 前言 前面系列文章分析了一把Spring Ioc的源码,是不是云里雾里,感觉并没有跟实际开发搭上半毛钱关系?看了一遍下来,对我的 ...

  8. Spring Ioc源码分析系列--容器实例化Bean的四种方法

    Spring Ioc源码分析系列--实例化Bean的几种方法 前言 前面的文章Spring Ioc源码分析系列--Bean实例化过程(二)在讲解到bean真正通过那些方式实例化出来的时候,并没有继续分 ...

  9. spring IoC源码分析 (3)Resource解析

    引自 spring IoC源码分析 (3)Resource解析 定义好了Resource之后,看到XmlFactoryBean的构造函数 public XmlBeanFactory(Resource  ...

随机推荐

  1. POJ3311 Hie with the Pie

    The Pizazz Pizzeria prides itself in delivering pizzas to its customers as fast as possible. Unfortu ...

  2. scrapy实战

    采用scrapy实现对股票网站的爬取 功能描述: 技术路线:scrapy 目标:获取上交所和深交所所有股票名称和交易信息并存储 实例编写: 步骤1:建立工程和spider爬虫模板 步骤2:编写spid ...

  3. jsp和java获取文件或路径

    1.如何获得当前文件路径常用:(1).Test.class.getResource("")得到的是当前类FileTest.class文件的URI目录.不包括自己!(2).Test. ...

  4. unbuntu 矫正电脑系统时间

    sudo tzconfig,如果命令不存在请使用 dpkg-reconfigure tzdata

  5. C#反射(Reflection)详解

    1. 什么是反射2. 命名空间与装配件的关系3. 运行期得到类型信息有什么用4. 如何使用反射获取类型5. 如何根据类型来动态创建对象6. 如何获取方法以及动态调用方法7. 动态创建委托 1.什么是反 ...

  6. javascript 表格隔行换色

    用到的知识点: 获取表格元素 tbody 和 rows都是有索引的 这里我们只有一组tbody所以 索引是0 偶数行 索引取余2为0 奇数行 索引取余2不为0 通过 遍历行索引设置相应的颜色. < ...

  7. LeetCode OJ-- N-Queens **

    https://oj.leetcode.com/problems/n-queens/ n皇后问题,1皇后有1个解,4皇后2个解,8皇后也有解…… 每个皇后不能在同一行上,同一列上,以及同一条45度线上 ...

  8. LeetCode OJ-- Longest Palindromic Substring **

    https://oj.leetcode.com/problems/longest-palindromic-substring/ 给一个string,求它的最长回文子串. 定义一个二维数组bool is ...

  9. Notepad++ 自动补全,括号自动完成插件,主题和字体设置

    Notepad++ 自动补全成对符号http://rabbit52.com/2012/devel/notepad-autocomplete-brackets QuickText 和 Zen Codin ...

  10. AC日记——仙岛求药 openjude 2727

    仙岛求药 思路: bfs: 来,上代码: #include <cstdio> #include <cstring> #include <iostream> #inc ...