我的博客同步发布,地址:http://yalunwang.com/2020/02/28/spring-BeanFactoryPostProcessors-PropertyPlaceholderConfigurer.html

BeanFactoryPostProcessors

介绍

BeanFactoryPostProcessors完整定义:

/**
* Allows for custom modification of an application context's bean definitions,
* adapting the bean property values of the context's underlying bean factory.
* @see BeanPostProcessor
* @see PropertyResourceConfigurer
*/
public interface BeanFactoryPostProcessor { /**
* Modify the application context's internal bean factory after its standard
* initialization. All bean definitions will have been loaded, but no beans
* will have been instantiated yet. This allows for overriding or adding
* properties even to eager-initializing beans.
* @param beanFactory the bean factory used by the application context
* @throws org.springframework.beans.BeansException in case of errors
*/
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; }

我们知道spring最大优点就是其可扩展性,BeanFactoryPostProcessor接口就是spring中提供给我们用于扩展的一个地方。我们看该接口上的javadoc其实非常的详细,这也是我们看spring源码的一个技巧,就是看一个类是干嘛的一定要先通读其注释。

结合接口上的注释大致描述下BeanFactoryPostProcessor:
允许用户通过修改applicationContext 中的bean定义(就是xml中定义的bean的信息即:BeanDefinition是和xml有一对一的配置,比如是否是单利,以及propert 属性的赋值等)
来调整applicationContext中bean工厂中bean属性值。 也就是说执行到BeanFactoryPostProcessor时全部的BeanDefinition定义已经加载好了但是bean实例还没有被创建,我们可以修补或者覆盖bean属性值。

我们可以看一下ApplicationContext中BeanFactoryPostProcessor的调用位置来印证是否如此

下面是ApplicationContext核心代码:

public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh(); //获取beanFactory实例
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); prepareBeanFactory(beanFactory); try { postProcessBeanFactory(beanFactory); //这里正是我们的BeanFactoryPostProcessor执行的位置
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.
//创建非懒加载的所有单例 这里是真正创建bean实例的地方
finishBeanFactoryInitialization(beanFactory); // Last step: publish corresponding event.
finishRefresh();
} catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans(); // Reset 'active' flag.
cancelRefresh(ex); // Propagate exception to caller.
throw ex;
}
}
}

经过

    	ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

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

这两步我们已经拿到了beanFactory实例,也就是每一个bean对应的BeanDefinition已经加载好了。下面才是执行invokeBeanFactoryPostProcessors(beanFactory),也就印证了我们上面的结论。

下面我们通过一个BeanFactoryPostProcessor的典型应用PropertyPlaceholderConfigurer来详细讲解BeanFactoryPostProcessor执行原理

PropertyPlaceholderConfigurer

简单使用的例子

PropertyPlaceholderConfigurer相信大家都使用过,我们在配置bean的属性可以使用占位符来赋值,然后通过调整properties文件中对应的属性值来修改。看一个使用PropertyPlaceholderConfigurer简单的例子:

    public class Student {

        private String name;

        private Integer age;

        public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public Integer getAge() {
return age;
} public void setAge(Integer age) {
this.age = age;
}
} <bean class="com.yalunwang.Student" id="student">
<property name="name" value="${student.name}"></property>
<property name="age" value="${student.age}"></property>
</bean> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:bean.properties"></property>
</bean>

bean.properties配置文件:

    student.name=anan
student.age=2

单元测试:

    @Test
public void test_ioc_app(){
ApplicationContext ac = new ClassPathXmlApplicationContext("spring-ioc.xml"); Student student =(Student) ac.getBean("student"); System.out.println("name: "+student.getName());
System.out.println("age: "+student.getAge());
}

输出结果一切正常:

name: anan
age: 2

源码分析

先看一下PropertyPlaceholderConfigurer的类继承图:

可以看到PropertyPlaceholderConfigurer实现了BeanFactoryPostProcessor和 PriorityOrdered。

