https://mp.weixin.qq.com/s/ZJK2U_3EBRmPPKfobjIoug

本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨论单例模式。

前传:

  • Spring框架中的设计模式(一)
  • Spring框架中的设计模式(二)
  • Spring框架中的设计模式(三)

适配器

当我们需要在给定场景下(也就是给定接口)想要不改变自身行为而又想做到一些事情的情况下(就是我给电也就是接口了,你来做事也就是各种电器),使用适配器设计模式(这里再说一点,就相当于我们再一个规章制度的环境下,如何去适应并达到我们期待的效果,放在架构设计这里,可以拿一个php系统和一个Java系统来说,假如两者要互相调用对方的功能,我们可以设计一套对外的api来适配)。这意味着在调用此对象之前,我们将更改使用对象而不改变机制。拿一个现实中的例子进行说明,想象一下你想要用电钻来钻一个洞。要钻一个小洞,你会使用小钻头,钻一个大的需要用大钻头。可以看下面的代码:

public
 
class
 
AdapterTest
 
{
  
public
 
static
 
void
 main
(
String
[]
 args
)
 
{
    
HoleMaker
 maker 
=
 
new
 
HoleMakerImpl
();
    maker
.
makeHole
(
1
);
    maker
.
makeHole
(
2
);
    maker
.
makeHole
(
30
);
    maker
.
makeHole
(
40
);
  
}
}
interface
 
HoleMaker
 
{
  
public
 
void
 makeHole
(
int
 diameter
);
}
interface
 
DrillBit
 
{
  
public
 
void
 makeSmallHole
();
  
public
 
void
 makeBigHole
();
}
// Two adaptee objects
class
 
BigDrillBit
 
implements
 
DrillBit
 
{
  
@Override
  
public
 
void
 makeSmallHole
()
 
{
    
// do nothing
  
}
  
@Override
  
public
 
void
 makeBigHole
()
 
{
    
System
.
out
.
println
(
"Big hole is made byt WallBigHoleMaker"
);
  
}
}
class
 
SmallDrillBit
 
implements
 
DrillBit
 
{
  
@Override
  
public
 
void
 makeSmallHole
()
 
{
    
System
.
out
.
println
(
"Small hole is made byt WallSmallHoleMaker"
);
  
}
  
@Override
  
public
 
void
 makeBigHole
()
 
{
    
// do nothing
  
}
}
// Adapter class
class
 
Drill
 
implements
 
HoleMaker
 
{
  
private
 
DrillBit
 drillBit
;
  
public
 
Drill
(
int
 diameter
)
 
{
    drillBit 
=
 getMakerByDiameter
(
diameter
);
  
}
  
@Override
  
public
 
void
 makeHole
(
int
 diameter
)
 
{
    
if
 
(
isSmallDiameter
(
diameter
))
 
{
            drillBit
.
makeSmallHole
();
    
}
 
else
 
{
            drillBit
.
makeBigHole
();
    
}
  
}
  
private
 
DrillBit
 getMakerByDiameter
(
int
 diameter
)
 
{
    
if
 
(
isSmallDiameter
(
diameter
))
 
{
            
return
 
new
 
SmallDrillBit
();
    
}
    
return
 
new
 
BigDrillBit
();
  
}
  
private
 
boolean
 isSmallDiameter
(
int
 diameter
)
 
{
    
return
 diameter 
<
 
10
;
  
}
}
// Client class
class
 
HoleMakerImpl
 
implements
 
HoleMaker
 
{
  
@Override
  
public
 
void
 makeHole
(
int
 diameter
)
 
{
    
HoleMaker
 maker 
=
 
new
 
Drill
(
diameter
);
    maker
.
makeHole
(
diameter
);
  
}
}

以上代码的结果如下:

Small
 hole 
is
 made byt 
SmallDrillBit
Small
 hole 
is
 made byt 
SmallDrillBit
Big
 hole 
is
 made byt 
BigDrillBit
Big
 hole 
is
 made byt 
BigDrillBit

可以看到,hole 是由所匹配的DrillBit对象制成的。如果孔的直径小于10,则使用SmallDrillBit。如果它更大,我们使用BigDrillBit。

