Spring5源码解析-Spring框架中的单例和原型bean

最近一直有问我单例和原型bean的一些原理性问题,这里就开一篇来说说的

通过Spring中的依赖注入极大方便了我们的开发。在xml通过<bean>定义(或者通过@Bean在配置类里定义)对象之后,然后只需简单地使用@Autowired注解,就可以使用由Spring上下文管理的每个对象。需要注意的是,所有这些对象在Spring中默认都是单例。

这一次我们会去讨论Spring如何来管理这些定义的bean。在第一部分中,我们将讲解单例和原型作用域的概念。第二部分中,我们将分析单例和原型作用域之间的依赖关系。其后说一下方法注入。最后专门对相关Spring的代码来做下分析,具体看看bean是如何构建出来的。

Spring中的bean默认都是单身贵族

Spring使用单例设计模式来管理bean?不完全是。Singleton设计模式假定它们是由Java的类加载器管理的jvm中给定类的唯一一个实例。在Spring中,还是有点不一样。默认情况下,它们为每个给定的org.springframework.context.ApplicationContext实例存在唯一的一个bean (有点别扭,也就是可以有多个Spring容器,每一个容器内存在唯一bean实例,之前的文章中有涉及例子的)。这意味着如果你有两个或更多上下文,所有这些上下文都由同一Java的类加载器管理(因为在同一个jvm环境中),则可能会有多个给定bean的实例。唯一需要做到的是必须在每个上下文中定义此bean。讲那么多不如代码更有说服力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MultipleContextes {
 
public static void main(String[] args) {
try {
// retreive two different contexts
ApplicationContext firstContext = new FileSystemXmlApplicationContext("/home/bartosz/webapp/src/main/resources/META-INF/applicationContext.xml");
ApplicationContext secondContext = new FileSystemXmlApplicationContext("/home/bartosz/webapp/src/main/resources/META-INF/applicationContext.xml");
 
// compare the objects from different contexts
ShoppingCart firstShoppingCart = (ShoppingCart) firstContext.getBean("shoppingCart");
ShoppingCart secondShoppingCart = (ShoppingCart) secondContext.getBean("shoppingCart");
System.out.println("1. Are they the same ? " + (firstShoppingCart == secondShoppingCart));
 
// compare the objects from the same context
ShoppingCart firstShoppingCartBis = (ShoppingCart) firstContext.getBean("shoppingCart");
System.out.println("2. Are they the same ? "+ (firstShoppingCart == firstShoppingCartBis));
} catch (Exception e) {
e.printStackTrace();
}
}
}

通过执行此代码,你应该得到:

1
2
1. Are they the same ? false
2. Are they the same ? true

所以你可以看到,bean只是一个上下文的单例。这就是为什么你不应该将Spring的单例概念与设计模式中的的单例混合在一起。

但是,如果要为一个定义的bean在一个上下文内可以使用不同的实例,应该怎么做?很简单,你应该将此Bean配置为原型作用域:

1
2
<bean id="shoppingCart" class="com.migo.data.ShoppingCart" scope="prototype">
</bean>

现在,在运行以前的代码之后,你可以看到如下输出:

1
2
1. Are they the same ? false
2. Are they the same ? false

我们已经知道两个作用域之间的区别。但在哪种情况下我们应该选择使用单例还是原型?Singleton适用于无状态的bean,即没有状态的bean。比如一个serviceDAO或者controller。他们都没有自己的状态(举个简单的例子,一个函数sin(x),这个函数本身就是无状态的,所以我们现在喜欢的函数式编程也遵循这个理念)。而是根据传输的参数执行一些操作(作为HTTP请求参数)。另一方面,我们可以通过状态bean管理一些状态。比如购物车bean,假如它是一个单例,那么两个不同消费者购买的产品将被放置在同一个对象上。而如果其中一个消费者想要删除一个产品,另一个消费者就铁定不高兴。这也就是状态类对象应该是原型