我们接着对上面的 invokeBeanFactoryPostProcessors(beanFactory)继续进行分析:

/**
* Instantiate and invoke all registered BeanFactoryPostProcessor beans,
* respecting explicit order if given.
* 实例化并调用所有已注册的BeanFactoryPostProcessor Bean,
* 如果继承了Order接口按顺序执行
* <p>Must be called before singleton instantiation.
*/
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<String>();
//如果是beanFactory实现了BeanDefinitionRegistry
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
List<BeanDefinitionRegistryPostProcessor> registryPostProcessors =
new LinkedList<BeanDefinitionRegistryPostProcessor>();
//遍历硬编码设置的beanFactory后置处理器
for (BeanFactoryPostProcessor postProcessor : getBeanFactoryPostProcessors()) {
//如果是BeanDefinitionRegistryPostProcessor类型先执行postProcessBeanDefinitionRegistry方法再将其添加到registryPostProcessors集合中进行后续postProcessBeanFactory方法的执行
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryPostProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryPostProcessor.postProcessBeanDefinitionRegistry(registry);
registryPostProcessors.add(registryPostProcessor);
}
else {
//同理将正常的beanFactory后置处理器添加到regularPostProcessors集合中进行后续postProcessBeanFactory方法的执行
regularPostProcessors.add(postProcessor);
}
}
//找出配置的BeanDefinitionRegistryPostProcessor后置处理器
Map<String, BeanDefinitionRegistryPostProcessor> beanMap =
beanFactory.getBeansOfType(BeanDefinitionRegistryPostProcessor.class, true, false);
List<BeanDefinitionRegistryPostProcessor> registryPostProcessorBeans =
new ArrayList<BeanDefinitionRegistryPostProcessor>(beanMap.values());
OrderComparator.sort(registryPostProcessorBeans);
//执行BeanDefinitionRegistryPostProcessor后置处理器的postProcessBeanDefinitionRegistry方法
for (BeanDefinitionRegistryPostProcessor postProcessor : registryPostProcessorBeans) {
postProcessor.postProcessBeanDefinitionRegistry(registry);
}
//执行上面添加的beanFactory后置处理器的集合里的方法
invokeBeanFactoryPostProcessors(registryPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(registryPostProcessorBeans, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
//处理后的添加到集合里 防止后面重复执行
processedBeans.addAll(beanMap.keySet());
}
else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(getBeanFactoryPostProcessors(), beanFactory);
} //获取配置的BeanFactoryPostProcessor
//以下按实现了 PriorityOrdered Ordered 没有继承 三种进行优先级排序执行
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
//这个就是上面记录的如果已经处理了配置的BeanFactoryPostProcessors就跳过
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
} // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
OrderComparator.sort(priorityOrderedPostProcessors);
//上面我们说了PropertyPlaceholderConfigurer 实现了BeanFactoryPostProcessor和 PriorityOrdered,所以会在这一步执行调用
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); // Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
OrderComparator.sort(orderedPostProcessors);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); // Finally, invoke all other BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
}

上面主要的执行逻辑我都添加了中文注释方便大家理解。

总结一下改方法主要做的事情:

  1. 先判断如果beanFactory是BeanDefinitionRegistry类型的话就添加对BeanDefinitionRegistryPostProcessor类型的调用,BeanDefinitionRegistryPostProcessor接口是BeanFactoryPostProcessor的子接口,BeanDefinitionRegistryPostProcessor的作用是方便我们可以手动注册bean交给spring来管理,可以通过扩展其唯一的方法(void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;) 来注册bean到spring里,这个不是本次分析的重点后面再举例讲解。
  2. 先对硬编码配置的BeanFactoryPostProcessor进行处理 如果是BeanDefinitionRegistryPostProcessor类型会进行postProcessBeanDefinitionRegistry调用和postProcessBeanFactory调用,如果不是则只进行postProcessBeanFactory调用。
  3. 再对配置的BeanDefinitionRegistryPostProcessor进行处理(postProcessBeanDefinitionRegistry调用和postProcessBeanFactory调用)
  4. 最后对配置BeanFactoryPostProcessor的进行处理会按 PriorityOrdered/Ordered/没有继承任何排序接口 三种进行优先级排序执行postProcessBeanFactory调用。其中我们的PropertyPlaceholderConfigurer 实现了BeanFactoryPostProcessor和 PriorityOrdered,所以会在这一步执行调用

    invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
  • ** 我们继续进行分析:**

invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

会执行PropertyPlaceholderConfigurer父类PropertyResourceConfigurer中的方法

/**
* {@linkplain #mergeProperties Merge}, {@linkplain #convertProperties convert} and
* {@linkplain #processProperties process} properties against the given bean factory.
* @throws BeanInitializationException if any properties cannot be loaded
*/
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
try {
Properties mergedProps = mergeProperties(); // Convert the merged properties, if necessary.
convertProperties(mergedProps); // Let the subclass process the properties.
processProperties(beanFactory, mergedProps);
}
catch (IOException ex) {
throw new BeanInitializationException("Could not load properties", ex);
}
}

mergeProperties()方法会先将 配置的properties加载到mergedProps里

后面调用** processProperties(beanFactory, mergedProps)**;进行处理

  • processProperties(beanFactory, mergedProps)分析
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)
throws BeansException { StringValueResolver valueResolver = new PlaceholderResolvingStringValueResolver(props); this.doProcessProperties(beanFactoryToProcess, valueResolver);
}

这里只有两行代码,第一行是创建StringValueResolver实例(用于替换占位符的真正方法)

我们跨过千山万水终于要到马上要进行占位符替换了,继续分析

protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver) { //将创建的valueResolver设置到BeanDefinitionVisitor里 用于最终替换逻辑 (替换占位符为对应properties里配置的值)
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
//拿出ioc容器里注册的beans
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName : beanNames) { //排除掉当前beanName也就是 PropertyPlaceholderConfigurer Bean 且beanFactoryToProcess必须是当前容器
if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
//获取一个bean对应的bean定义
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try {
//进行bean定义元数据的替换操作
visitor.visitBeanDefinition(bd);
}
catch (Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage());
}
}
} // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
beanFactoryToProcess.resolveAliases(valueResolver); // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}

上面我已经使用中文注释写的很清楚了接着进行分析

  • visitor.visitBeanDefinition(bd);
public void visitBeanDefinition(BeanDefinition beanDefinition) {
visitParentName(beanDefinition);
visitBeanClassName(beanDefinition);
visitFactoryBeanName(beanDefinition);
visitFactoryMethodName(beanDefinition);
visitScope(beanDefinition);
visitPropertyValues(beanDefinition.getPropertyValues());
ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
visitIndexedArgumentValues(cas.getIndexedArgumentValues());
visitGenericArgumentValues(cas.getGenericArgumentValues());
}

这里可以对应 bean的parentName beanClassName property等进行替换操作我们这里只关注属性的替换操作

  • visitPropertyValues(beanDefinition.getPropertyValues());
protected void visitPropertyValues(MutablePropertyValues pvs) {
PropertyValue[] pvArray = pvs.getPropertyValues();
for (PropertyValue pv : pvArray) {
Object newVal = resolveValue(pv.getValue());
if (!ObjectUtils.nullSafeEquals(newVal, pv.getValue())) {
pvs.add(pv.getName(), newVal);
}
}
}
  • Object newVal = resolveValue(pv.getValue());