思路就是,要打洞,那就要有打洞的工具,这里提供一个电钻接口和钻头。电钻就是用来打洞的,所以,它就一个接口方法即可,接下来定义钻头的接口,无非就是钻头的尺寸标准,然后搞出两个钻头实现类出来,接下来就是把钻头和电钻主机组装起来咯,也就是 Drill类,里面有电钻接口+钻头(根据要钻的孔大小来确定用哪个钻头),其实也就是把几个单一的东西组合起来拥有丰富的功能,最后我们进行封装下: HoleMakerImpl,这样只需要根据尺寸就可以打相应的孔了,对外暴露的接口极为简单,无须管内部逻辑是多么复杂

Spring使用适配器设计模式来处理不同servlet容器中的加载时编织(load-time-weaving)。在面向切面编程(AOP)中使用load-time-weaving,一种方式是在类加载期间将AspectJ的方面注入字节码。另一种方式是对类进行编译时注入或对已编译的类进行静态注入。

我们可以从关于Spring和JBoss的处理接口这里找到一个很好的例子,它包含在org.springframework.instrument.classloading.jboss包中。我们检索 JBossLoadTimeWeaver类负责 JBoss容器的编织管理。然而,类加载器对于 JBoss6(使用 JBossMCAdapter实例)和 JBoss7/8(使用 JBossModulesAdapter实例)是不同的。根据 JBoss版本,我们在 JBossLoadTimeWeaver构造函数中初始化相应的适配器(与我们示例中的 Drill的构造函数完全相同):

public
 
JBossLoadTimeWeaver
(
ClassLoader
 classLoader
)
 
{
  
private
 
final
 
JBossClassLoaderAdapter
 adapter
;
  
Assert
.
notNull
(
classLoader
,
 
"ClassLoader must not be null"
);
  
if
 
(
classLoader
.
getClass
().
getName
().
startsWith
(
"org.jboss.modules"
))
 
{
    
// JBoss AS 7 or WildFly 8
    
this
.
adapter 
=
 
new
 
JBossModulesAdapter
(
classLoader
);
  
}
  
else
 
{
    
// JBoss AS 6
    
this
.
adapter 
=
 
new
 
JBossMCAdapter
(
classLoader
);
  
}
}

而且,此适配器所创建的实例用于根据运行的servlet容器版本进行编织操作:

@Override
public
 
void
 addTransformer
(
ClassFileTransformer
 transformer
)
 
{
  
this
.
adapter
.
addTransformer
(
transformer
);
}
@Override
public
 
ClassLoader
 getInstrumentableClassLoader
()
 
{
  
return
 
this
.
adapter
.
getInstrumentableClassLoader
();
}

总结:适配器模式,其实就是我们用第一人称的视角去看世界,我想拓展我自己的技能的时候,就实行拿来主义,就好比这里的我是电钻的视角,那么我想拥有钻大孔或者小孔的功能,那就把钻头拿到手组合起来就好。

和装饰模式的区别:装饰模式属于第三人称的视角,也就是上帝视角!我只需要把几个功能性的组件给拿到手,进行组合一下,实现一个更加 niubility的功能这里提前说下,这样看下面的内容能好理解些。下面解释装饰模式

装饰

这里描述的第二种设计模式看起来类似于适配器。它是装饰模式。这种设计模式的主要作用是为给定的对象添加补充角色。举个现实的例子,就拿咖啡来讲。通常越黑越苦,你可以添加( 装饰)糖和牛奶,使咖啡不那么苦。咖啡在这里被装饰的对象,糖与牛奶是用来装饰的。可以参考下面的例子:

public
 
class
 
DecoratorSample
 
{
  
@Test
  
public
 
void
 test
()
 
{
    
Coffee
 sugarMilkCoffee
=
new
 
MilkDecorator
(
new
 
SugarDecorator
(
new
 
BlackCoffee
()));
    assertEquals
(
sugarMilkCoffee
.
getPrice
(),
 
6d
,
 
0d
);
  
}
}
// decorated
abstract
 
class
 
Coffee
{
  
protected
 
int
 candied
=
0
;
  
protected
 
double
 price
=
2d
;
  
public
 
abstract
 
int
 makeMoreCandied
();
  
public
 
double
 getPrice
(){
    
return
 
this
.
price
;
  
}
  
public
 
void
 setPrice
(
double
 price
){
    
this
.
price
+=
price
;
  
}
}
class
 