这里说点题外话,不能确定时间的保证,未来会出一个用Java的代码习惯去解析vue的一些东西,内容已经总结完毕,也应用到自己的项目中了,然后得出的一些方法论,为什么在这里去说,就是因为vue也是遵循这个无状态和状态专门管理的原则的,扯远了,接着进行下一部分。

将原型放在单例中,反之亦然

通过上面的描述,很多概念都很清楚了吧,但有时候会发生一些更复杂的情况。第一个是在原型bean中放置单例。显然,如果注入的单例对象真的是一个单例的bean(没有状态),这个真的没一点问题。想象一下,对于我们的购物车,我们需要注入产品服务。此服务只会检查添加到购物车的产品是否库存。由于服务没有状态,并且会基于在方法签名中所传递的对象进行验证,因此不存在风险。

另一方面,将原型bean放在单例中需要做更多的工作。我们不能在单例bean中通过使用自动注入(比如@Autowired注解)注入原型bean。当Spring初始化所有具有依赖关系的单例bean时,这些注入只会执行一次。这也就意味着在以下代码,ShoppingCart的实例将始终是相同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class TestController {
@Autowired
private ShoppingCart shoppingCart;
 
@RequestMapping(value = "/addProduct/{productName}")
public String testAdd(@PathVariable(value="productName") String productName) {
Product product = new Product();
product.setName(productName);
this.shoppingCart.addProduct(product);
LOGGER.debug("ShoppingCart is "+this.shoppingCart);
return "test";
}
}

编译此类并进行一些URL调用:http://localhost:8080/addProduct/ice%20tea,http://localhost:8080/addProduct/milk。你将看到如下输出的顺序:

1
2
3
4
// after http://localhost:8080/addProduct/ice%20tea
ShoppingCart is ShoppingCart {products: [Product {ice tea}]}
// after http://localhost:8080/addProduct/milk
ShoppingCart is ShoppingCart {products: [Product {ice tea}, Product {milk}]}