@SuppressWarnings("rawtypes")
protected Object resolveValue(Object value) {
if (value instanceof BeanDefinition) {
visitBeanDefinition((BeanDefinition) value);
}
else if (value instanceof BeanDefinitionHolder) {
visitBeanDefinition(((BeanDefinitionHolder) value).getBeanDefinition());
}
else if (value instanceof RuntimeBeanReference) {
RuntimeBeanReference ref = (RuntimeBeanReference) value;
String newBeanName = resolveStringValue(ref.getBeanName());
if (!newBeanName.equals(ref.getBeanName())) {
return new RuntimeBeanReference(newBeanName);
}
}
else if (value instanceof RuntimeBeanNameReference) {
RuntimeBeanNameReference ref = (RuntimeBeanNameReference) value;
String newBeanName = resolveStringValue(ref.getBeanName());
if (!newBeanName.equals(ref.getBeanName())) {
return new RuntimeBeanNameReference(newBeanName);
}
}
else if (value instanceof Object[]) {
visitArray((Object[]) value);
}
else if (value instanceof List) {
visitList((List) value);
}
else if (value instanceof Set) {
visitSet((Set) value);
}
else if (value instanceof Map) {
visitMap((Map) value);
}
else if (value instanceof TypedStringValue) {
TypedStringValue typedStringValue = (TypedStringValue) value;
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
String visitedString = resolveStringValue(stringValue);
typedStringValue.setValue(visitedString);
}
}
else if (value instanceof String) {
return resolveStringValue((String) value);
}
return value;
}

这里有很多类型,是因为spring支持很多类型的配置比如property的值我们可以配置为ref=xxxbean那么value就是RuntimeBeanReference类型,

如果配置

             <list>
<value>343</value>
<value>45</value>
</list>

那么value就是List类型等等。这里我们例子中配置的类型是TypedStringValue,那么执行

	else if (value instanceof TypedStringValue) {
TypedStringValue typedStringValue = (TypedStringValue) value;
//这里拿到的值是原始的带有占位符的比如例子里就是${student.name}、${student.age}这种
String stringValue = typedStringValue.getValue();
if (stringValue != null) {
//这一步就是去进行替换
String visitedString = resolveStringValue(stringValue);
typedStringValue.setValue(visitedString);
}
}
  • String visitedString = resolveStringValue(stringValue);
/**
* Resolve the given String value, for example parsing placeholders.
* @param strVal the original String value
* @return the resolved String value
*/
protected String resolveStringValue(String strVal) {
if (this.valueResolver == null) {
throw new IllegalStateException("No StringValueResolver specified - pass a resolver " +
"object into the constructor or override the 'resolveStringValue' method");
}
//调用我们之前传进来的valueResolver也就是上面 (BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver); 这里传进来的) 进行替换操作
String resolvedValue = this.valueResolver.resolveStringValue(strVal);
// Return original String if not modified.
return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
}

valueResolver其实就是PlaceholderResolvingStringValueResolver实例,它又委托PropertyPlaceholderHelper进行操作

也就是

/**
* Replaces all placeholders of format {@code ${name}} with the value returned
* from the supplied {@link PlaceholderResolver}.
* @param value the value containing the placeholders to be replaced.
* @param placeholderResolver the {@code PlaceholderResolver} to use for replacement.
* @return the supplied value with placeholders replaced inline.
*/
public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
Assert.notNull(value, "Argument 'value' must not be null.");
return parseStringValue(value, placeholderResolver, new HashSet<String>());
} protected String parseStringValue(
String strVal, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) { StringBuilder buf = new StringBuilder(strVal); int startIndex = strVal.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(buf, startIndex);
if (endIndex != -1) {
//将ex. ${student.name} 转成 student.name
String placeholder = buf.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
// 递归调用直到没有占位符
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// 获取properties配置文件对应的值 就是获取student.name 对应在 properties里的值
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) {
//再次递归调用 确保最后的值没有占位符
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
//将原先的StringBuff的原始值替换为拿到的值
buf.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
//这里因为已经替换成真正的值 拿不到占位符(${)所以值就是-1 会跳出循环返回
startIndex = buf.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = buf.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in string value \"" + strVal + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
} return buf.toString();
}

