文章目录
- 前言
- 1.暴露远程MBean
- 2.访问远程MBean
- 2.1通过jsoncole工具访问
- 2.2 基于RMI客户端代码访问
- 3. 代理MBean
- 3.1 MBean暴露接口
- 3.2 客户端用代理访问MBean
- 4.通知
- 4.1 发布通知
- 4.2 监听通知
- 4.3 单个监听器监听多个MBean通知
- 4.3 为单个MBean通知配置多个监听器
- 5.参考
前言
虽然最初的JMX规范提及了通过MBean进行应用的远程管理,但并没有定义实际的远程访问协议或者API。为了有标准方式进行远程访问JMX,JCP(Java community Process)指定了
JSR-160
,即Java管理扩展远程访问API规范(Java Management Extensions Remote API Specification),该规范定义JMX远程访问的标准,该标准至少需要绑定RMI和可选JMX消息协议(JMX Messaging Protocol,JMXMP)
Spring JMX可以将Spring bean导出为模型MBean,并且能够对MBean导出的方法和属性进行细粒度控制
1.暴露远程MBean
使MBean成为远程对象最简单的方式是配置Spring的ConnectorServerFactoryBean
,ConnectorServerFactoryBean
会创建和启动JSR-160 JMXConnetorServer
。默认情况下,服务器使用的是JMXMP
协议并监听9875
,因此绑定的地址是"service:jmx:jmxmp://localhost:9875
"。
@Bean
public ConnectorServerFactoryBean connectoryFactoryBean() {
return new ConnectorServerFactoryBean();
}
根据不同的JMX实现,有多种远程访问协议可供选择,如RMI(Remote Method Invocation)
、SOAP
、Hessian/Burlap
和IIOP(Internet InterORB Protocol)
。为使MBean绑定不同远程访问协议,需要设置ConnectorServerFactoryBean
的serviceUrl
属性。例如使用RMI
远程访问MBean
,可以作如下配置:
@Bean
public ConnectorServerFactoryBean connectorServerFactoryBean() {
ConnectorServerFactoryBean csfb = new ConnectorServerFactoryBean();
csfb.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
return csfb;
}
这里将ConnectorServerFactoryBean
绑定到一个RMI
注册表,该注册表监听本机的1099
端口。这意味着需要一个RMI注册表运行并且监听改该端口,此处通过Spring中声明一个RmiRegistryFactoryBean
来启动一个RMI注册表,如下:
@Bean
public RmiRegistryFactoryBean rmiRegistryFB() {
RmiRegistryFactoryBean rmiRegistryFB = new RmiRegistryFactoryBean();
rmiRegistryFB.setPort(1099);
return rmiRegistryFB;
}
———基于RMI的公开MBean管理接口和访问
下面案例基于RMI暴露一个远程的MBean,客户端就可以基于RMI来远程访问MBean。为了配置简单,使用SpringBoot项目,完整的代码如下:
【步骤一】:使用java配置方式,创建的一个RMI注册表,并将JMX服务器绑定到此注册表上。
@Configuration
public class JavaConfig {
//1.默认的方式
/* @Bean
public ConnectorServerFactoryBean connectoryFactoryBean() {
return new ConnectorServerFactoryBean();
}*/
//启动一个RMI注册表,并监听1099端口
@Bean
public RmiRegistryFactoryBean rmiRegistryFB() {
RmiRegistryFactoryBean rmiRegistryFB = new RmiRegistryFactoryBean();
rmiRegistryFB.setPort(1099);
return rmiRegistryFB;
}
//2.基于RMI的远程服务器,需要绑定到RMI注册表上
@Bean
public ConnectorServerFactoryBean connectorServerFactoryBean() {
ConnectorServerFactoryBean csfb = new ConnectorServerFactoryBean();
csfb.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
return csfb;
}
}
【步骤二】:基于注解方式暴露MBean管理接口。
@Controller
@ManagedResource(objectName = "spitter:name=SpittleController")
// @EnableMBeanExport(registration=RegistrationPolicy.FAIL_ON_EXISTING,)
public class SpittleController {
public static final int DEFAULT_SPITTLES_PER_PAGE = 25;
private int spittlesPerPage = DEFAULT_SPITTLES_PER_PAGE;
@ManagedAttribute
public int getSpittlesPerPage() {
return spittlesPerPage;
}
@ManagedAttribute
public void setSpittlesPerPage(int spittlesPerPage) {
this.spittlesPerPage = spittlesPerPage;
}
@ManagedOperation
public int printSpittlesPerPage() {
System.out.println("spittlesPerPage--" + spittlesPerPage);
return spittlesPerPage;
}
}
【步骤三】:SpringBoot项目启动类。
@SpringBootApplication
@ComponentScan(basePackages = "com.corp")
//@ImportResource("classpath:applicationContext.xml")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.访问远程MBean
2.1通过jsoncole工具访问
jsoncole的工具的位置在D:\jdk1.8\bin
,新建连接->远程进程访问,访问地址是service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter
。
可以修改属性SpittlesPerPage的值,在操作里面操作调用printSpittlesPerPage,后台将打印如下:
spittlesPerPage--30
2.2 基于RMI客户端代码访问
客户端要访问远程MBean服务器,需要在Spring上下文中配置MbeanServerConnectionFactoryBean
,它可以创建一个MBeanServerConnection
的工厂bean,MBeanServerConnnection
用于与MBean服务器端进行通信。
【步骤一】:基于java类配置一个MbeanServerConnectionFactoryBean,并设置serviceUrl属性。
@Configuration
public class JavaConfig {
@Bean
@Primary
public MBeanServerConnectionFactoryBean connectionFactoryBean() throws MalformedURLException {
MBeanServerConnectionFactoryBean mbscfb = new MBeanServerConnectionFactoryBean();
mbscfb.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
return mbscfb;
}
}
【步骤二】:将MBeanServerConnectionFactoryBean 生成的MBeanServerConnnection作为远程MBean服务器的本地代理对象,注入到MBeanServerConnection 中,并通过MBeanServerConnection来访问远程MBean服务器信息和MBean属性和方法。
@Controller
public class JMXClient {
private MBeanServerConnection mbsConnection;
@Autowired
public void setMbsConnection(MBeanServerConnection mbsConnection) {
this.mbsConnection = mbsConnection;
}
@RequestMapping("/request")
public String getRequest() throws Exception {
//获取注册到MBeanServer服务器上的MBean数量
System.out.println("Mbeans count------"+mbsConnection.getMBeanCount());
//查询MBeanServer上指定条件的实例对象
ObjectName name = new ObjectName("spitter:name=SpittleController");
Set<ObjectInstance> objSet = mbsConnection.queryMBeans(name, null);
for(ObjectInstance obj:objSet) {
System.out.println(obj);
}
Object attribute = mbsConnection.getAttribute(name, "SpittlesPerPage");
System.out.println("spittlesPerPage value-----"+attribute);
//修改spittlesPerPage的属性值
mbsConnection.setAttribute(name, new Attribute("SpittlesPerPage",30));
//调用MBean操作,使用invoke()方法,执行pringSpittlesPerPage方法
mbsConnection.invoke(name, "printSpittlesPerPage", null, null);
//指定setSpittlesPerPage方法,其中int.class.getName()="int"
mbsConnection.invoke(name, "setSpittlesPerPage", new Object[] {100}, new String[] {int.class.getName()});
//通过setSpittlesPerPage方法修改属性值后,查看SpittlesPerPage的值
Object attribute1 = mbsConnection.getAttribute(name, "SpittlesPerPage");
System.out.println("spittlesPerPage value after change----"+attribute1);
return "";
}
}
页面访问http://localhost:8888/request
客户端打印结果:
Mbeans count------56
com.corp.demo.SpittleController[spitter:name=SpittleController]
spittlesPerPage value-----25
spittlesPerPage value after change----100
服务端打印结果:
spittlesPerPage--30
3. 代理MBean
Spring整合RMI
、Hessian
或者Spring HTTPInvoker
都提供一个了代理来访问远程服务(代理分别是HessianProxyFactoryBean
、RmiProxyFactoryBean
、HttpInvokerProxyFactoryBean
)。同样Spring JMX也提供了代理工厂bean,类名是MeanProxyFactoryBean
,用于访问远程的Spring受管理的bean。MBeanProxyFactoryBean
可以创建通过MBeanServerConnection
可访问的任何MBean的代理,客户端通过此代理与远程的MBean进行交互,如同配置本地的其他bean一样。
3.1 MBean暴露接口
【步骤一】:编写接口。
public interface SpittleControllerManagedOperations {
public int getSpittlesPerPage();
public void setSpittlesPerPage(int spittlesPerPage);
public int printSpittlesPerPage();
}
【步骤二】:使用java注解驱动MBean的方式,其中SpittleController不是必须实现SpittleControllerManagedOperations接口。
@Controller
@ManagedResource(objectName ="spitter:name=SpittleController")
public class SpittleController implements SpittleControllerManagedOperations{
public static final int DEFAULT_SPITTLES_PER_PAGE = 25;
private int spittlesPerPage = DEFAULT_SPITTLES_PER_PAGE;
@ManagedAttribute
public int getSpittlesPerPage() {
return spittlesPerPage;
}
@ManagedAttribute
public void setSpittlesPerPage(int spittlesPerPage) {
this.spittlesPerPage = spittlesPerPage;
}
@ManagedOperation
public int printSpittlesPerPage() {
System.out.println("spittlesPerPage--" + spittlesPerPage);
return spittlesPerPage;
}
}
【步骤三】:SpingBoot项目启动。
@SpringBootApplication
@ComponentScan(basePackages = "com.corp")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.2 客户端用代理访问MBean
MBeanProxyFactoryBean
会创建远程MBean
的代理,其中ObjectName
属性指定了远程MBean对象名称,server
属性设置为MBeanServerConnectionFactoryBean
工厂bean
类生成的对象,通过它实现MBean
所有通信的路由。最后ProxyInterface
属性指定了代理需要实现的接口,即SpittleControllerManagedOperations
接口。由MBeanProxyFactoryBean
产生的代理对象,可以将其注入到SpittleControllerManagedOperations
的bean属性中,将使用它来访问远程MBean
。
【步骤一】:编写接口
public interface SpittleControllerManagedOperations {
public int getSpittlesPerPage();
public void setSpittlesPerPage(int spittlesPerPage);
public int printSpittlesPerPage();
}
【步骤二】:使用java类配置方式,配置MBeanServerConnectionFactoryBean和MBeanProxyFactoryBean。
@Configuration
public class JavaConfig {
@Bean
@Primary
public MBeanServerConnectionFactoryBean connectionFactoryBean() throws MalformedURLException {
MBeanServerConnectionFactoryBean mbscfb = new MBeanServerConnectionFactoryBean();
mbscfb.setServiceUrl("service:jmx:rmi://localhost/jndi/rmi://localhost:1099/spitter");
return mbscfb;
}
@Bean
public MBeanProxyFactoryBean spittleController(MBeanServerConnection mbeanServerClient) throws Exception {
MBeanProxyFactoryBean proxy = new MBeanProxyFactoryBean();
//设置要访问的MBean的ObjectName
proxy.setObjectName("spitter:name=SpittleController");
//设置代理接口
proxy.setProxyInterface(SpittleControllerManagedOperations.class);
proxy.setServer(mbeanServerClient);
return proxy;
}
}
【步骤三】:将通知MBeanProxyFactoryBean生成的MBean服务代理对象注入到代理接口SpittleControllerManagedOperations中,从而访问MBean的操作 。如下直接可以调用setSpittlesPerPage()和getSpittlesPerPage()方法了。
@Controller
public class MBeanProxyClient {
private SpittleControllerManagedOperations spittleController;
@Autowired
public void setSpittleController(SpittleControllerManagedOperations spittleController) {
this.spittleController = spittleController;
}
@RequestMapping("/request")
public String request() {
spittleController.setSpittlesPerPage(10);
System.out.println("spittlesPerPage value---"+spittleController.getSpittlesPerPage());
return "";
}
}
【步骤四】:SpringBoot项目启动类,由于服务器端tcomat端口是8080,因此为了避免端口冲突,将客户端tomcat端口修改为8888。
在src/main/resource目录下面新建文件,名称为application.properties
,配置端口。
Server.port=8888
@SpringBootApplication
@ComponentScan(basePackages = "com.corp")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
此时页面访问localhost:8888/request
,后台打印如下:
spittlesPerPage value---10
4.通知
4.1 发布通知
通过MBean的公开属性和方法获取了资源信息,但当发生有重要的事件时,如果希望能够及时告知用户,较好的方式是MBean发送通知给用户,JMX通知(JMX Notification)是MBean与外部主动通信的一种方式,而不是等待外部应用对MBean进行信息查询。Spring提供了JMX通知的支持,Spring JMX发布通知的关键接口是org.springframework.jmx.export.notification.NotificationPublisher
。通过MBeanExporter导出为MBean的任何Spring Bean可以实现NotificationPublisherAware
接口,并且重写方法setNotificationPublisher(NotificationPublisher notificationPublisher)
来获取一个NotificationPublisher
实例。使用NotificationPublisher
方式比较简单,需要创建一个Notification
实例(或者Notification
子类实例)并设置发布通知的相关数据,在NotificationPublisher
实例上面调用sendNotification(Notification notification)
来发布通知。
4.2 监听通知
上面说到MBean可以发布通知,那么就需要一个监听器来监听和处理通知,否则发送通知毫无意义。接收MBean通知的标准方法是实现javax.management.NotificationListener
接口,并且通过方法handleNotification()
来处理通知。通过设置MBeanExporter
的NotificationListeners
属性或者notificationListenerMappings
属性来配置监听器和希望监听的MBean之间建立映射。如果一个监听器需要监听单个或者多个通知时,可以配置notificationListenerMappings
属性,此属性是一个映射集,将键配置为统配符(*
)表示单个监听器监听所有的MBean通知。相反如果要为一个MBean
通知注册多个不同的监听器,可以使用notificationListeners
列表属性。notificationListeners
列表中需要配置一个NotificationListenerBean
,NotificationListenerBean
封装了一个notificationListeners
和OBjectName
,同时还包含其他属性,例如在高级通知方案 中使用的NotificationFilter
和handback
对象。
4.3 单个监听器监听多个MBean通知
通过设置MBeanExporter
的notificationListenerMappings
属性,来实现单个监听器监听单个或者多个MBean
通知。
其中notificationListenerMappings
是一个MBean的ObjectName
名称和监听器对象的映射集。某个监听器对象对应的键为统配符(*
),表示该监听器监听所有的MBean通知。如果设置为MBean的ObjectName
名称,表示监听的是指定的MBean通知。
【步骤一】:编写一个接口IJmxTestBean。
public interface IJmxTestBean {
public int add(int x, int y);
public int getAge();
public void setAge(int age);
public void setName(String name);
public String getName();
}
【步骤二】:编写接口的实现JmxTestBean,用于发送通知。JmxTestBean
类实现了IJmxTestBean
接口和NotificationPublisherAware
,并且通过方法setNotificationPublisher
来获取一个NotificationPublisher
对象,在add()
方法中调用NotificationPublisher
对象的sendNotification()
方法来发送通知。当本地或者远程访问MBean
中的add()
方法时,Spring JMX将会发送通知给监听器。
public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware {
private String name;
private int age;
private NotificationPublisher publisher;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//发送通知
public int add(int x, int y) {
int answer = x + y;
this.publisher.sendNotification(new Notification("add", this, 0,"invoke add operation,x+y="+answer));
return answer;
}
public void dontExposeMe() {
throw new RuntimeException();
}
//获取NotificationPublisher实例
public void setNotificationPublisher(NotificationPublisher notificationPublisher) {
this.publisher = notificationPublisher;
}
}
【步骤三】:编写一个监听器类ConsoleLoggingNotificationListener,为了接收到通知,需要实现NotificationListener接口。此外可以实现NotificationFilter接口,用于过滤出感兴趣的通知。ConsoleLoggingNotificationListener
类实现了NotificationListener
接口,重写handleNotification()
方法,在此方法中对通知进行处理。实现了NotificationFilter
接口,通过isNotificationEnabled()
方法来过滤出感兴趣的通知。
@Component
public class ConsoleLoggingNotificationListener implements NotificationListener,NotificationFilter{
@Override
public boolean isNotificationEnabled(Notification notification) {
//return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
return true;
}
@Override
public void handleNotification(Notification notification, Object handback) {
System.out.println("notification---"+notification);
System.out.println("handback---"+handback);
}
}
【步骤四】:XML配置方式配置一个MBeanExporter,将Spring bean导出为MBean,并且设置监听类。
将MBeanExporter
属性notificationListenerMappings
设置为map集合,此映射集中对应的键值对分别表示的是一个被监听对象和一个监听器。例如此处为JmxTestBean配置一个监听器ConsoleLoggingNotificationListener
,键可以有三种设置方式:
- 设置为JmxTestBean的
ObjectName
名称,此处为bean:name=JmxTestBean
。 - 设置为JmxTestBean的bean的
id
,此处testBean
。 - 设置为统配符
"*"
,表示监听所有的通知,因此可以监听到JmxTestBean中发出的通知。
<bean id="exporter"
class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=JmxTestBean" value-ref="testBean" />
</map>
</property>
<property name="notificationListenerMappings">
<map>
<!-- <entry key="bean:name=testBean1"> -->
<!-- key配置为JmxTestBean的id,即testBean -->
<!-- <entry key="testBean"> -->
<entry key="*">
<bean
class="com.corp.notification.ConsoleLoggingNotificationListener" />
</entry>
</map>
</property>
</bean>
<bean id="testBean" class="com.corp.notification.JmxTestBean">
<property name="name" value="test" />
<property name="age" value="100" />
</bean>
【步骤五】:SpringBoot项目启动类。
@SpringBootApplication
@ComponentScan(basePackages = "com.corp")
@ImportResource("classpath:applicationContext.xml")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
通过jconsole
工具来访问MBean,点击add操作调用,监听器可以接收到相应的通知。
后台打印:
notification---javax.management.Notification[source=bean:name=testBean1][type=add][message=invoke add operation,x+y=0]
handback---null
4.3 为单个MBean通知配置多个监听器
当需要针对MBean
注册多个不同的监听器时,可以使用MBeanExporter
对象的notificationListeners
列表属性,notificationListeners
列表中的对象是notificationListenerBean
。notificationListenerBean
封装了要注册的监听器notificationListener
和ObjectName
(或ObjectNames
),此外还包含其他属性。
因此可以通过NotificationListenerBean
实现单监听器监听单个或者多个MBean
通知,也可以实现为单个MBean
通知配置多个监听器。
————使用NotificationListenerBean配置单个监听器监听单个MBean通知。
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean" />
</map>
</property>
<property name="notificationListeners">
<list>
<bean
class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg>
<bean class="com.corp.notification.ConsoleLoggingNotificationListener" />
</constructor-arg>
<property name="mappedObjectNames">
<list>
<value>bean:name=testBean1</value>
</list>
</property>
</bean>
</list>
</property>
</bean>
<bean id="testBean" class="com.corp.notification.JmxTestBean">
<property name="name" value="TEST" />
<property name="age" value="100" />
</bean>
通过jconsole
工具来访问MBean,点击add操作调用,监听器可以接收到相应的通知。
notification---javax.management.Notification[source=bean:name=testBean1][type=add][message=invoke add operation,x+y=0]
handback---null
————使用NotificationListenerBean配置单个监听器监听多个MBean通知。
<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean1"/>
<entry key="bean:name=testBean2" value-ref="testBean2"/>
</map>
</property>
<property name="notificationListeners">
<list>
<bean class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg ref="customerNotificationListener"/>
<property name="mappedObjectNames">
<list>
<!-- handles notifications from two distinct MBeans -->
<value>bean:name=testBean1</value>
<value>bean:name=testBean2</value>
</list>
</property>
<property name="handback">
<bean class="java.lang.String">
<constructor-arg value="This could be anything..."/>
</bean>
</property>
<property name="notificationFilter" ref="customerNotificationListener"/>
</bean>
</list>
</property>
</bean>
<!-- implements both the NotificationListener and NotificationFilter interfaces -->
<bean id="customerNotificationListener" class="com.corp.notification.ConsoleLoggingNotificationListener"/>
<bean id="testBean1" class="com.corp.notification.JmxTestBean">
<property name="name" value="TEST"/>
<property name="age" value="100"/>
</bean>
<bean id="testBean2" class="com.corp.notification.JmxTestBean">
<property name="name" value="ANOTHER TEST"/>
<property name="age" value="200"/>
</bean>
通过jconsole
工具来访问MBean,点击add操作调用,监听器可以接收到相应的通知。
后台打印结果:
notification---javax.management.Notification[source=bean:name=testBean1][type=add][message=invoke add operation,x+y=0]
handback---This could be anything...
notification---javax.management.Notification[source=bean:name=testBean2][type=add][message=invoke add operation,x+y=0]
handback---This could be anything...
————使用NotificationListenerBean为单个MBean通知配置多个监听器。
此处可以直接复制类ConsoleLoggingNotificationListener
,添加第二个监听器,类名为ConsoleLoggingNotificationListener1
。
@Component
public class ConsoleLoggingNotificationListener1 implements NotificationListener,NotificationFilter{
@Override
public boolean isNotificationEnabled(Notification notification) {
return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
}
@Override
public void handleNotification(Notification notification, Object handback) {
System.out.println("notification1---"+notification);
System.out.println("handback1---"+handback);
}
}
<bean id="exporter"
class="org.springframework.jmx.export.MBeanExporter">
<property name="beans">
<map>
<entry key="bean:name=testBean1" value-ref="testBean1" />
</map>
</property>
<property name="notificationListeners">
<list>
<bean
class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg ref="customerNotificationListener" />
<property name="mappedObjectNames">
<list>
<value>bean:name=testBean1</value>
</list>
</property>
<!-- <property name="handback"> <bean class="java.lang.String"> <constructor-arg
value="This could be anything..."/> </bean> </property> <property name="notificationFilter"
ref="customerNotificationListener"/> -->
</bean>
<bean
class="org.springframework.jmx.export.NotificationListenerBean">
<constructor-arg
ref="customerNotificationListener1" />
<property name="mappedObjectNames">
<list>
<value>bean:name=testBean1</value>
</list>
</property>
<!-- <property name="handback"> <bean class="java.lang.String"> <constructor-arg
value="This could be anything..."/> </bean> </property> <property name="notificationFilter"
ref="customerNotificationListener"/> -->
</bean>
</list>
</property>
</bean>
<!-- implements both the NotificationListener and NotificationFilter interfaces -->
<bean id="customerNotificationListener" class="com.corp.notification.ConsoleLoggingNotificationListener" />
<bean id="customerNotificationListener1" class="com.corp.notification.ConsoleLoggingNotificationListener1" />
<bean id="testBean1" class="com.corp.notification.JmxTestBean">
<property name="name" value="TEST" />
<property name="age" value="100" />
</bean>
通过jconsole
来访问MBean的操作。
后台打印:
notification---javax.management.Notification[source=bean:name=testBean1][type=add][message=invoke add operation,x+y=0]
handback---null
notification1---javax.management.Notification[source=bean:name=testBean1][type=add][message=invoke add operation,x+y=0]
handback1---null
5.参考
- 《Spring In Action》
- Spring官方文档