BlackCoffee
 
extends
 
Coffee
{
  
@Override
  
public
 
int
 makeMoreCandied
(){
    
return
 
0
;
  
}
  
@Override
  
public
 
double
 getPrice
(){
    
return
 
this
.
price
;
  
}
}
// abstract decorator
abstract
 
class
 
CoffeeDecorator
 
extends
 
Coffee
{
  
protected
 
Coffee
 coffee
;
  
public
 
CoffeeDecorator
(
Coffee
 coffee
){
    
this
.
coffee
=
coffee
;
  
}
  
@Override
  
public
 
double
 getPrice
(){
    
return
 
this
.
coffee
.
getPrice
();
  
}
  
@Override
  
public
 
int
 makeMoreCandied
(){
    
return
 
this
.
coffee
.
makeMoreCandied
();
  
}
}
// concrete decorators
class
 
MilkDecorator
 
extends
 
CoffeeDecorator
{
  
public
 
MilkDecorator
(
Coffee
 coffee
){
    
super
(
coffee
);
  
}
  
@Override
  
public
 
double
 getPrice
(){
    
return
 
super
.
getPrice
()+
1d
;
  
}
  
@Override
  
public
 
int
 makeMoreCandied
(){
    
return
 
super
.
makeMoreCandied
()+
1
;
  
}
}
class
 
SugarDecorator
 
extends
 
CoffeeDecorator
{
  
public
 
SugarDecorator
(
Coffee
 coffee
){
    
super
(
coffee
);
  
}
  
@Override
  
public
 
double
 getPrice
(){
    
return
 
super
.
getPrice
()+
3d
;
  
}
  
@Override
  
public
 
int
 makeMoreCandied
(){
    
return
 
super
.
makeMoreCandied
()+
1
;
  
}
}

上面这个简单的装饰器的小例子是基于对父方法的调用,从而改变最后的属性(我们这里是指价格和加糖多少)。在Spring中,我们在处理与Spring管理缓存同步事务的相关类中可以 发现装饰器设计模式的例子。这个类是org.springframework.cache.transaction.TransactionAwareCacheDecorator。

这个类的哪些特性证明它是org.springframework.cache.Cache对象的装饰器?首先,与我们的咖啡示例一样, TransactionAwareCacheDecorator的构造函数接收参数装饰对象(Cache):

private
 
final
 
Cache
 targetCache
;
/**
 * Create a new TransactionAwareCache for the given target Cache.
 * @param targetCache the target Cache to decorate
 */
public
 
TransactionAwareCacheDecorator
(
Cache
 targetCache
)
 
{
  
Assert
.
notNull
(
targetCache
,
 
"Target Cache must not be null"
);
  
this
.
targetCache 
=
 targetCache
;
}

其次,通过这个对象,我们可以得到一个新的行为:为给定的目标缓存创建一个新的TransactionAwareCache。这个我们可以在 TransactionAwareCacheDecorator的注释中可以阅读到,其主要目的是提供缓存和Spring事务之间的同步级别。这是通过org.springframework.transaction.support.TransactionSynchronizationManager中的两种缓存方法实现的: put 和 evict(其实最终不还是通过 targetCache来实现的么):


@Override
public
 
void
 put
(
final
 
Object
 key
,
 
final
 
Object
 value
)
 
{
  
if
 
(
TransactionSynchronizationManager
.
isSynchronizationActive
())
 
{
    
TransactionSynchronizationManager
.
registerSynchronization
(
      
new
 
TransactionSynchronizationAdapter
()
 
{
        
@Override
        
public
 
void
 afterCommit
()
 
{
          targetCache
.
put
(
key
,
 value
);
        
}
    
});
  
}
  
else
 
{
    
this
.
targetCache
.
put
(
key
,
 value
);
  
}
}
@Override
public
 
void
 evict
(
final
 
Object
 key
)
 
{
  
if
 
(
TransactionSynchronizationManager
.
isSynchronizationActive
())
 
{
          
TransactionSynchronizationManager
.
registerSynchronization
(
            
new
 
TransactionSynchronizationAdapter
()
 
{
              
@Override
              
public
 
void
 afterCommit
()
 
{
                targetCache
.
evict
(
key
);
              
}
          
});
  
}
  
else
 
{
    
this
.
targetCache
.
evict
(
key
);
  
}
}