为了在按照我们预想情况下工作(要求不一样的ShoppingCart),我们可以通过bean工厂手动获取ShoppingCart实例(这样就可以再一次生成一个不一样的ShoppingCart实例了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class TestController {
@Autowired
private ApplicationContext context;
 
@RequestMapping(value = "/addProduct/{productName}")
public String testAdd(@PathVariable(value="productName") String productName) {
Product product = new Product();
product.setName(productName);
 
ShoppingCart shoppingCart = (ShoppingCart) context.getBean("shoppingCart");
shoppingCart.addProduct(product);
LOGGER.debug("ShoppingCart is "+shoppingCart);
return "test";
}
}

这样,你就可以日志中看到,每次调用都会有新的ShoppingCart实例的生成:

1
2
3
4
// after http://localhost:8080/addProduct/ice%20tea
ShoppingCart is ShoppingCart {products: [Product {ice tea}]}
// after http://localhost:8080/addProduct/milk
ShoppingCart is ShoppingCart {products: [Product {milk}]}

方法注入

有没有别的方法在每次调用都会产生一个新实例?这就是接下来要说的方法注入的技术。它看起来有点像我们的手动去进行bean的查找,但更优雅。一个可以被上下文所感知(访问应用程序上下文可以得到)的bean将负责在单例bean中生成原型bean实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service("shoppingCartProvider")
public class ShoppingCartProvider implements ApplicationContextAware {
 
private ApplicationContext context;
 
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
 
public ShoppingCart getInstance() {
return (ShoppingCart) context.getBean("shoppingCart");
}
 
}

经过上面的修改,controller这里相应修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
public class TestController {
@Autowired
private ShoppingCartProvider shoppingCartProvider;
 
@RequestMapping(value = "/addProduct/{productName}")
public String testAdd(@PathVariable(value="productName") String productName) {
Product product = new Product();
product.setName(productName);
 
ShoppingCart shoppingCart = shoppingCartProvider.getInstance();
shoppingCart.addProduct(product);
System.out.println("ShoppingCart is "+shoppingCart);
return "test";
}
}

也可以在XML配置文件中定义。里面会有一个属性引用原型bean,并允许在每次调用时创建新的实例。它可以很轻松地在一个bean中混合更多东西:

1
2
3
4
5
6
7
<bean id="shoppingCartProvider" class="com.migo.data.ShoppingCartProvider">
<lookup-method name="getInstance" bean="shoppingCart">
</lookup-method>
</bean>
 
<bean id="shoppingCart" class="com.migo.data.ShoppingCart" scope="prototype">
</bean>
1
2
3
public abstract class ShoppingCartProvider {
public abstract ShoppingCart getInstance();
}

Controller的代码与实现ApplicationContextAware接口的provider的那个例子是一样的。而区别也仅在于provider的bean定义和实现。该定义包含一个标签查找方法。它指定必须使用哪个方法来获取bean属性中指定的bean的新实例。在我们的这个例子中,我们通过调用ShoppingCartProvider类的getInstance方法来寻找新的ShoppingCart的实例。需要注意的一点,类和方法都可以是抽象的。通过这样做,你可以让Spring生成将实现该方法并返回所需bean的子类。如果这个方法不是抽象的,Spring会重写覆盖它。

Spring中的Bean类

单例的源码实现主要存在于org.springframework.beansorg.springframework.context包中。首先,从Bean包中查看BeanFactory接口。它包含两个我们绝对感兴趣的方法,可用来确定bean是单例还是原型:

  • boolean isSingleton(String name)throws NoSuchBeanDefinitionException
  • boolean isPrototype(String name)throws NoSuchBeanDefinitionException

接下来,我们来深入一下AbstractFactoryBean,从这个类的注释可以知道它是作为“FactoryBean实现的简单模板超类(还是直白翻译下比较好,说默认实现也觉得不靠谱)”。它包含一个用来返回单例或创建原型bean的getObject方法的实现。原型和单例是通过createInstance方法在不同的时间段进行的。

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
/**
* Simple template superclass for {@link FactoryBean} implementations that
* creates a singleton or a prototype object, depending on a flag.
*
* <p>If the "singleton" flag is {@code true} (the default),
* this class will create the object that it creates exactly once
* on initialization and subsequently return said singleton instance
* on all calls to the {@link #getObject()} method.
*
* <p>Else, this class will create a new instance every time the
* {@link #getObject()} method is invoked. Subclasses are responsible
* for implementing the abstract {@link #createInstance()} template
* method to actually create the object(s) to expose.
*
* @author Juergen Hoeller
* @author Keith Donald
* @since 1.0.2
* @see #setSingleton
* @see #createInstance()
*/
public abstract class AbstractFactoryBean<T>
implements FactoryBean<T>, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
/**
* Expose the singleton instance or create a new prototype instance.
* @see #createInstance()
* @see #getEarlySingletonInterfaces()
*/
@Override
public final T getObject() throws Exception {
if (isSingleton()) {
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
}
else {
return createInstance();
}
}
...
/**
* Template method that subclasses must override to construct
* the object returned by this factory.
* <p>Invoked on initialization of this FactoryBean in case of
* a singleton; else, on each {@link #getObject()} call.
* @return the object returned by this factory
* @throws Exception if an exception occurred during object creation
* @see #getObject()
*/
protected abstract T createInstance() throws Exception;
...
}

另一个我们会感兴趣的一个点是BeanDefinition接口bean如其名,它定义了一个bean属性,例如:scope,class name,factory method name,properties或constructor arguments。

org.springframework.beans.factory.config.BeanDefinition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* A BeanDefinition describes a bean instance, which has property values,
* constructor argument values, and further information supplied by
* concrete implementations.
*
* <p>This is just a minimal interface: The main intention is to allow a
* {@link BeanFactoryPostProcessor} such as {@link PropertyPlaceholderConfigurer}
* to introspect and modify property values and other bean metadata.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @since 19.03.2004
* @see ConfigurableListableBeanFactory#getBeanDefinition
* @see org.springframework.beans.factory.support.RootBeanDefinition
* @see org.springframework.beans.factory.support.ChildBeanDefinition
*/
public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
//限于篇幅,请自行查看源码,能发现很多有用的东西
}

