简介
在之前的一些学习过程当中,对spring框架的一些设计思路和基本实现做了一些了解。结合书本的描述和自己的一些分析做一个初步的分析。这里是针对最早期spring框架实现的一个雏形进行的分析,和现在最新版本的比起来虽然简陋很多,但是少了很多纷繁芜杂的细节,对于我们理解它的设计思想更加方便一些。
spring框架之前
在使用spring框架之前,我们如果要开发一些应用,并使得它们能够很好工作的话,需要考虑和使用到比较多的地方就是应用组件的配置以及组合。比如说,我们怎么配置一个类,使得它能够被其他的类使用起来而不至于太麻烦。同时,根据应用的需要,一些对应的应用的配置该用什么样的方式来处理?是使用properties文件还是使用xml文件?不管使用哪一种文件,有没有一种统一的方式来读取和解析配置文件呢?我们希望有一个框架来解决这个问题而使得自己更多的集中在系统业务逻辑上面。
除了上述的问题,还有各种零零散散的问题,这里就不再赘述了。总的来说,怎么样构造出一个松散耦合的系统,使得他们易于配置和组合是一个值得深入思考的问题。在spring的设计思路里,就直接使用了JavaBean的思路。
JavaBean
首先一个,什么是JavaBean呢?这好像是一个很不起眼但又有点模糊的概念。从JavaBean官方的文档说明来看,它首先是一个标准。对于一个JavaBean来说,它有这么几个属性:
1. 它是一个可序列化的对象(通常指实现serializable接口,能够正确的实现序列化和反序列化)。
2. 它所有的属性是私有的,所有对这些属性的访问都是通过get/set方法来访问。
3. 它有默认的无参数构造函数。
从这几点看来,这更像是一个我们日常设计类和编码的一个规范。因为我们大多数的实现也会按照这个规范来。现在回过头来看,为什么要采用这种方式呢?
其实这种方式有一个比较好的地方,对于每个对象自己的属性来说,它不是直接暴露在外面,而是通过给定的的一些约定的方法来访问。这样就正好符合了我们设计思想中的紧内聚和松耦合的意思。从更高粒度的角度来看,如果每一个组件都采用JavaBean的方式来构造的话,我们的软件构造就相当于拼接一块块的组件。这些组件的拼接就取决于这些约定俗成的JavaBean访问方式。
当然,关于JavaBean的这几个特性的描述只是它的基础。在javaee的官方规范上提供了对JavaBean这种组件模型的详细支持,它使得我们可以很方便的将软件组件组合在一起。我们还有必要对这些官方支持的api作一个进一步的了解。
JavaBean相关的API
在深入的去看spring框架的一些详细设计和实现之前,还有一个需要详细了解的就是几个相关的API。在java.beans 这个包里包含了一系列和JavaBean相关特性的API。其中比较常用的几个类如下:
在实际的应用里,我们会使用Introspector来分析Java Bean的方法和属性。Introspector通过几个静态方法getBeanInfo(Class)来获取某个给定的类所对应的元数据信息。这个信息统一包含在BeanInfo类里。BeanInfo里面提供了关于method, property, event等bean相关的基础信息。而且对于这几类信息分别提供了BeanDescriptor, MethodDescriptor, PropertyDescriptor等描述类信息。通过这几个类,我们就能得到给定一个类里的详细信息。通过这种方式相当于得到了一个类的所有元数据信息。这样,不管我们定义的Bean是什么具体类型,我们都可以通过这种方式来获取到它们的基础信息。这几个部分在后续详细实现的分析里会起到很大的作用。
在这里,我们可能会有一个疑问。在什么地方我们会用上这些个api呢?这些api有什么用呢?在前面我们提到过一点,就是spring框架的设计思想里考虑过要采用JavaBean的方式来构造和组合各种对象。如果有使用过spring框架的经验的话可能会更好理解一点。在spring框架里,我们会定义一些类,然后在代码里或者xml文件里将他们关联起来。而这里的关联和耦合就类似于JavaBean里头属性的设置。那么这里就有一个问题,在实际的工程中,我们会定义一系列的类,然后在框架运行的时候会创建对应的对象,可能为单例的,也可能非单例的。那么这意味着我们需要有一个地方来保存这些类的元数据信息,以方便后面创建对象。然后在后面需要建立对象来进行耦合的时候再根据这些信息来构建。在具体的应用里,每个类它的方法和属性都是不一样的,我们如果要构造这个类的对象的话是没法根据每个类的具体情况来构建的,肯定需要一种比较通用的方式来构造。那么这就需要一种统一抽象的方式来构造和记录他们。
在前面的讨论里,我们已经看到通过Introspector得到的Bean的属性,方法和事件等信息。如果把这些信息给组织起来,在后面构建对象的时候就会方便很多的。
三个层面的抽象
在spring框架的设计里,将对象容器设计抽象成3个层面。BeanWrapper, BeanFactory和ApplicationContext。每个层面都是基于前一个层面为基础。这里,我们会根据它们的具体实现来讨论。
BeanWrapper
BeanWrapper这一层主要是用来操作JavaBean对象和属性的。比如说我们在配置文件里定义了一个bean,而且这个bean还有它的若干个暴露出来的bean方法。我们怎么来设置它的值和对应的属性呢?我们怎么来获取它的值和属性呢?在前面的讨论里我们也提到,通过一些JavaBean的api可以拿到一些bean的描述信息,这些信息该怎么组织来方便我们操作和构造bean呢?这些就是我们这一层所需要考虑的。在这一层里,相关的几个类如下图:
我们针对它们的实现详细看过来。
我们一开始最关心的肯定就是怎么来获取和设置属性的值。所以它们最重要的就是要提供一组getProperty, setProperty的方法。在BeanWrapper里面就定义了一系列相关的方法,它本身是一个接口,相关的几个方法如下:
public interface BeanWrapper {
/**
* Set a property value. This method is provided for convenience only.
* The setPropertyValue(PropertyValue)
* method is more powerful, providing the ability to set indexed properties etc.
* @param propertyName name of the property to set value of
* @param value the new value
*/
void setPropertyValue(String propertyName, Object value) throws PropertyVetoException, BeansException;
void setPropertyValue(PropertyValue pv) throws PropertyVetoException, BeansException;
/**
* Get the value of a property
* @param propertyName name of the property to get the value of
* @return the value of the property.
* @throws FatalBeanException if there is no such property,
* if the property isn't readable or if the property getter throws
* an exception.
*/
Object getPropertyValue(String propertyName) throws BeansException;
void setPropertyValues(Map m) throws BeansException;
void setPropertyValues(PropertyValues pvs) throws BeansException;
void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, PropertyValuesValidator pvsValidator) throws BeansException;
/** Get the property descriptor for a particular property, or null if there
* is no such property
* @param propertyName property to check status for
* @return the property descriptor for a particular property, or null if there
* is no such property
*/
PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException;
/**
* Return the bean wrapped by this object.
* Cannot be null
* @return the bean wrapped by this object
*/
Object getWrappedInstance();
/**
* Change the wrapped object. Implementations are required
* to allow the type of the wrapped object to change.
* @param obj wrapped object that we are manipulating
*/
void setWrappedInstance(Object obj) throws BeansException;
/**
* Return the class of the wrapped object
* @return the class of the wrapped object
*/
Class getWrappedClass();
/**
* Invoke the named method. This interface is designed
* to encourage working with bean properties, rather than methods,
* so this method shouldn't be used in most cases,
* but it's also useful to provide a simple means to invoking
* a named method.
* @param methodName name of the method to invoke
* @param args args to pass
* @return follows java.util.Method.invoke(). Void calls
* return null; primitives are wrapped as objects
*/
Object invoke(String methodName, Object[] args) throws BeansException;
}
这里省略了一部分代码。主要的几个方法除了getProperty, setProperty,还有getWrappedInstance, getWrappedClass, invoke等方法。这就相当于,如果我们有一个实现该接口的对象。针对给定的类型,我们就可以通过统一的这几个方法来设置它的属性值或者调用它的方法了。这样,我们就达到了一个类似于反射的效果。
在类里面依赖的两个类PropertyValue和PropertyValues是两个包装类。其中PropertyValue表示我们用来设置property传递的参数,它包含name, value两个部分,表示我们要设置的属性名和值。而PropertyValues则用来构造多个PropertyValue,方便实现多个PropertyValue。
所以,这里实现的重点就是BeanWrapperImpl。它是对BeanWrapper接口的详细实现。
在BeanWrapperImpl里,我们会看到如下几个重要的成员:
/** The wrapped object */
private Object object;
private CachedIntrospectionResults cachedIntrospectionResults;
object表示我们wrapper里要包装的对象。用我们这个类包装好的对象就可以通过前面的get/setProperty来访问对象了。而cachedIntrospectionResults则是用来保存前面Introspector分析的类元数据的。
我们先看一下CachedIntrospectionResults里面的结构。
private static HashMap $cache = new HashMap();
//---------------------------------------------------------------------
// Instance data
//---------------------------------------------------------------------
private BeanInfo beanInfo;
/** Property desciptors keyed by property name */
private HashMap propertyDescriptorMap;
/** Property desciptors keyed by property name */
private HashMap methodDescriptorMap;
这是它里面定义的几个成员变量。其中$cache变量是一个static的全局HashMap,它主要保存的是以class为key,CachedIntrospectionResults对象为value的结构。这样,对于每个类型的class来说,它会在全局的cache里保存一份且仅有一份信息。这些正好就是用来描述类元数据的。
而其他的3个类成员变量则分别记录了BeanInfo信息, HashMap<String, PropertyDescriptor>, HashMap<String, MethodDescriptor>信息。那么,它们几个的信息是如何被构造出来的呢?主要取决于如下方法:
private CachedIntrospectionResults(Class clazz) throws BeansException {
try {
logger.info("Getting BeanInfo for class " + clazz);
beanInfo = Introspector.getBeanInfo(clazz);
logger.fine("Caching PropertyDescriptors for class " + clazz);
propertyDescriptorMap = new HashMap();
// This call is slow so we do it once
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (int i = 0; i < pds.length; i++) {
logger.fine("Found property [" + pds[i].getName() + "] of type [" + pds[i].getPropertyType() + "]; editor=[" + pds[i].getPropertyEditorClass() + "]");
propertyDescriptorMap.put(pds[i].getName(), pds[i]);
}
logger.fine("Caching MethodDescriptors for class " + clazz);
methodDescriptorMap = new HashMap();
// This call is slow so we do it once
MethodDescriptor[] mds = beanInfo.getMethodDescriptors();
for (int i = 0; i < mds.length; i++) {
logger.fine("Found method [" + mds[i].getName() + "] of type [" + mds[i].getMethod().getReturnType() + "]");
methodDescriptorMap.put(mds[i].getName(), mds[i]);
}
}
catch (IntrospectionException ex) {
throw new FatalBeanException("Cannot get BeanInfo for object of class [" + clazz.getName() + "]", ex);
}
}
在这里CachedIntrospectionResults通过一个私有的带class类型参数的构造函数来解析传入的class对象。通过获取它的BeanInfo, PropertyDescriptor, MethodDescriptor等信息来构造出这个信息。同时CachedIntrospectionResults里的forClass方法保证这个构造函数里获取的对象会被保存到全局的$cache这个HashMap中来:
public static CachedIntrospectionResults forClass(Class clazz) throws BeansException {
Object o = $cache.get(clazz);
if (o == null) {
try {
o = new CachedIntrospectionResults(clazz);
}
catch (BeansException ex) {
o = ex;
}
$cache.put(clazz, o);
}
else {
logger.finest("Using cached introspection results for class " + clazz);
}
// o is now an exception or CachedIntrospectionResults
// We already have data for this class in the cache
if (o instanceof BeansException)
throw (BeansException) o;
return (CachedIntrospectionResults) o;
}
上述代码里前10行就是典型的一个创建对象并将对象加入缓存的操作手法。有了CachedIntrospectionResults提供的解析类元数据的功能之后,BeanWrapperImpl里其他的实现就是基于它来对包装的对象进行属性设置了。
我们先看构造函数相关的部分,在BeanWrapperImpl的构造函数里,他们基本上都调用了一个方法setObject,它的实现如下:
private void setObject(Object object) throws BeansException {
if (object == null)
throw new FatalBeanException("Cannot set BeanWrapperImpl target to a null object", null);
this.object = object;
if (cachedIntrospectionResults == null || !cachedIntrospectionResults.getBeanClass().equals(object.getClass())) {
cachedIntrospectionResults = CachedIntrospectionResults.forClass(object.getClass());
}
setEventPropagationEnabled(this.eventPropagationEnabled);
// assert: cachedIntrospectionResults != null
}
这部分主要是通过设置对应的对象和解析出对应的CachedIntrospectionResult对象以方便后面使用。需要注意的是,只要我们每调用一次CachedIntrospectionResult的forClass方法,这个对象对应的class元数据就会被解析后放到cache里去了。
我们再来看看setPropertyValue的实现:
public void setPropertyValue(PropertyValue pv) throws PropertyVetoException, BeansException {
if (isNestedProperty(pv.getName())) {
try {
BeanWrapper nestedBw = getBeanWrapperForNestedProperty(this, pv.getName());
nestedBw.setPropertyValue(new PropertyValue(getFinalPath(pv.getName()), pv.getValue()));
return;
}
catch (NullValueInNestedPathException ex) {
// Let this through
throw ex;
}
catch (FatalBeanException ex) {
// Error in the nested path
throw new NotWritablePropertyException(pv.getName(), getWrappedClass());
}
}
// WHAT ABOUT INDEXED PROEPRTIES!?
int pos = pv.getName().indexOf(NESTED_PROPERTY_SEPARATOR);
// Handle nested properties recursively
if (pos > -1) {
String nestedProperty = pv.getName().substring(0, pos);
String nestedPath = pv.getName().substring(pos + 1);
logger.fine("Navigating to property path '" + nestedPath + "' of nested property '" + nestedProperty + "'");
// Could consider caching these, but they're not that expensive to instantiate
BeanWrapper nestedBw = new BeanWrapperImpl(getPropertyValue(nestedProperty), false);
nestedBw.setPropertyValue(new PropertyValue(nestedPath, pv.getValue()));
return;
}
if (!isWritableProperty(pv.getName()))
throw new NotWritablePropertyException(pv.getName(), getWrappedClass());
PropertyDescriptor pd = getPropertyDescriptor(pv.getName());
Method writeMethod = pd.getWriteMethod();
Method readMethod = pd.getReadMethod();
Object oldValue = null; // May stay null if it's not a readable property
PropertyChangeEvent propertyChangeEvent = null;
try {
if (readMethod != null && eventPropagationEnabled) {
// Can only find existing value if it's a readable property
try {
oldValue = readMethod.invoke(object, new Object[] { });
}
catch (Exception ex) {
// The getter threw an exception, so we couldn't retrieve the old value.
// We're not really interested in any exceptions at this point,
// so we merely log the problem and leave oldValue null
logger.logp(Level.WARNING, "BeanWrapperImpl", "setPropertyValue",
"Failed to invoke getter '" + readMethod.getName() + "' to get old property value before property change: getter probably threw an exception",
ex);
}
}
// Old value may still be null
propertyChangeEvent = createPropertyChangeEventWithTypeConversionIfNecessary(object, pv.getName(), oldValue, pv.getValue(), pd.getPropertyType());
// May throw PropertyVetoException: if this happens the PropertyChangeSupport
// class fires a reversion event, and we jump out of this method, meaning
// the change was never actually made
if (eventPropagationEnabled) {
vetoableChangeSupport.fireVetoableChange(propertyChangeEvent);
}
// Make the change
if (logger.isLoggable(Level.FINEST))
logger.finest("About to invoke write method [" + writeMethod + "] on object of class '" + object.getClass().getName() + "'");
writeMethod.invoke(object, new Object[] { propertyChangeEvent.getNewValue() });
if (logger.isLoggable(Level.FINEST))
logger.finest("Invoked write method [" + writeMethod + "] ok");
// If we get here we've changed the property OK and can broadcast it
if (eventPropagationEnabled)
propertyChangeSupport.firePropertyChange(propertyChangeEvent);
}
catch (InvocationTargetException ex) {
if (ex.getTargetException() instanceof PropertyVetoException)
throw (PropertyVetoException) ex.getTargetException();
if (ex.getTargetException() instanceof ClassCastException)
throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex);
throw new MethodInvocationException(ex.getTargetException(), propertyChangeEvent);
}
catch (IllegalAccessException ex) {
throw new FatalBeanException("illegal attempt to set property [" + pv + "] threw exception", ex);
}
catch (IllegalArgumentException ex) {
throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex);
}
} // setPropertyValue
这部分的代码看起来比较复杂,因为前30行主要是用来处理嵌套的属性以及索引的属性方式访问。 而从第33行到第65行主要是根据readMethod来获取属性旧的值,并在如果允许触发属性变化事件的情况下去触发这个事件。而后面的部分则是通过writeMethod.invoke()方法来真正修改property值。至此,我们会发现,当给定一个对象的属性名和属性值的时候,我们是通过反射调用它的writeMethod来设置它的值的。那么对于给定属性名获取它的属性值呢?这就是getPropertyValue的实现:
public Object getPropertyValue(String propertyName) throws BeansException {
if (isNestedProperty(propertyName)) {
BeanWrapper nestedBw = getBeanWrapperForNestedProperty(this, propertyName);
logger.finest("Final path in nested property value '" + propertyName + "' is '" + getFinalPath(propertyName) + "'");
return nestedBw.getPropertyValue(getFinalPath(propertyName));
}
PropertyDescriptor pd = getPropertyDescriptor(propertyName);
Method m = pd.getReadMethod();
if (m == null)
throw new FatalBeanException("Cannot get scalar property [" + propertyName + "]: not readable", null);
try {
return m.invoke(object, null);
}
catch (InvocationTargetException ex) {
throw new FatalBeanException("getter for property [" + propertyName + "] threw exception", ex);
}
catch (IllegalAccessException ex) {
throw new FatalBeanException("illegal attempt to get property [" + propertyName + "] threw exception", ex);
}
}
它的实现就好像是写属性的对称实现一样,它是通过调用readMethod里的方法来获取值的。本质上还是反射。
有了前面的对象封装之后,BeanWrapper接口的getWrappedClass和getWrappedInstance就简单了很多:
public Class getWrappedClass() {
return object.getClass();
}
public Object getWrappedInstance() {
return object;
}
在这里还有一个重要的方法就是invoke:
public Object invoke(String methodName, Object[] args) throws BeansException {
try {
MethodDescriptor md = this.cachedIntrospectionResults.getMethodDescriptor(methodName);
if (logger.isLoggable(Level.FINE))
logger.fine("About to invoke method [" + methodName + "]");
Object returnVal = md.getMethod().invoke(this.object, args);
if (logger.isLoggable(Level.FINE))
logger.fine("Successfully invoked method [" + methodName + "]");
return returnVal;
}
catch (InvocationTargetException ex) {
//if (ex.getTargetException() instanceof ClassCastException)
// throw new TypeMismatchException(propertyChangeEvent, pd.getPropertyType(), ex);
// CHECK!!!!
throw new MethodInvocationException(ex.getTargetException(), null);
}
catch (IllegalAccessException ex) {
throw new FatalBeanException("Illegal attempt to invoke method [" + methodName + "] threw exception", ex);
}
catch (IllegalArgumentException ex) {
throw new FatalBeanException("Illegal argument to method [" + methodName + "] threw exception", ex);
}
}
这里的实现也很简单,因为有前面的cache保存好对应的methodDescriptor了,只要找到对应的method,再去调用它就行了。
总的来说,上述的BeanWrapperImpl的实现是通过将目标对象包装起来,在初始化的时候通过 CachedIntrospectionResults解析类元数据并设置缓存。在后续的属性设定里通过找到对ing的元数据信息再调用对应的反射方法就可以了。
有了前面BeanWrapperImpl实现的基础,我们对于对象属性的操作就可以采用另外一种方式了。比如说我们有一个如下的接口:
public interface ITestBean {
int getAge();
void setAge(int age);
String getName();
void setName(String name);
ITestBean getSpouse();
void setSpouse(ITestBean spouse);
}
同时有实现这个接口的对象TestBean。我们通过包装这个TestBean对象来操作属性值的方法如下:
TestBean bean = new TestBean();
BeanWrapper bw = new BeanWrapperImpl(bean);
bw.setPropertyValue("age", new Integer(32));
bw.setPropertyValue("name", "frank");
bw.setPropertyValue("spouse", new TestBean());
bw.setPropertyValue("spouse.name", "kerry");
bw.setPropertyValue("spouse.spouse", bean);
Integer age = (Integer) bw.getPropertyValue("age");
String name = (String) bw.getPropertyValue("name");
String spouseName = (String) bw.getPropertyValue("spouse.name");
BeanFactory
有了前面部分对BeanWrapper的定义和实现,我们得到了一个操作对象和属性的基础。当然,在实际的应用中,光有这个东西还是远远不够的。还有几个点我们需要考虑:
1. 我们希望通过框架的支持来将应用给组合起来。在前面的基础里,如果需要组合各组件的话,还太低级。我们希望能够通过一种统一配置的方式来获取bean。
2. 另外一个,我们也希望这个框架能够提供一种统一的方式来读取配置属性和创建对象。
3. 这些创建的对象对于应用来说是可见的,由于具体应用的复杂性,它可能构成一个复杂的对象图,这些都需要进行管理。我们也需要使得这些创建的每个对象都是唯一的。
正是基于上述的原因,这里定义了更高的一层,来提供一些统一的方式来读取应用配置信息,同时也提供统一的对象创建和管理功能。提到对于对象的创建和管理,这里还有一个需要提到的,就是它和singleton以及factory模式应用的比较。在一些场景下,我们实际上只使用了唯一的一个对象,于是我们可以考虑用singleton或者factory方法的方式来创建它们。但是这种方法存在着一些问题:
1. 我们希望应用更多的是直接面向接口编程,而如果使用singleton或者factory的时候,就直接和具体的类耦合了,如果根据需要对一些实现做调整的话,会比较困难。
2. 每个单例对象可能需要去读取一些配置信息,比如properties文件或者JDBC等。在每个实现里对于配置的读取和管理将散落在各个地方。这既牵扯了一些无关的精力又使得实现更加混乱。好多和业务无关的东西也搅和到里面了。
3. 还有一个问题就是像singleton模式本身不灵活,如果我们需要不仅仅一个对象的时候,在原有的实现里将会很麻烦,需要做大的改动。
基于上述的讨论,spring里对于BeanFactory相关的实现类结构如下图:
我们先从BeanFactory这边看过来,它的实现如下:
public interface BeanFactory {
Object getBean(String name) throws BeansException;
Object getBean(String name, Class requiredType) throws BeansException;
}
后续详细的实现就是围绕着这个接口进行的。有了这些详细的实现,我们后面要定义和使用对象的方式如下:
Mylnterface mi = (Mylnterface) getBean("mylnterface");
Mylnterface mi = (Mylnterface) getBean("mylnterface", Mylnterface.class);
通过这种方式,我们实现了在BeanFactory里实现具体的配置管理,而在应用层面来说,它只需要关心具体的业务逻辑。这样实现了配置管理和应用代码的分离。我们可以在具体的BeanFactory实现里来选择具体的配置读取方式和解析方法。这样也可以支持不同格式的应用配置。
在BeanFactory的实现里,AbstractBeanFactory的实现非常重要,它对getBean方法的实现如下:
public final Object getBean(String name) {
BeanDefinition bd = getBeanDefinition(name);
return bd.isSingleton() ? getSharedInstance(name) : createBean(name);
}
这里采用了template method的模式思路,getBeanDefinition的实现在这里是一个抽象方法,具体的实现由它的子类来定义。我们来看看getSharedInstance()和createBean()这两个方法。getSharedInstance()方法的实现如下:
private final synchronized Object getSharedInstance(String name) throws BeansException {
Object o = sharedInstanceCache.get(name);
if (o == null) {
logger.info("Cached shared instance of Singleton bean '" + name + "'");
o = createBean(name);
sharedInstanceCache.put(name, o);
}
else {
if (logger.isLoggable(Level.FINE))
logger.fine("Returning cached instance of Singleton bean '" + name + "'");
}
return o;
}
在AbstractBeanFactory里定义了一个HashMap,用来保存bean名字和创建的bean对象:
private HashMap sharedInstanceCache = new HashMap();
所以每次我们调用这个方法的时候会首先去这个全局缓存里查找bean的名字以提高bean查找的效率。这个方法的实现也在一定条件下调用createBean()方法。createBean()方法实现如下:
private Object createBean(String name) throws BeansException {
Object bean = getBeanWrapperForNewInstance(name).getWrappedInstance();
invokeInitializerIfNecessary(bean);
return bean;
}
它真正的详细实现放在getBeanWrapperForNewInstance()方法里。我们继续跟进看它的详细实现:
private BeanWrapper getBeanWrapperForNewInstance(String name) throws BeansException {
logger.fine("getBeanWrapperForNewInstance (" + name + ")");
BeanDefinition bd = getBeanDefinition(name);
logger.finest("getBeanWrapperForNewInstance definition is: " + bd);
BeanWrapper instanceWrapper = null;
if (bd instanceof RootBeanDefinition) {
RootBeanDefinition rbd = (RootBeanDefinition) bd;
instanceWrapper = rbd.getBeanWrapperForNewInstance();
}
else if (bd instanceof ChildBeanDefinition) {
ChildBeanDefinition ibd = (ChildBeanDefinition) bd;
instanceWrapper = getBeanWrapperForNewInstance(ibd.getParentName());
}
// Set our property values
if (instanceWrapper == null)
throw new FatalBeanException("Internal error for definition [" + name + "]: type of definition unknown (" + bd + ")", null);
PropertyValues pvs = bd.getPropertyValues();
applyPropertyValues(instanceWrapper, pvs, name);
return instanceWrapper;
}
从上述的代码里我们可以看到,我们需要通过获取BeanDefinition才能够获得定义的BeanWrapper对象。然后我们再通过applyPropertyValues()方法来设置对象的具体属性。所以说,通过这个BeanDefinition的信息,我们可以构造出对应的bean对象了。关于BeanDefinition的定义以及实现我们接着会进一步分析。我们先看看applyPropertyValues()的实现:
private void applyPropertyValues(BeanWrapper bw, PropertyValues pvs, String name) throws BeansException {
if (pvs == null)
return;
MutablePropertyValues deepCopy = new MutablePropertyValues(pvs);
PropertyValue[] pvals = deepCopy.getPropertyValues();
// Now we must check each PropertyValue to see whether it
// requires a runtime reference to another bean to be resolved.
// If it does, we'll attempt to instantiate the bean and set the reference.
for (int i = 0; i < pvals.length; i++) {
if (pvals[i].getValue() != null && (pvals[i].getValue() instanceof RuntimeBeanReference)) {
RuntimeBeanReference ref = (RuntimeBeanReference) pvals[i].getValue();
try {
// Try to resolve bean reference
logger.fine("Resolving reference from bean [" + name + "] to bean [" + ref.getBeanName() + "]");
Object bean = getBean(ref.getBeanName());
// Create a new PropertyValue object holding the bean reference
PropertyValue pv = new PropertyValue(pvals[i].getName(), bean);
// Update mutable copy
deepCopy.setPropertyValueAt(pv, i);
}
catch (BeansException ex) {
throw new FatalBeanException("Can't resolve reference to bean [" + ref.getBeanName() + "] while setting properties on bean [" + name + "]", ex);
}
} // if this was a runtime reference to another bean
} // for each property
// Set our (possibly massaged) deepCopy
try {
bw.setPropertyValues(deepCopy);
}
catch (FatalBeanException ex) {
// Improve the message by showing the context
throw new FatalBeanException("Error setting property on bean [" + name + "]", ex);
}
}
上面的实现看起来有点复杂, 其实这是因为我们要设置它所有的propertyValue。而这里设置的时候会存在一些需要设置的属性也是bean对象引用。所以这里需要递归的调用getBean方法来获取引用。这里获得了所有的propertyValues信息之后再通过BeanWrapper的实现将这些property设置上。
在前面我们提到过,我们使用了template method的方法来使得装载bean的过程可扩展。这是在AbstractBeanFactory里定义的一个抽象方法。在ListableBeanFactoryImpl的实现如下:
protected final BeanDefinition getBeanDefinition(String prototypeName) throws NoSuchBeanDefinitionException {
BeanDefinition bd = (BeanDefinition) beanDefinitionHash.get(prototypeName);
if (bd == null)
throw new NoSuchBeanDefinitionException(prototypeName, toString());
return bd;
}
因为这里定义了一个beanDefinitionHash的HashMap:
private Map beanDefinitionHash = new HashMap();
所以实际上在调用这个getBeanDefinition这部分之前,它就事先将这些BeanDefinition给解析加载好了。在ListableBeanFactoryImpl里有registerBeanDefinitions方法,它是通过这个方法解析properties文件,来实现解析和加载的效果。它的实现有点复杂, 首先这两个方法的实现如下:
public final int registerBeanDefinitions(ResourceBundle rb, String prefix) throws BeansException {
// Simply create a map and call overloaded method
Map m = new HashMap();
Enumeration keys = rb.getKeys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
m.put(key, rb.getObject(key));
}
return registerBeanDefinitions(m, prefix);
}
public final int registerBeanDefinitions(Map m, String prefix) throws BeansException {
if (prefix == null)
prefix = "";
int beanCount = 0;
Set keys = m.keySet();
Iterator itr = keys.iterator();
while (itr.hasNext()) {
String key = (String) itr.next();
if (key.startsWith(prefix)) {
// Key is of form prefix<name>.property
String nameAndProperty = key.substring(prefix.length());
int sepIndx = nameAndProperty.indexOf(SEPARATOR);
if (sepIndx != -1) {
String beanName = nameAndProperty.substring(0, sepIndx);
logger.fine("Found bean name '" + beanName + "'");
if (beanDefinitionHash.get(beanName) == null) {
// If we haven't already registered it...
registerBeanDefinition(beanName, m, prefix + beanName);
++beanCount;
}
}
else {
// Ignore it: it wasn't a valid bean name and property,
// although it did start with the required prefix
logger.fine("invalid name and property '" + nameAndProperty + "'");
}
} // if the key started with the prefix we're looking for
} // while there are more keys
return beanCount;
}
在这两个实现方法里,它是通过ResourceBundle首先解析properties文件,将文件的内容解析成一个HashMap之后再将这个map传入到registerBeanDefinition方法里做具体的解析。真正做详细解析并将解析结果封装成BeanDefinition的就是这个registerBeanDefinition方法。它的实现比较冗长,这里就不详细贴出来了。这个方法的主要实现思路如下:
它里面定义了一个成员变量:
MutablePropertyValues pvs = new MutablePropertyValues();
它会通过遍历传入的Map参数里所有的key元素,经过判断分析它所属的类别,然后来构造对应的PropertyValue对象。然后将这个对象添加到pvs里面。然后再通过这个pvs作为构造函数的参数之一构造BeanDefinition对象。最后再将我们设置的beanName和这个beanDefinition对象加入到全局的beanDefinitionHash里。
有了这个registerBeanDefinitions的实现,我们可以在应用里使用properties文件来设置bean相关的信息。比如说,我们有如下的properties文件:
rod.class=com.interface21.beans.TestBean
rod.name=Rod
rod.age=32
rod.spouse(ref)=kerry
kerry.class=com. i nterface21.beans.TestBean
kerry.name=Kerry
kerry.age=35
kerry.spouse(ref)=rod
这个时候,如果我们想要得到rod这个bean,之需要使用如下的代码:
TestBean rod = (TestBean) beanFactory.getBean("rod");
上述是ListableBeanFactoryImpl对于properties文件解析和构建bean的支持。在继承的类里XmlBeanFactory则提供了对xml文件解析和构建bean的支持。
它的几个主要构造函数如下:
public XmlBeanFactory(String filename) throws BeansException {
try {
logger.info("Loading XmlBeanFactory from file '" + filename + "'");
loadBeanDefinitions(new FileInputStream(filename));
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Can't open file [" + filename + "]", ex);
}
}
public XmlBeanFactory(InputStream is) throws BeansException {
loadBeanDefinitions(is);
}
public XmlBeanFactory(Document doc) throws BeansException {
loadBeanDefinitions(doc);
}
在构造函数里无一例外的都调用了loadBeanDefinitions方法。这里loadBeanDefinitions方法和前面的registerBeanDefinitions方法类似,只是它是通过解析xml文件来构造BeanDefinition。它支持的解析方式稍微有点不一样。假设我们有如下的xml定义文件:
<bean name="rod"
singleton="true"
class="com.interface21.beans.TestBea n ">
<property name="name">Rod</propert y >
<property name="age">31</property>
<property name="spouse" beanRef="true">kerry</property>
</bean>
<bean name="kerry" class="com.interface21.beans.TestBean">
<property name="name">Kerry</prope r ty>
<property name="age">34</property>
<property name="spouse" beanRef="true">rod</property>
</bean>
这个时候如果我们使用如下的代码:
TestBean rod = (TestBean) beanFactory.getBean("rod");
也一样起到了和前面构造bean对象的效果。
这样,我们对于创建一个bean的过程就算是有了一个大致的了解了。它主要还是依靠解析文件加载定义好的BeanDefinition来实现的。我们通过拿到BeanDefinition,再通过它创建对应的BeanWrapper对象,并设置相关的propertyValue到这个对象上。这里,我们还有必要对BeanDefinition的实现细节深入了解一下。
BeanDefinition
BeanDefinition相关的类结构如下图:
这里最高层的父类是BeanDefinition,它是一个接口,定义了如下两个方法:
public abstract interface BeanDefinition {
PropertyValues getPropertyValues();
/**
* Is this a <b>Singleton</b>, with a single, shared
* instance returned on all calls,
* or should be apply the <b>Prototype</b> design pattern,
* with each caller requesting an instance getting an independent
* instance?
* @return whether this is a Singleton
*/
boolean isSingleton();
}
在这个接口里的两个方法,其中isSingleton和getPropertyValues的实现比较简单。在后续继承实现的类里AbstractBeanDefinition提供了基本的构造函数和基础实现,这部分的代码实现如下:
public abstract class AbstractBeanDefinition implements BeanDefinition {
private boolean singleton;
/** Property map */
private PropertyValues pvs;
protected AbstractBeanDefinition(PropertyValues pvs, boolean singleton) {
this.pvs = pvs;
this.singleton = singleton;
}
protected AbstractBeanDefinition() {
this.singleton = true;
}
public final boolean isSingleton() {
return singleton;
}
public PropertyValues getPropertyValues() {
return pvs;
}
}
还有几个后续的实现类里像AbstractRootBeanDefinition和DefaultRootBeanDefinition都提供了带有singleton和propertyValues参数的构造函数。
还有一个值得注意的地方就是在前面分析BeanFactory的时候,我们看到有这么一部分代码:
RootBeanDefinition rbd = (RootBeanDefinition) bd;
instanceWrapper = rbd.getBeanWrapperForNewInstance();
RootBeanDefinition类可以构造出对应的BeanWrapper对象。那么,这个部分的代码是如何实现的呢?
在AbstractRootBeanDefinition类里有getBeanWrapperForNewInstance的实现,它这部分的代码实现如下:
/**
* Subclasses must implement this, to create bean
* wrappers differently or perform custom preprocessing
* @return a BeanWrapper for the wrapped bean
*/
protected abstract BeanWrapper newBeanWrapper();
/**
* Given a bean wrapper, add listeners
*/
public final BeanWrapper getBeanWrapperForNewInstance() throws BeansException {
BeanWrapper bw = newBeanWrapper();
// Add any listeners
// GO IN CHILD ALSO!? promote?
List listeners = getListeners();
for (int i = 0; i < listeners.size(); i++) {
ListenerRegistration lr = (ListenerRegistration) listeners.get(i);
if (lr.getListener() instanceof VetoableChangeListener) {
VetoableChangeListener l = (VetoableChangeListener) lr.getListener();
if (lr.getPropertyName() == null)
bw.addVetoableChangeListener(l);
else
bw.addVetoableChangeListener(lr.getPropertyName(), l);
}
else if (lr.getListener() instanceof PropertyChangeListener) {
PropertyChangeListener l = (PropertyChangeListener) lr.getListener();
if (lr.getPropertyName() == null)
bw.addPropertyChangeListener(l);
else
bw.addPropertyChangeListener(lr.getPropertyName(), l);
}
}
return bw;
}
它实际上创建BeanWrapper的工作还是放在抽象方法newBeanWrapper里。在具体的实现里DefaultRootBeanDefinition定义了对应的方法:
protected BeanWrapper newBeanWrapper() {
return new BeanWrapperImpl(getBeanClass());
}
而这里getBeanClass方法在AbstractRootBeanDefinition的构造函数里就基本上设置好class了。
ApplicationContext
在前面BeanFactory这个层面,我们解决了读取解析配置文件,定义统一的方式来管理创建的Bean对象等功能。这样就可以通过指定Bean的名字来获取它的bean对象。 如果为了可以更进一步的组合应用系统以及在运行时共享工作的对象,我们还有必要做一个更进一步的抽象。这就是ApplicationContext。这部分主要关注一下几个方面:
1. 通过observer模式的方式来发布事件。这种模式可以很好的达到组件的送耦合。
2. 在应用上下文中,实际上我们可能会有不同的子系统和模块,它们都有不同的配置信息。如果我们定义成一个层次化的application context的话,可以方便开发者来选择合适的层级来共享它的配置信息。
3. 方便在运行时在不同应用组件共享工作的对象。
4. 通过字符串来查找消息,可以来支持国际化。
5. 对测试提供支持。通过定义不同的应用场景,我们可以使得需要测试的应用运行在一个给定的application context里头,这样测试起来更加方便,而不用太依赖各种具体的服务或组件。
6. 通过这种方式也保证在不同类型的应用开发中使用统一的配置管理。
总的来说,ApplicationContext相关的类结构如下图:
这里最核心的类就是ApplicationContext,它继承了接口MessageSource和ListableBeanFactory。结合前面讲述BeanFactory的内容,我们会发现,后续ApplicationContext的实现也继承了BeanFactory里的功能。而MessageSource接口的定义更多的是为了支持消息的解析以及国际化的,它里面定义的接口如下:
public interface MessageSource {
String getMessage(String code, Locale locale, String defaultMessage);
String getMessage(String code, Locale locale) throws NoSuchMessageException;
}
这部分倒还比较简单。对于ApplicationContext接口来说,它增加了若干个方法,这里方法比较多,最重要的几个方法如下:
/** Return the parent context, or null if there is no parent,
* and this is the root of the context hierarchy.
* @return the parent context, or null if there is no parent
*/
ApplicationContext getParent();
/** Notify all listeners registered with this application of
* a web application event. Events may be framework events (such as RequestHandledEvent)
* or application-specific events.
* @param e event to publish
*/
void publishEvent(ApplicationEvent e);
/** Load or refresh the persistent representation of the configuration, which
* might for example be an XML file, properties file or relational database schema.
* @param servletConfig if the config is invalid
* @throws IOException if the config cannot be loaded
*/
void refresh() throws ApplicationContextException;
/**
* Put an object available for sharing. Note that this
* method is not synchronized. As with Java 2 collections,
* it's up to calling code to ensure thread safety.
* Also, this doesn't work in a cluster. It's
* analogous to putting something in a ServletContext.
* @param key object key
* @param o object to put
*/
void shareObject(String key, Object o);
我们在详细的实现分析里也主要根据这几个接口方法来。
首先看getParent方法。在ApplicationContext里,每一个application context都有一个parent context,如果它的当前parent contenxt为null,则表示这个application context就是parent context。这种设置有什么好处呢?主要就是如果当前application context需要查找一些bean或者解析一些message,当它发现当前的context下解析不了的话,它会尝试把这个请求向上传递给它的父context。这种模式看起来有点像java classloader的双亲委派模式。
在具体的实现里,getParent的定义主要是放在AbstractApplicationContext,和AbstractXmlApplicationContext等几个子类里。它们的构造函数里都有带ApplicationContext参数的方法。所以只要在构造它们的时候把对应的参数传进去就可以得到它了。在AbstractApplicationContext里有专门的成员变量:
private ApplicationContext parent;
我们再来看看publishEvent方法的实现。虽然这个方法的内容多一点,但是它比较好理解。因为它的实现就是利用了Observer pattern来实现消息的发布和通知的。它们相关的类关系如下图:
这部分的代码其实很好理解,在ApplicationEventMulticasterImpl里面有专门的成员:
private Set eventListeners = new HashSet();
通过它来实现ApplicationListener的注册和注销。剩下的就是observer pattern的基本套路了。
我们再来看看refersh方法的实现。它主要做的事就是刷新配置信息,也就是重新再读取一遍配置文件,它的实现如下:
public final void refresh() throws ApplicationContextException {
if (contextOptions!= null && !contextOptions.isReloadable())
throw new ApplicationContextException("Forbidden to reload config");
this.startupTime = System.currentTimeMillis();
refreshBeanFactory();
try {
loadOptions();
}
catch (BeansException ex) {
throw new ApplicationContextException("Unexpected error loading context options", ex);
}
try {
this.messageSource = (MessageSource) getBeanFactory().getBean(MESSAGE_SOURCE_BEAN_NAME);
}
catch (BeansException ex) {
logger.config("No MessageSource defined in ApplicationContext: using parent's");
this.messageSource = this.parent;
if (this.messageSource == null)
throw new ApplicationContextException("No MessageSource defined in WebApplicationContext and no parent", ex);
}
if ((this.messageSource instanceof NestingMessageSource) && this.parent != null) {
( (NestingMessageSource) messageSource).setParent(this.parent);
}
refreshListeners();
configureAllManagedObjects();
}
这部分代码相当于是具体刷新过程的一个拼接。里面实现具体刷新过程的方法有refreshBeanFactory, loadOptions, refreshListeners, configureAllManagedObjects这4个方法。其中refreshBeanFactory是一个抽象方法,它由具体的子类来实现。在这里,它的定义如下:
/**
* Subclasses must implement this method to perform the actual configuration load.
* @param servletConfig the app's ServletConfig to use to load the configuration.
* (We'll need this if it's inside the WAR.)
*/
protected abstract void refreshBeanFactory() throws ApplicationContextException;
在一个子类AbstractXmlApplicationContext里有这个方法的具体实现,它的具体实现如下:
protected void refreshBeanFactory() throws ApplicationContextException {
String identifier = "application context with display name [" + getDisplayName() + "'";
InputStream is = null;
try {
// Supports remote as well as local URLs
is = getInputStreamForBeanFactory();
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = db.parse(is);
parseDocument(doc);
}
catch (ParserConfigurationException ex) {
throw new ApplicationContextException("ParserConfiguration exception for " + identifier, ex);
}
catch (SAXException ex) {
throw new ApplicationContextException("XML document is invalid for " + identifier, ex);
}
catch (IOException ex) {
throw new ApplicationContextException("IOException parsing XML document for " + identifier, ex);
}
finally {
try {
if (is != null)
is.close();
}
catch (IOException ex) {
throw new ApplicationContextException("IOException closing stream for XML document for " + identifier, ex);
}
}
}
它实质上就是重新再解析一遍配置文件来得到最新的ListableBeanFactory。
loadOptions的方法如下:
private void loadOptions() throws BeansException {
if (this.contextOptions == null) {
// Try to load from bean
try {
this.contextOptions = (ContextOptions) getBeanFactory().getBean(OPTIONS_BEAN_NAME);
}
catch (NoSuchBeanDefinitionException ex) {
this.contextOptions = ContextOptions.DEFAULT_OPTIONS;
}
}
}
这部分主要是加载应用配置属性的一个bean对象。
而refreshListeners的方法则是要找到所有ApplicationListener的实现类,并重新更新和注册一遍这些listener:
private void refreshListeners() throws ApplicationContextException {
logger.config("Refreshing listeners");
String[] listenerNames = getBeanDefinitionNames(ApplicationListener.class);
logger.fine("Found " + listenerNames.length + " listeners in bean factory; names=" +
StringUtils.arrayToDelimitedString(listenerNames, ",") + "]");
for (int i = 0; i < listenerNames.length; i++) {
String beanName = listenerNames[i];
try {
Object bean = getBeanFactory().getBean(beanName);
ApplicationListener l = (ApplicationListener) bean;
configureManagedObject(l);
addListener(l);
logger.config("Bean listener added: [" + l + "]");
}
catch (BeansException ex) {
throw new ApplicationContextException("Couldn't load config listener with name '" + beanName+ "'", ex);
}
}
}
而configAllManagedObjects方法主要是针对所有注册定义好的bean对象做一个处理,凡是实现了ApplicationContextAware接口的对象都要设置一下它的父context:
private void configureAllManagedObjects() throws ApplicationContextException {
logger.config("Refreshing listeners");
String[] beanNames = getBeanDefinitionNames();
logger.fine("Found " + beanNames.length + " listeners in bean factory; names=" +
StringUtils.arrayToDelimitedString(beanNames, ",") + "]");
for (int i = 0; i < beanNames.length; i++) {
String beanName = beanNames[i];
try {
Object bean = getBeanFactory().getBean(beanName);
configureManagedObject(bean);
}
catch (BeansException ex) {
throw new ApplicationContextException("Couldn't instantiate object with name '" + beanName+ "'", ex);
}
}
}
还有一个比较重要的方法就是shareObject,它的实现则比较简单。在AbstractApplicationContext里有一个成员sharedObjects:
private HashMap sharedObjects = new HashMap();
对sharedObjects的读写就是对这个map的访问,所以它的实现如下:
/**
* @see ApplicationContext#sharedObject(String)
*/
public synchronized Object sharedObject(String key) {
return sharedObjects.get(key);
}
/**
* @see ApplicationContext#shareObject(String, Object)
*/
public synchronized void shareObject(String key, Object o) {
logger.info("Set shared object '" + key + "'");
sharedObjects.put(key, o);
}
非常简单,无需赘述。
这样,我们对于整个spring container的一个雏形设计和实现有了一个完整的分析。
总结
因为spring是一个功能相当强大的框架,对于它实现bean对象定义、解析、创建和组合等功能来说,要实现的功能也很复杂。在它的实现里,通过3个层面的抽象来达到这个目的。在最底层的BeanWrapper层实现了对创建对象的一种通用的构造和属性访问。我们可以通过给定属性的名字来进行各种读写访问,只要所有的对象定义的属性都符合JavaBean的规范。而在BeanFactory层面则实现了对各种常用配置文件的统一解析和支持,通过一种统一的访问来访问bean对象。而在ApplicationContext层面则增加了对于不同应用场景的支持,实现不同层面的配置共享。同时也提供了事件发布通知以及共享对象的方法,使得对于不同子系统之间的对象访问和共享更加方便。
这是基于spring最早期的雏形代码的分析,对于后续成熟框架的实现也还是有一定的帮助的。
参考材料
https://www.amazon.com/Expert-One-One-Design-Development/dp/0764543857/ref=sr_1_1?s=books&ie=UTF8&qid=1507469612&sr=1-1&keywords=expert+one+on+one+j2ee
http://javadude.com/articles/propedit/
https://stackoverflow.com/questions/3295496/what-is-a-javabean-exactly
http://www.oracle.com/technetwork/articles/javaee/spec-136004.html
http://www.wrox.com/WileyCDA/WroxTitle/Expert-One-on-One-J2EE-Design-and-Development.productCd-0764543857,descCd-DOWNLOAD.html
- 大小: 137.1 KB
- 大小: 178.7 KB
- 大小: 94.4 KB
- 大小: 141.7 KB
- 大小: 50.2 KB
- 大小: 58.5 KB
分享到:
相关推荐
基于Spring框架的汽车租赁系统分析与设计.docx基于Spring框架的汽车租赁系统分析与设计.docx基于Spring框架的汽车租赁系统分析与设计.docx基于Spring框架的汽车租赁系统分析与设计.docx基于Spring框架的汽车租赁系统...
Spring技术内幕:深入解析Spring架构与设计原理(第2部分) 《Spring技术内幕:深入解析Spring架构与设计原理》是Spring领域的问鼎之作,由业界拥有10余年开发经验的资深Java专家亲自执笔!Java开发者社区和Spring...
第三部分讲述了ACEGI安全框架、DM模块以及Flex模块等基于Spring的典型应用的设计与实现。 无论你是Java程序员、Spring开发者,还是平台开发人员、系统架构师,抑或是对开源软件源代码着迷的代码狂人,都能从本书中...
java毕业设计——基于spring boot的就业信息管理网站设计与实现(源码+数据库).zip java毕业设计——基于spring boot的就业信息管理网站设计与实现(源码+数据库).zip java毕业设计——基于spring boot的就业信息管理...
《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》从源代码的角度对Spring的内核和各个主要功能模块的架构、设计和实现原理进行了深入剖析。你不仅能从本书中参透Spring框架的出色架构和设计思想,还能从...
SpringContainer SpringContainer is a FrameLayout that supports overscroll on both top and bottom. Feel free to put any view(s) in. Based on that feature, it's very easy to do refreshing、 loading or ...
SpringCloud基于分布式的租售一体电商平台的设计与实现 SpringCloud基于分布式的租售一体电商平台的设计与实现 SpringCloud基于分布式的租售一体电商平台的设计与实现 SpringCloud基于分布式的租售一体电商平台的...
基于React+Spring的教学系统设计与实现.docx基于React+Spring的教学系统设计与实现.docx基于React+Spring的教学系统设计与实现.docx基于React+Spring的教学系统设计与实现.docx基于React+Spring的教学系统设计与实现...
内容概要:通过带着...阅读建议:此资源以开发SpringCloud的房屋租售系统学习其原理和内核,不仅是代码编写实现也更注重内容上的需求分析和方案设计,所以在学习的过程要结合这些内容一起来实践,并调试对应的代码。
基于React+Spring的教学系统设计与实现.pdf基于React+Spring的教学系统设计与实现.pdf基于React+Spring的教学系统设计与实现.pdf基于React+Spring的教学系统设计与实现.pdf基于React+Spring的教学系统设计与实现.pdf...
基于Spring Boot的校园轻博客系统的设计与实现.pdf,完整设计论文,全面详细,清晰。下载后即可清晰阅读
内容概要:通过带着读者基于SpringCloud框架实现企业在线培训平台,将平台的应用服务 设计为以培训服务、微信服务、论坛服务为主,消息中心,数据检索,决策引 擎和文件服务为辅的结构体系。注重业务系统向分布式...
阅读建议:此资源以开发SpringCloud微服务架构的移动安保系统学习其原理和内核,不仅是代码编写实现也更注重内容上的需求分析和方案设计,所以在学习的过程要结合这些内容一起来实践,并调试对应的代码。
java毕业设计——基于spring boot的音乐播放网站设计与实现(源码+数据库).zip java毕业设计——基于spring boot的音乐播放网站设计与实现(源码+数据库).zip java毕业设计——基于spring boot的音乐播放网站设计与...
Spring技术内幕:深入解析Spring架构与设计原理.pdf
Spring框架的设计理念与设计模式分析, spring框架原理 ,spring核心设计
阅读建议:此资源以开发SpringCloud微服务架构社交系统学习其原理和内核,不仅是代码编写实现也更注重内容上的需求分析和方案设计,所以在学习的过程要结合这些内容一起来实践,并调试对应的代码。
Spring作为现在最优秀的框架之一,被广泛的使用并有很多对其分析的文章。本文将从另外一个视角试图剖析出Spring框架的作者设计Spring框架的骨骼架构的设计理念,有那几个核心组件?为什么需要这些组件?它们又是如何...
毕业设计springboot宠物咖啡馆平台的设计与实现毕业设计:基于Spring Boot的宠物咖啡馆平台的设计与实现毕业设计:基于Spring Boot的宠物咖啡馆平台的设计与实现毕业设计:基于Spring Boot的宠物咖啡馆平台的设计与...