这种模式看起来类似于适配器,对吧?但是,它们还是有区别的。我们可以看到,适配器将对象适配到运行时环境,即。如果我们在JBoss 6中运行,我们使用与JBoss 7不同的类加载器。Decorator每次使用相同的主对象(Cache)工作,并且仅向其添加新行为(与本例中的Spring事务同步),另外,可以通过我在解读这个设计模式之前的说法来区分二者。

我们再以springboot的初始化来举个例子的,这块后面会进行仔细的源码分析的,这里就仅仅用设计模式来说下的:

/**
 * Event published as early as conceivably possible as soon as a {@link SpringApplication}
 * has been started - before the {@link Environment} or {@link ApplicationContext} is
 * available, but after the {@link ApplicationListener}s have been registered. The source
 * of the event is the {@link SpringApplication} itself, but beware of using its internal
 * state too much at this early stage since it might be modified later in the lifecycle.
 *
 * @author Dave Syer
 */
@SuppressWarnings
(
"serial"
)
public
 
class
 
ApplicationStartedEvent
 
extends
 
SpringApplicationEvent
 
{
    
/**
     * Create a new {@link ApplicationStartedEvent} instance.
     * @param application the current application
     * @param args the arguments the application is running with
     */
    
public
 
ApplicationStartedEvent
(
SpringApplication
 application
,
 
String
[]
 args
)
 
{
        
super
(
application
,
 args
);
    
}
}

从注释可以看出 ApplicationListener要先行到位的,然后就是started的时候 Eventpublished走起,接着就是 Environment配置好, ApplicationContext进行初始化完毕,那我们去看 ApplicationListener的源码:

/**
 * Listener for the {@link SpringApplication} {@code run} method.
 * {@link SpringApplicationRunListener}s are loaded via the {@link SpringFactoriesLoader}
 * and should declare a public constructor that accepts a {@link SpringApplication}
 * instance and a {@code String[]} of arguments. A new
 * {@link SpringApplicationRunListener} instance will be created for each run.
 *
 * @author Phillip Webb
 * @author Dave Syer
 */
public
 
interface
 
SpringApplicationRunListener
 
{
    
/**
     * Called immediately when the run method has first started. Can be used for very
     * early initialization.
     */
    
void
 started
();
    
/**
     * Called once the environment has been prepared, but before the
     * {@link ApplicationContext} has been created.
     * @param environment the environment
     */
    
void
 environmentPrepared
(
ConfigurableEnvironment
 environment
);
    
/**
     * Called once the {@link ApplicationContext} has been created and prepared, but
     * before sources have been loaded.
     * @param context the application context
     */
    
void
 contextPrepared
(
ConfigurableApplicationContext
 context
);
    
/**
     * Called once the application context has been loaded but before it has been
     * refreshed.
     * @param context the application context
     */
    
void
 contextLoaded
(
ConfigurableApplicationContext
 context
);
    
/**
     * Called immediately before the run method finishes.
     * @param context the application context or null if a failure occurred before the
     * context was created
     * @param exception any run exception or null if run completed successfully.
     */
    
void
 finished
(
ConfigurableApplicationContext
 context
,
 
Throwable
 exception
);
}

看类注释我们可以知道,需要实现此接口内所定义的这几个方法,ok,来看个实现类:

/**
 * {@link SpringApplicationRunListener} to publish {@link SpringApplicationEvent}s.
 * <p>
 * Uses an internal {@link ApplicationEventMulticaster} for the events that are fired
 * before the context is actually refreshed.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 */
public
 
class
 
EventPublishingRunListener
 
implements
 
SpringApplicationRunListener
,
 
Ordered
 