想要看到bean被初始化的位置,我们需要跳转到context包中,更准确地说就是在AbstractApplicationContext类(这个类我们已经接触过好多次了)中。在它的public void refresh()throws BeansException,IllegalStateException我们可以找到一些关于bean创建的片段,特别是:

  • protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory):实现org.springframework.beans.factory.config .BeanFactoryPostProcessor接口的所有bean 都被初始化和调用。这种类型bean允许修改另一个bean的属性或构造函数参数(请看PostProcessorRegistrationDelegate的相应代码可以知道,使用BeanFactoryPostProcessor来处理我们所要用beanFactory生成的bean,这里可以直接把beanFactory看成是我们需要的bean即可)。但是请注意,在此阶段只能修改bean定义。“正常”bean实例尚未创建。关于这块会请参考文章Spring中的bean工厂后置处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Instantiate and invoke all registered BeanFactoryPostProcessor beans,
* respecting explicit order if given.
* <p>Must be called before singleton instantiation.
*/
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
 
// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}

org.springframework.context.support.PostProcessorRegistrationDelegate

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
/**
* Delegate for AbstractApplicationContext's post-processor handling.
*
* @author Juergen Hoeller
* @since 4.0
*/
class PostProcessorRegistrationDelegate {
 
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
 
// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<>();
 
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<>();
List<BeanDefinitionRegistryPostProcessor> registryPostProcessors =
new LinkedList<>();
 
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryPostProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
registryPostProcessor.postProcessBeanDefinitionRegistry(registry);
registryPostProcessors.add(registryPostProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}
...
}
  • protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory):这里上下文实例化并调用实现了org.springframework.beans.factory.config.BeanPostProcessor接口的所有bean 。实现此接口的bean包含可以在其他bean初始化之前或之后调用的回调。因为内容比较多,关于这块会请参考文章Spring中的bean工厂后置处理器
1
2
3
4
5
6
7
8
/**
* Instantiate and invoke all registered BeanPostProcessor beans,
* respecting explicit order if given.
* <p>Must be called before any instantiation of application beans.
*/
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}
  • protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory):主要调用定义在org.springframework.beans.factory.config.ConfigurableListableBeanFactory接口内的preInstantiateSingletons方法。该方法的目的是实例化所有被定义为非延迟加载的bean。如果在应用程序上下文加载时遇到BeansException异常,则可能来自此方法。当bean无法创建时,它会抛出BeansException异常。
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
/**
* Finish the initialization of this context's bean factory,
* initializing all remaining singleton beans.
*/
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
 
// Register a default embedded value resolver if no bean post-processor
// (such as a PropertyPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
}
 
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}
 
// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);
 
// Allow for caching all bean definition metadata, not expecting further changes.
beanFactory.freezeConfiguration();
 
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}
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
@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.
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.
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();
}
}
}

总结

首先,我们讲了单例和原型作用域之间的区别。第一个为每个容器创建一个对象,而第二个在每个请求时创建一个新的bean对象。单例和原型都可以一起交叉使用,但原型不能通过@Autowired或其他注入方式来解决。它们应该使用getBean()方法方法查找来生成新实例。最后随意说了一说关于bean及其初始化的内容。

