Frank Chen <chenhm@gmail.com>
这是关于spring核心功能的小指南,方便快速理解spring,需要有一点Spring使用基础,样例代码可以在 https://github.com/chenhm/from-spring-to-spring-boot 找到。
Spring核心是个ioc container,所有实现 org.springframework.context.ApplicationContext
接口的类都是spring提供的container,可以根据需要选择,常见的有
-
AnnotationConfigApplicationContext
根据扫描到的注解创建container, 充分利用注解的便利性
@Configuration @ComponentScan({"com.chenhm"}) (3) @ImportXml("classpath:com/company/data-access-config.xml") (4) public class AppLocal { public static void main(String[] args) { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); (1) ctx.getEnvironment().setActiveProfiles("local"); ctx.register(AppLocal.class); (2) ctx.refresh(); ctx.registerShutdownHook(); } @Bean @Profile("local") public DataSource local() { return initDataSource(); } }
-
创建一个空的Container
-
注入当前类,注意这是个
@Configuration
类 -
扫描
com.chenhm
包查找@Component
注解 -
导入xml配置的bean
使用上面配置类的DataSource:
package com.chenhm; @Repository public class JdbcFooRepository implements FooRepository { @Autowired private DataSource dataSource; // ... }
-
-
ClassPathXmlApplicationContext
根据classpath中的xml配置文件创建container,配置繁琐,但更灵活。
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
下面这个xml配置与上面注解的方式是等效的
<context:annotation-config /> <context:component-scan base-package="com.chenhm" />
-
XmlWebApplicationContext
spring在web应用中的默认container,通常用下面的方式初始化
<context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext*.xml</param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener>
或者
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet>
当Spring识别到ApplicationContextAware后,会将当前容器注入该对象,方便操作容器
@Bean
public class MyContext implements ApplicationContextAware {
private static ApplicationContext appContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
appContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return appContext.getBean(clazz);
}
}
然后可以在任何位置访问
MyClass myClass = MyContext.getBean(MyClass.class)
IoC解决了对象依赖问题,AOP则可以处理代码的通用逻辑,大大简化编码。在AOP以前,我们通常使用模版类提供的回调接口或interceptor来实现,比如servlet filter接口。由于需要预先设计接口,这种方式并不灵活直观。AOP则可以运行时动态拦截代码,插入通用逻辑,提供了极高的便利。拦截代码主要依赖动态代理(仅针对接口)和字节码修改技术。另外我们也可以使用Load-time instrumentation和Compile-time instrumentation,但一个需要Java agent,使用起来不够方便,一个只能在Compile-time做,不够灵活,当然instrumentation也有优势,它可以脱离容器运行。
Spring AOP 有几个核心概念:
-
Join point: 连接点,定义在哪里(哪些点)加入你的逻辑功能,对于Spring
-
Pointcut: 切入点,即一组Join point,Spring默认使用AspectJ的表达式语法匹配
-
Advice: 通知,指拦截到jointpoint之后所要做的事情,Spring AOP中分为前置通知(Before advice)、后置通知(AfterreturningAdvice)、异常通知(ThrowAdvice)、最终通知(AfterThrowing)、环绕通知(AroundAdvice)。使用AspectJ annotation 参考 http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html
-
Aspect: 切面,Advice和Pointcut的组合,在Spring中也叫 advisor,参考下面的spring事务配置理解
<tx:advice id="txAdvice" transaction-manager="txManager"> <tx:attributes> <tx:method name="get*" read-only="true"/> <tx:method name="*"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="userServicePointCut" expression="within(com.chenhm.dao.*)"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="userServicePointCut"/> </aop:config>
-
Introduction: 引入,Introduction 可以在运行期给一个class增加新的接口并指定接口的实现,也可以添加方法或Field
-
Target object: 就是advised object,在spring中永远是代理对象
-
AOP proxy: JDK dynamic proxy 或 CGLIB proxy,用于实现 Aspect
-
Weaving: 织入,应用 Aspect 创建 advised object 的过程,可以在compile time (例如AspectJ compiler), load time 或 runtime。Sping 的 weaving 发生在 runtime.
除了上面xml方式配置切面外,Spring还使用aspectj注解创建切面,例子如下:
@Aspect (1)
@Component (2)
public class LogAspect {
private Logger logger = LoggerFactory.getLogger(getClass());
@Before("execution(public * org.springframework.data.rest.webmvc.RepositoryEntityController.get*(..)) && args(resourceInformation,..)") (3)
public void before(JoinPoint jp, RootResourceInformation resourceInformation) {
logger.info("before " + jp); (4)
}
}
-
使用
@Aspect
注解标记切面类 -
@Component
使spring在容器内创建该类,也可通过xml配置让spring感知此类 -
Pointcut声明,注意参数需要用args标记
-
JoinPoint可以获得当前方法和参数信息
Spring本身大量使用了自定义注解,大大方便了开发者,我们也可以定义自己的注解配合切面完成通用功能。下面是个记录日志的例子。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AroundLog {
String level() default "info";
}
@Around("@annotation(aroundLog)") (1)
public Object AroundLog(ProceedingJoinPoint jp, AroundLog aroundLog (2)
) throws Throwable {
try {
log(logger, aroundLog.level(), "start " + jp );
return jp.proceed(); (3)
} finally {
log(logger, aroundLog.level(), "finished " + jp );
}
}
-
匹配带有annotation的方法
-
方法上的annotation类型是AroundLog
-
调用原方法
@RestController
@RequestMapping("/rest/")
public class TodoController {
@AroundLog(level = "debug")
@RequestMapping(value = "todoes/{id}", produces = MediaType.APPLICATION_JSON_VALUE )
public Todo findOne(@PathVariable Long id){
return todoRepository.findOne(id);
}
}
上面的代码我们先创建了名为 AroundLog
的注解类型,然后通过Pointcut表达式匹配,并定义了该切面的行为,最后在业务代码中通过 @AroundLog(level = "debug")
调用。Spring完成类型增强后生成的新代码大致伪码如下
@RestController
@RequestMapping("/rest/")
public class TodoController$$FastClassBySpringCGLIB$$18a9e4f3 {
final TodoController todoController
@RequestMapping(value = "todoes/{id}", produces = MediaType.APPLICATION_JSON_VALUE )
public Todo findOne(@PathVariable Long id){
return AroundLog(() -> {
todoController.findOne(id)
}, aroundLog);
}
}
早期Spring MVC是通过返回 ModelAndView
对象实现model和view的绑定。
@RequestMapping(value = "todo.html", produces = MediaType.TEXT_HTML_VALUE )
public ModelAndView todo_html(){
return new ModelAndView("todo").addObject("todoList", todoRepository.findAll());
}
至于渲染层则可以通过xml配置灵活替换。
<!-- freemarker config -->
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>
<!--
View resolvers can also be configured with ResourceBundles or XML files. If you need
different view resolving based on Locale, you have to use the resource bundle resolver.
-->
<bean id="viewResolver" class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">
<property name="cache" value="true"/>
<property name="prefix" value=""/>
<property name="suffix" value=".ftl"/>
</bean>
现在后端更加服务化,通常只返回rest接口数据,我们可以使用 @RestController
类似2.3节的代码创建rest服务,spring会自动将Bean映射为json或xml。
Spring WebSocket提供了STOMP over WebSocket的能力,这使我们可以方便的开发一些简单的实时交互应用。
首先,启用STOMP over WebSocket:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS(); (1)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue", "/topic"); (2)
registry.setApplicationDestinationPrefixes("/app"); (3)
}
}
-
注册WebSocket的endpoint,这里同时使用SockJS的通讯协议,当浏览器不支持WebSocket时可以fallback到Ajax/XHR或long polling。
-
Stomp并不真的提供
queue
或topic
,它使用SEND
和SUBSCRIBE
语义操作“destination”,这里的"/queue", "/topic"
都是destination前缀。参考 Stomp specification -
应用初始化消息的前缀。
然后我们发送应用的初始化数据:
@SubscribeMapping("/todoes") (1)
public Iterable<Todo> findAll(){
return todoRepository.findAll();
}
-
标记findAll响应Subscribe消息,当客户端Subscribe
"/app/todoes"
时,客户端会收到findAll的结果。
最后在"topic"上发送增量数据实现实时响应
@Around("(execution(* save(..)) || execution(* delete(..))) && target(repository)") (1)
public Object publishChange(ProceedingJoinPoint jp, CrudRepository repository) throws Throwable {
logger.info("publishChange " + jp);
List original = Lists.newArrayList(repository.findAll()); (2)
Object ret = jp.proceed();
List updated = Lists.newArrayList(repository.findAll()); (3)
ObjectMapper mapper = new ObjectMapper();
JsonNode patchNode = JsonDiff.asJson(mapper.valueToTree(original), mapper.valueToTree(updated));
messaging.convertAndSend("/topic/todoes", patchNode); (4)
return ret;
}
-
拦截Repository的save和delete方法
-
获取方法执行前的数据
-
获取方法执行后的数据
-
发送patch数据
Note
|
UpdateAspect无法捕捉到数据的更新操作,因为CrudRepository更新数据的流程是先根据主键调用findOne找到当前Bean,对Bean设值,然后save。显然在save之前缓存已经更新了,所以通过拦截save方法无法获得数据的变化。 |
Tip
|
如果使用表达式 @Around("target(repository)") 是否会导致 findAll() 被切面拦截或递归拦截?
|
详细用法请参考 UpdateAspect
类和前端js脚本。
Spring的声明式事务是Spring中最精彩的部分,它充分利用了Spring的容器、AOP和Servlet同步模型。JdbcTemplate是Spring直接操作jdbc的工具类,我们可以从该类入手观察整个Spring事务时如何工作的。追踪代码,获得大致流程如下:
-
TransactionAspectSupport 完成切面拦截,事务从这里开始。
-
根据事务配置,Spring会返回对应的PlatformTransactionManager,例如原生jdbc就是DataSourceTransactionManager。
-
TransactionManager开始工作前利用TransactionSynchronizationManager将所需资源绑定到当前线程。由于Servlet 3.0之前都是同步的,一次请求中的方法都是在同一线程中执行,TransactionSynchronizationManager大大简化了方法调用之间的参数传递。
-
DataSourceUtils调用TransactionSynchronizationManager中绑定的资源获取Connection。
-
在Connection完成操作后,TransactionManager根据执行情况commit或rollback。
participant "TransactionAspectSupport" as A participant "PlatformTransactionManager\n(DataSourceTransactionManager)" as B participant "TransactionSynchronizationManager" as C participant "DataSourceUtils" as D A -> B: invokeWithinTransaction activate B B -> C: doBegin activate C C -> D: bindResource activate D D -> Connection: getConnection activate Connection Connection -> Connection: exectue Connection --> D: releaseConnection deactivate Connection D --> C: unbindResource deactivate D C --> B: doCommit deactivate C B --> A: cleanupTransactionInfo deactivate B
由于Spring整体的配置较多,即使用注解仍有许多配置项,而一些常见项目与Spring的集成配置基本是通用的,于是Spring将这些项目预集成,通过检测classpath中是否有对应的类来开启配置,这就是Spring Boot项目。在 http://start.spring.io/ 可以通过勾选项目特性快速生成自己的项目配置,当然也可以在pom手动加入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
</dependencies>
-
spring-boot-starter: 引入Spring core,并实现spring boot的自动配置。
-
spring-boot-starter-web: 启用Spring webmvc,并通过spring-boot-starter-tomcat内嵌tomcat。
-
spring-boot-starter-jetty: 使用内嵌的Jetty。
-
spring-boot-starter-websocket: 跟Jetty,Tomcat,Undertow,WebLogic,WebSphere等常见容器的WebSocket适配器。
-
spring-boot-starter-freemarker: 跟Freemarker的集成。
-
spring-boot-starter-data-jpa: 启用JPA,通过Hibernate实现。
-
spring-boot-starter-data-rest: 启用spring-data-rest-webmvc,实现Data model到rest接口的自动暴露。
-
spring-boot-starter-jdbc: 启用原生jdbc。
预定义配置自然不能完全满足我们的要求,Spring boot使用 application.properties
作为全局配置文件,默认配置值可以在 http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html 找到。
Spring Boot还可以利用maven插件将项目打包成standalone jar文件,boot的Launcher会自动查找项目内含有main方法的class,然后执行。
-
AppBoot.java
项目入口
-
LogAspect.java
日志切面例子,含before advice和around advice,还有针对方法和类的不同pointcut
-
UpdateAspect.java
利用切面获取数据更新状态,然后使用WebSocket发送差异数据
-
DatabaseConfig.java
数据源配置
-
WebSocketStompConfig.java
WebSocket配置
-
TodoController.java
Spring webmvc和websocket的Controller例子
-
resources/public
Spring boot默认的静态文件目录
-
resouces/templates
Spring boot默认的template文件目录,例子用的Freemarker
运行方法:
直接执行AppBoot或 mvn package
后用 java -jar
执行生成的jar包。
http://127.0.0.1:8080/ 是个todo list的例子(需要最新版的Chrome或Firefox),可以添加删除内容,如果开多个浏览器,数据会在多个窗口同步。
http://127.0.0.1:8080/profile/todoes 是alps格式的API描述
http://127.0.0.1:8080/browser/index.html#/todoes 是data rest接口的UI