{
    
private
 
final
 
SpringApplication
 application
;
    
private
 
final
 
String
[]
 args
;
    
private
 
final
 
ApplicationEventMulticaster
 initialMulticaster
;
    
public
 
EventPublishingRunListener
(
SpringApplication
 application
,
 
String
[]
 args
)
 
{
        
this
.
application 
=
 application
;
        
this
.
args 
=
 args
;
        
this
.
initialMulticaster 
=
 
new
 
SimpleApplicationEventMulticaster
();
        
for
 
(
ApplicationListener
<?>
 listener 
:
 application
.
getListeners
())
 
{
            
this
.
initialMulticaster
.
addApplicationListener
(
listener
);
        
}
    
}
    
@Override
    
public
 
int
 getOrder
()
 
{
        
return
 
0
;
    
}
    
@Override
    
public
 
void
 started
()
 
{
        
this
.
initialMulticaster
                
.
multicastEvent
(
new
 
ApplicationStartedEvent
(
this
.
application
,
 
this
.
args
));
    
}
    
@Override
    
public
 
void
 environmentPrepared
(
ConfigurableEnvironment
 environment
)
 
{
        
this
.
initialMulticaster
.
multicastEvent
(
new
 
ApplicationEnvironmentPreparedEvent
(
                
this
.
application
,
 
this
.
args
,
 environment
));
    
}
    
@Override
    
public
 
void
 contextPrepared
(
ConfigurableApplicationContext
 context
)
 
{
    
}
    
@Override
    
public
 
void
 contextLoaded
(
ConfigurableApplicationContext
 context
)
 
{
        
for
 
(
ApplicationListener
<?>
 listener 
:
 
this
.
application
.
getListeners
())
 
{
            
if
 
(
listener 
instanceof
 
ApplicationContextAware
)
 
{
                
((
ApplicationContextAware
)
 listener
).
setApplicationContext
(
context
);
            
}
            context
.
addApplicationListener
(
listener
);
        
}
        
this
.
initialMulticaster
.
multicastEvent
(
                
new
 
ApplicationPreparedEvent
(
this
.
application
,
 
this
.
args
,
 context
));
    
}
    
@Override
    
public
 
void
 finished
(
ConfigurableApplicationContext
 context
,
 
Throwable
 exception
)
 
{
        
// Listeners have been registered to the application context so we should
        
// use it at this point
        context
.
publishEvent
(
getFinishedEvent
(
context
,
 exception
));
    
}
    
private
 
SpringApplicationEvent
 getFinishedEvent
(
            
ConfigurableApplicationContext
 context
,
 
Throwable
 exception
)
 
{
        
if
 
(
exception 
!=
 
null
)
 
{
            
return
 
new
 
ApplicationFailedEvent
(
this
.
application
,
 
this
.
args
,
 context
,
                    exception
);
        
}
        
return
 
new
 
ApplicationReadyEvent
(
this
.
application
,
 
this
.
args
,
 context
);
    
}
}

从上可以看出, EventPublishingRunListener里对接口功能的实现,主要是通过 SpringApplication ApplicationEventMulticaster 来实现的,自己不干活,挂个虚名,从上帝模式的角度来看,这不就是应用了装饰模式来实现的么。

更多源码解析请关注后续的本人对Spring框架全面的重点部分解析系列博文

单例

单例,我们最常用的设计模式。正如我们在很多Spring Framework中关于单例和原型bean的文章(网上太多了)中已经看到过的,单例是几个bean作用域中的中的一个。此作用域在每个应用程序上下文中仅创建一个给定bean的实例。与signleton设计模式有所区别的是,Spring将实例的数量限制的作用域在整个应用程序的上下文。而Singleton设计模式在Java应用程序中是将这些实例的数量限制在给定类加载器管理的整个空间中。这意味着我们可以为两个Spring的上下文(同一份配置文件起两个容器,也就是不同端口的容器实例)使用相同的类加载器,并检索两个单例作用域的bean。

在看Spring单例应用之前,让我们来看一个Java的单例例子:

public
 
class
 
SingletonTest
 
{
  
@Test
  
public
 
void
 test
()
 
{
    
President
 president1 
=
 
(
President
)
 
SingletonsHolder
.
PRESIDENT
.
getHoldedObject
();
    
President
 president2 
=
 
(
President
)
 
SingletonsHolder
.
PRESIDENT
.
getHoldedObject
();
    assertTrue
(
"Both references of President should point to the same object"
,
 president1 
==
 president2
);
    
System
.
out
.
println
(
"president1 = "
+
president1
+
" and president2 = "
+
president2
);
    
// sample output
    
// president1 = com.waitingforcode.test.President@17414c8 and president2 = com.waitingforcode.test.President@17414c8
  
}
}
enum
 