Spring5源码解析-Spring框架中的单例和原型bean的更多相关文章

  1. 【Spring】8、Spring框架中的单例Beans是线程安全的么

    看到这样一个问题:spring框架中的单例Beans是线程安全的么? Spring框架并没有对单例bean进行任何多线程的封装处理.关于单例bean的线程安全和并发问题需要开发者自行去搞定.但实际上, ...

  2. Spring框架中的单例bean是线程安全的吗?

    不,Spring框架中的单例bean不是线程安全的.

  3. Spring 框架中的单例 bean 是线程安全的吗?

    不,Spring 框架中的单例 bean 不是线程安全的.

  4. Spring5源码,Spring Web中的处理程序执行链

    一.什么是Spring中的处理程序执行链? 二.HandlerExecutionChain类 三.自定义处理程序执行链 Spring的DispatcherServlet假如缺少几个关键元素将无法分派请 ...

  5. Spring框架中的单例Beans是线程安全的么

    Spring框架并没有对单例bean进行任何多线程的封装处理.关于单例bean的线程安全和并发问题需要开发者自行去搞定.但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和 ...

  6. Spring框架中的单例Beans是线程安全的么?

    Spring框架并没有对单例bean进行任何多线程的封装处理.关于单例bean的线程安全和并发问题需要开发者自行去搞定.但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和 ...

  7. Spring5源码解析-论Spring DispatcherServlet的生命周期

    Spring Web框架架构的主要部分是DispatcherServlet.也就是本文中重点介绍的对象. 在本文的第一部分中,我们将看到基于Spring的DispatcherServlet的主要概念: ...

  8. 3.3 Spring5源码---循环依赖过程中spring读取不完整bean的最终解决方案

    根据之前解析的循环依赖的源码, 分析了一级缓存,二级缓存,三级缓存的作用以及如何解决循环依赖的. 然而在多线程的情况下, Spring在创建bean的过程中, 可能会读取到不完整的bean. 下面, ...

  9. Spring5源码解析_IOC之容器的基本实现

    前言: 在分析源码之前,我们简单回顾一下SPring核心功能的简单使用: 容器的基本用法 Bean是Spring最核心的东西,Spring就像是一个大水桶,而Bean就是水桶中的水,水桶脱离了水就没有 ...

随机推荐

  1. React篇-报错信息:warning: Can't call setState (or forceUpdate) on an unmounted component.

    报错信息是: Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but ...

  2. sqlite基本操作

    sqlite准备步骤; .下载:https://www.sqlite.org/download.html: sqlite-dll-win64-3250200.zip 和 sqlite-tools-wi ...

  3. HTTP协议和WEB框架

    一.HTTP协议 <<HTTP权威指南>>读书笔记:https://www.cnblogs.com/qcssmd/p/5508150.html 一.HTTP简介 HTTP协议是 ...

  4. windows系统上利用putty通过SSH连接亚马逊AWS服务器

    1. 找到在购买亚马逊的AWS服务器时保存的密钥文件(假设为abc.pem). 2.打开PuTTYgen,如下图,点击图中1处的“load”,找到abc.pem文件所在的位置,并选择abc.pem,确 ...

  5. DropzoneJS是一个提供文件拖拽上传并且提供图片预览的开源类库.

    DropzoneJS是一个提供文件拖拽上传并且提供图片预览的开源类库. 它是轻量级的,不依赖任何其他类库(如JQuery)并且高度可定制. 试试看! 将文件拖至此处或点击上传.(这仅仅是 dropzo ...

  6. SpringMVC连接多数据源配置

    在spring-config-datasource.xml中配置: <ds:ibatis-config> <ds:sql-map-client id="sqlMapClie ...

  7. ERROR [main] zookeeper.RecoverableZooKeeper: ZooKeeper create failed after 4 attempts

    ERROR [main] zookeeper.RecoverableZooKeeper: ZooKeeper create failed after 4 attempts ERROR [main] m ...

  8. .yml文件格式

    http://yaml.org/ YAML: YAML Ain't Markup Language What It Is: YAML is a human friendly data serializ ...

  9. iOS 新浪微博-5.3 首页微博列表_集成图片浏览器

    实际上,我们可以使用李明杰在教程里集成的MJPhotoBrowser,地址: http://code4app.com/ios/快速集成图片浏览器/525e06116803fa7b0a000001 使用 ...

  10. ida脚本函数

    #打印光标所在位置函数中地址和汇编代码 startaddr=GetFunctionAttr(ea, FUNCATTR_START) items = idautils.FuncItems(startad ...