该方法是真正替换的操作所在,改方法已经添加中文注释应该很好理解了,总结就是做了一下两件事:

1.首先会将ex. ${student.name} 转成 student.name

2.将student.name通过 tring propVal = placeholderResolver.resolvePlaceholder(placeholder);获取真正的值然后返回。

protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) {
String propVal = null;
if (systemPropertiesMode == SYSTEM_PROPERTIES_MODE_OVERRIDE) {
propVal = resolveSystemProperty(placeholder);
}
if (propVal == null) {
propVal = resolvePlaceholder(placeholder, props);
}
if (propVal == null && systemPropertiesMode == SYSTEM_PROPERTIES_MODE_FALLBACK) {
propVal = resolveSystemProperty(placeholder);
}
return propVal;
}

该方法就比较简单了就是根据不同的模式做处理,systemPropertiesMode默认是SYSTEM_PROPERTIES_MODE_FALLBACK

  1. 如果systemPropertiesMode = SYSTEM_PROPERTIES_MODE_OVERRIDE 是指 系统配置文件优先级大于我们的配置文件。
  2. 拿不到或者配置不等于SYSTEM_PROPERTIES_MODE_OVERRIDE 就去我们配置文件里进行获取
  3. 如果还获取不到且如果systemPropertiesMode=SYSTEM_PROPERTIES_MODE_FALLBACK就再尝试去系统文件里查找。

结论

经过以上各个步骤最终BeanDefinition里的parentName beanClassName property中的占位符都会被我们propertis配置文件中对应的值所替换掉,这就为后续实例化bean后做bean实例属性填充时做好了准备。

我们再进行 Student student =(Student) ac.getBean("student"); 时就可以正常打印对应的值了。

看源码的一点分享tip

  • 看源码可能会比较枯燥,坚持很重要,如果看一遍是懵的那就是歇一歇再继续多看几遍这样慢慢就会找到感觉
  • 看的过程中一定要写例子进行调试,然后再继续反复看慢慢就会明白其中的含义
  • 看源码可以看某一个核心类的继承图,以及javadoc。还可以尝试画出核心的时序图,这些都能对我们看懂源码起到事半功倍的作用。

联系我

至此本篇源码分析就结束了,后续继续spring源码分析的文章。加油!!!

最新文章

  1. js中函数的一些理论知识
  2. Android Paint的属性
  3. JS生成二维码,允许中文转码
  4. 你应该知道的那些Android小经验
  5. c++ 头文件
  6. Oracle Redo
  7. PHP 小方法之 过滤参数
  8. 在一个Label上设置多种颜色字体
  9. greenDao 3.0基础
  10. openssl 生成CSR
  11. C#运算符的优先级
  12. Spring3.2新注解@ControllerAdvice
  13. python 记录linux网速到文件。
  14. Java 基础之一对象导论
  15. linux下查找某文件关键字(grep 函数)
  16. Consider defining a bean of type &#39;org.springframework.data.redis.connection.RedisConnectionFactory&#39; in your configuration
  17. 解决SSH Secure Shell 连接Liunx 有乱码情况。
  18. 登录验证码实现(Captcha)
  19. php -- 数据库信息
  20. python 入门练习

热门文章

  1. Python语言学习:homework1
  2. tensorflow(六)
  3. Overlapping generations model
  4. 【Java杂货铺】JVM#虚拟机加载机制
  5. Perl:正则中问号的四周用途:1.字面意义的问号 2. 量词 3. 表示非贪心的修饰符 4.用以表示不具有记忆功能的圆括号
  6. BaseAdapter教程(1) 最简单地使用BaseAdapter
  7. c/c++[001]:start
  8. 监控 Linux 服务器活动的几个命令(watch top ac)
  9. 一文带你了解BOM基本知识
  10. 系统学习javaweb4----CSS层叠样式表(结束)