SingletonsHolder
 
{
  PRESIDENT
(
new
 
President
());
  
private
 
Object
 holdedObject
;
  
private
 
SingletonsHolder
(
Object
 o
)
 
{
          
this
.
holdedObject 
=
 o
;
  
}
  
public
 
Object
 getHoldedObject
()
 
{
          
return
 
this
.
holdedObject
;
  
}
}
class
 
President
 
{
}

这个测试例子证明,只有一个由SingletonsHolder所持有的President实例。在Spring中,我们可以在bean工厂中找到单例应用的影子(例如在org.springframework.beans.factory.config.AbstractFactoryBean中):

/**
 * 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
();
  
}
}

我们看到,当需求对象被视为单例时,它只被初始化一次,并且在每次使用同一个bean类的实例后返回。我们可以在给定的例子中看到,类似于我们以前看到的President情况。将测试bean定义为:

<bean
 
id
=
"shoppingCart"
 
class
=
"com.waitingforcode.data.ShoppingCart"
 
/>
测试用例如下所示:

public
 
class
 
SingletonSpringTest
 
{
  
@Test
  
public
 
void
 test
()
 
{
    
// retreive two different contexts
    
ApplicationContext
 firstContext 
=
 
new
 
FileSystemXmlApplicationContext
(
"applicationContext-test.xml"
);
    
ApplicationContext
 secondContext 
=
 
new
 
FileSystemXmlApplicationContext
(
"applicationContext-test.xml"
);
    
// prove that both contexts are loaded by the same class loader
    assertTrue
(
"Class loaders for both contexts should be the same"
,
 
      firstContext
.
getClassLoader
()
 
==
 secondContext
.
getClassLoader
());
    
// compare the objects from different contexts
    
ShoppingCart
 firstShoppingCart 
=
 
(
ShoppingCart
)
 firstContext
.
getBean
(
"shoppingCart"
);
    
ShoppingCart
 secondShoppingCart 
=
 
(
ShoppingCart
)
 secondContext
.
getBean
(
"shoppingCart"
);
    assertFalse
(
"ShoppingCart instances got from different application context shouldn't be the same"
,
 
      firstShoppingCart 
==
 secondShoppingCart
);
    
// compare the objects from the same context
    
ShoppingCart
 firstShoppingCartBis 
=
 
(
ShoppingCart
)
 firstContext
.
getBean
(
"shoppingCart"
);
    assertTrue
(
"ShoppingCart instances got from the same application context should be the same"
,
 
      firstShoppingCart 
==
 firstShoppingCartBis
);
  
}
}

这个测试案例显示了Spring单例模式与纯粹的单例设计模式的主要区别。尽管使用相同的类加载器来加载两个应用程序上下文,但是ShoppingCart的实例是不一样的。但是,当我们比较两次创建并属于相同上下文的实例时,我们认为它们是相等的。

也正因为有了单例,Spring可以控制在每个应用程序上下文中只有一个这样指定的bean的实例可用。因为适配器,Spring可以决定使用由谁来处理 JBossservlet容器中的加载时编织,也可以实现 ConfigurableListableBeanFactory的相应实例。第三种设计模式,装饰器,用于向Cache对象添加同步功能,还有Springboot的容器初始化。

其实对于适配器和装饰者确实有太多的相似的地方,一个是运行时选择,一个是加料组合产生新的化学效应,还有从看待事物的角度不同得到不同的行为,适配适配,更注重面向接口的实现,而内部又根据不同情况调用面向一套接口的多套实现的实例的相应方法来实现所要实现的具体功能,装饰者更注重添油加醋,通过组合一些其他对象实例来让自己的功能实现的更加华丽一些(达到1+1>2的这种效果)。一家之言,有更好的理解可以联系我。

推荐阅读

  • Spring框架中的设计模式(一)
  • Spring框架中的设计模式(二)
  • Spring框架中的设计模式(三)
  • 程序员你为什么这么累?
  • 程序员你为什么这么累【续】:编码习惯之接口定义
  • 程序员你为什么这么累【续】:编码习惯之Controller规范
  • 程序员你为什么这么累【续】:编码习惯之日志建议
  • Logback+ELK+SpringMVC搭建日志收集服务器 长按指纹 一键关注