要分析产生死锁的原因,需要查看mysql的相关日志信息。由于show engine innodb status输出的信息较多,在屏幕中看不到所有的输出信息,因此先设置输出日志到文档中,然后执行日志输出:
1 | mysql:pager cat - >> ~/tzwang/mysqllog.txt; mysql:show engine innodb status; |
在产生死锁的事务中,有两条SQL(下文SQL经过脱敏处理):
1 | 语句1: |
其中tbl_form_dtl表的主键为id,在form_no字段有二级索引。
通过explain命令查看语句1,发现fromtbl_form_dtl t走了二级索引(form_no),如下图所示:
由于事务隔离级别是可重复读级别,且索引是二级索引,所以上述语句1在锁住二级索引的行锁、GAP LOCK外,还会锁住对应的聚簇索引。
通过explain命令查看语句2,发现语句2走的是聚簇索引,如下图所示:
按理来说,语句2是可以走form_no字段的索引的,但是mysql会自动优化,由于该条sql的结果占表数据总量比较大,sql就将其优化成走聚簇索引(之所以优化成走聚簇索引,应该是因为二级索引在查询到后,还得再到聚簇索引中update数据)。经测试发现,当将表中的数据增大,使得语句2的覆盖范围占整个表的数据量比例较小时,就走了二级索引。
分析mysql的日志:
RECORD LOCKS space id 439657 page no 12 n bits 120 index
PRIMARY
of tabledatabasename
.tbl_form_dtl
trx id 54562223 lock_mode X waitingRECORD LOCKS space id 439657 page no 12 n bits 120 index
PRIMARY
of tabledatabasename
.tbl_form_dtl
trx id 54562222 lock_mode XRECORD LOCKS space id 439657 page no 2317 n bits 120 index
PRIMARY
of tabledatabasename
.tbl_form_dtl
trx id 54562222 lock_mode X waiting
产生死锁的原因:
事务54562222在执行语句2时,占有了page no =12的X锁,要获取page no=2317的X锁,而该锁已被事务54562223锁住;
事务54562223在执行语句1时,在锁住二级索引时,也获取了page no = 2317的S锁,然后执行语句2,要获取page no = 12的锁,而该锁已被事务54562222锁住。
如下图所示,事务1在执行语句1时查找二级索引(form_no),锁住绿色的部分,包括二级索引的Record lock和GAP LOCK,聚簇索引的Record Lock(因为聚簇索引是unique的,所以不用锁GAP LOCK);此时事务2也在执行语句1,锁住了青色的部分,也包括Record和GAP LOCK,及聚簇索引的Record Lock。然后事务1和事务2各自执行语句2,由于语句2走了聚簇索引,所以要遍历聚簇索引;在mysql的innodb引擎中,由于where条件不是id,因此走聚簇索引需要锁住各个行,不过mysql有优化,在返回到服务器端后,mysql会根据where条件释放不必要的锁,不过在返回服务器端前还是要先加锁的。此时事务1需要等待id=4的锁,id=4已经被事务2锁住;而事务2需要等待id=1的锁,而id=1的锁已经被事务1锁住,因此就造成了死锁。
mysql在检测到发生了死锁后,会释放一个事务的锁,另一个事务就能获得锁,此时就会报死锁异常,在mysql的相关日志中可以看到LATEST DETECTED DEADLOCK
的相关记录,通过这部分记录就可以分析出死锁产生的原因。
造成死锁的原因一般是加锁的顺序不一致导致的问题。数据库大神何登成的mysql加锁处理分析一文分析了mysql加锁的方式,以及死锁的一些场景,写的简单易懂,值得一看。
]]>要分析产生死锁的原因,需要查看mysql的相关日志信息。由于show engine innodb status输出的信息较多,在屏幕中看不到所有的输出信息,因此先设置输出日志到文档]]>
AQS底层调用了很多Unsafe类里的方法来进行并发的处理,Unsafe类里的方法基本都是native的,通过利用系统硬件的支持,来实现并发的控制。其中利用到硬件的最重要的一个指令是CAS(compare and swap),CAS包含了3个操作数:需要读写的内存位置V、进行比较的值A和拟写入的新值B,当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。
在concurrent包的atomic目录下包含了很多原子变量类,如AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等,这些原子变量类的底层都是利用unsafe类提供的方法来实现的。
AQS是一个用于构造锁和同步器的框架,许多同步器都可通过AQS很容易且高效的构造出来。它解决了在实现同步器时涉及的大量细节问题。
AQS有一个整数状态信息,可以通过getState、setState以及compareAndSetState等方法来进行操作,这个整数可以用于表示任意的状态。例如ReentrantLock用它来表示所有者线程已经重复获取该锁的次数,Semaphore用它来表示剩余的许可数量,FutureTask(JDK1.7以前)用它来表示任务的状态(尚未开始、正在运行、已完成、已取消),ReentrantReadWriteLock用它的低16位来表示写操作的计数,高16位表示读操作的计数等等。此外,AQS的父类有一个exclusiveOwnerThread变量,可以用来存储独占锁情形下当前的执行线程。
在AQS中还维护着一个队列(采用双向链表实现),用来存储等待获取锁的线程,队列由Node类型的节点组成:
1 | static final class Node { |
waitStatus用来表示:
AQS维护的队列
队列的第一个节点head就是代表持有锁的节点。
在使用AQS的同步器中,并不是直接继承AQS,而是将一些功能委托给AQS,在同步器类内部中定义内部类,通过内部类对象来调用AQS的功能。因为如果直接继承AQS,那么AQS中的很多在同步器中的无用的方法,也会暴露给调用者,调用者很容易勿用,破坏简洁性。
AQS的功能可以分为两类:独占功能和共享锁功能,下面用ReentrantLock源码来分析独占功能的在源码中的使用方法,以ReentrantReadWriteLock源码来分析共享锁功能的使用方法。
本文以ReentrantLock为例来分析独占锁的使用,ReentrantLock的类图如下所示:
源码及核心逻辑解释写在了下面的源码注释中:
1 | //FairSync |
Condition
之于wait/notify,就像Lock之于synchronize,在执行wait/notify时需要先获取对象的控制权,比如将对象用synchronize放在同步块中,在同步块里面执行wait/notify,否则会报IllegalMonitorStateException异常;同样的,在执行Condition的await/signal之前,需要先调用ReentrantLock.lock()等方法获取锁,在锁里面执行await/signal。
1 | public final void await() throws InterruptedException { |
ReentrantReadWriteLock的ReadLock、Semaphore等都是利用AQS的共享方法来实现的,下文以ReentrantReadWriteLock来分析共享锁的使用方法,Semaphore的实现方式类似。
1 | //ReadLock |
Semaphore
也是share模式,跟ReentrantReadWriteLock的读锁机制类似,只不过ReentrantReadWriteLock的ReadLock在lock()时status是递增的,而Semaphore调用acquire()方法是递减的,当减到0时就要在队列等待,直到其他线程调用了release()且status的值>=等待线程要获取的permit的数量时,才获取permit。
在Semaphore中,公平与不公平的一个区别为:公平的,假如当前有线程A和B在等待,A的permit=4,B的permit=1,当前的status=2,线程A比线程B早到,则虽然status>1,但是由于A排在B前面,因此B还是得等待;如果非公平的,则B可以直接获取permit;创建Semaphore对象时,如果没有指定是否公平的参数,则默认为不公平的。
https已经成为电商网站、银行、资金有关的系统的标配,特别是现在网络流量劫持增多、资金频繁被盗等场景下,重要网站https化势在必行。那么https原理是什么?单向认证、双向认证、为何要依赖于证书颁发机构、12306网站为何需要配置根证书?这些都是值得探讨的。
网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。SSL目前的版本是3.0,被IETF(Internet Engineering Task Force)定义在RFC 6101中,之后IETF对SSL 3.0进行了升级,于是出现了TLS(Transport Layer Security) 1.0,定义在RFC 2246。实际上我们现在的HTTPS都是用的TLS协议,但是由于SSL出现的时间比较早,并且依旧被现在浏览器所支持。
1. 客户端发起https请求
这个过程中客户端发送的信息会说明它支持的最高TLS协议版本、随机数、密码算法列表及压缩方法给服务端;
2. 服务端配置
采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面。
3. 服务器返回证书
发送给浏览器的证书包含公钥、证书的颁发机构、过期时间、域、签名等信息;
4. 验证证书
这部分工作是由客户端的TLS来完成的,客户端会验证证书是否有效,如服务器证书是否过期、发行服务器证书的CA是否可靠、发行CA的公钥能否正确解开服务器证书的发行CA的数字签名、服务器证书上的域名是否和服务器的实际域名相匹配等等,如果发现异常则会弹出一个警告框,提示证书存在问题,让用户选择是否继续。
证书中会包含数字签名,该数字签名是加密过的,是用颁发机构的私钥对本证书的公钥、名称及其他信息做hash散列加密而生成的。客户端浏览器会一层层的去寻找颁发者的证书,直到自签名的根证书,然后通过相应的公钥再反过来验证下一级的数字签名的正确性,如果不能正常解密,则就是”发现异常”,说明该证书是伪造的。
5. 传送加密信息
如果上步证书验证通过,则生成一个随机值A作为后面通讯的密钥(对称加密),用证书的公钥对这个随机值加密,发送该加密后的随机值到服务器端,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
如果服务器要求客户端的身份认证(在握手过程中为可选)即双向认证,客户端可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户端自己的证书以及加密过的随机值A发送给服务器。
6. 服务端解密信息
服务端用证书的私钥解密后,得到了客户端传过来的随机值A,至此一个非对称加密的过程结束,看到TLS利用非对称加密实现了身份认证和密钥协商。
如果服务器要求客户端的身份认证(双向认证),服务器必须检验客户端证书和签名随机数的合法性,具体的合法性验证包括:客户端证书是否过期,发行客户端证书的CA是否可靠,发行CA的公钥能否正确解开客户端证书的发行CA的数字签名,检查客户端证书是否在证书废止列表(CRL)中。如果合法性验证没有通过,通讯立刻中断;如果合法性验证通过,才获取随机值A,进行后续的步骤。
7. 传输加密后的信息
用上文客户端传来的密钥A加密客户端请求的内容,发给客户端。
8. 客户端解密信息
客户端用密钥A解密消息,获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。(为什么不直接使用公钥和私钥进行通讯?因为非对称解密相当损耗CPU性能,https网站cpu损耗差不多90%的性能损耗都在非对称秘钥的解密上。全部内容都用非对称加密交流性能大大下降)
不管是双向认证还是单向认证,服务端都需要配置CA证书,单向认证不需要客户端拥有CA证书,双向认证客户端需要拥有CA证书。
一般Web应用都是采用单向认证的,原因很简单,用户数目广泛,且无需做在通讯层做用户身份验证,一般都在应用逻辑层来保证用户的合法登入。但如果是企业应用对接,情况就不一样,可能会要求对客户端(相对而言)做身份验证。这时就需要做双向认证。
如果任何的证书客户端都信任,那么在上面的步骤1中,攻击者可以通过DNS劫持或IP伪造的方法使其访问到攻击者的服务器上,攻击者伪造一个同样域名的证书发给客户端,客户端验证了日期、域等都没错,从而骗过了客户端。
为了抵御这种中间人攻击,证书需要由可信的证书颁发机构颁发,形成一个证书链(比如Gmail的证书链为:最底层为网域mail.google.com,上一层为Thawte SGC CA证书颁发机构,最顶层为很有名的VeriSign证书颁发机构)。那么,浏览器除了需要验证域和有效期外,还要检查证书链中的上级证书公钥是否有效,上级的上级证书公钥是否有效,直至根证书公钥为止。这样就可以有效避免中间人攻击了,因为根证书公钥都是预装在浏览器或操作系统中的,黑客如果不是暴力破解,无法得到根证书的私钥,如果黑客自己生成一个私钥,浏览器验证根证书公钥的时候发现无法通过浏览器中预装的公钥加密数据后使用这个私钥进行解密,从而判定这个公钥是无效的。这个方案也是现在https通讯通常的方案。
那么,这个现在所有的浏览器正在使用的https通讯方案就无懈可击了吗?答案仍是否定的。我们可以看到,在后一个方案中,https的安全性需要在证书颁发机构公信力的强有力保障前提下才能发挥作用。如果证书颁发机构在没有验证黑客为mail.google.com的持有者的情况下,给黑客颁发了网域为mail.google.com的证书,那么黑客的中间人攻击又可以顺利实施。
12306网站就是没有采用证书颁发机构颁发的证书,而是自己制作证书,因此需要在浏览器安装根证书,否则访问的时候就会提示‘此网站的安全证书有问题’,阻止用户继续访问。
证书分为DV(Digital Verification),OV(Organization Verification)和EV(Extended Verification),其中EV证书最贵,可以在浏览器中看到绿色的就是EV证书。
https已经成为电商网站、银行、资金有关的系统的标配,特别是现在网络流量劫持增多、资金频繁被盗等场景下,重要网站https化]]>
- Spring2.5之前,我们都是通过实现Controller接口或其实现来定义我们的处理器类。已经@Deprecated。
- Spring2.5引入注解式处理器支持,通过@Controller 和 @RequestMapping注解定义我们的处理器类。
并且提供了一组强大的注解:
- @Controller:用于标识是处理器类;
- @RequestMapping:请求到处理器功能方法的映射规则;
- @RequestParam:请求参数到处理器功能处理方法的方法参数上的绑定;
- @ModelAttribute:请求参数到命令对象的绑定;
- @SessionAttributes:用于声明session级别存储的属性,放置在处理器类上,通常列出模型属性(如@ModelAttribute)对应的名称,则这些属性会透明的保存到session中;
- @InitBinder:自定义数据绑定注册支持,用于将请求参数转换到命令对象属性的对应类型;
注意:需要通过处理器映射DefaultAnnotationHandlerMapping和处理器适配器AnnotationMethodHandlerAdapter来开启支持@Controller 和@RequestMapping注解的处理器。- Spring3.0引入RESTful架构风格支持(通过@PathVariable注解和一些其他特性支持),且又引入了更多的注解支持:
- @CookieValue:cookie数据到处理器功能处理方法的方法参数上的绑定;
- @RequestHeader:请求头(header)数据到处理器功能处理方法的方法参数上的绑定;
- @RequestBody:请求的body体的绑定(通过HttpMessageConverter进行类型转换);
- @ResponseBody:处理器功能处理方法的返回值作为响应体(通过HttpMessageConverter进行类型转换);
- @ResponseStatus:定义处理器功能处理方法/异常处理器返回的状态码和原因;
- @ExceptionHandler:注解式声明异常处理器;
- @PathVariable:请求URI中的模板变量部分到处理器功能处理方法的方法参数上的绑定,从而支持RESTful架构风格的URI;
- Spring3.1使用新的HandlerMapping 和 HandlerAdapter来支持@Contoller和@RequestMapping注解处理器。
新的@Contoller和@RequestMapping注解支持类:处理器映射RequestMappingHandlerMapping和处理器适配器RequestMappingHandlerAdapter组合来代替Spring2.5开始的处理器映射DefaultAnnotationHandlerMapping和处理器适配器AnnotationMethodHandlerAdapter,提供更多的扩展点。
配置了<mvc:annotation-driven/>
相当于自动加了一堆bean:
registers a RequestMappingHandlerMapping, a RequestMappingHandlerAdapter, and an ExceptionHandlerExceptionResolver (among others) in support of processing requests with annotated controller methods using annotations such as @RequestMapping, @ExceptionHandler, and others.
It also enables the following:
- Spring 3 style type conversion through a ConversionService instance in addition to the JavaBeans PropertyEditors used for Data Binding.
- Support for formatting Number fields using the @NumberFormat annotation through the ConversionService.
- Support for formatting Date, Calendar, Long, and Joda Time fields using the @DateTimeFormat annotation.
- Support for validating @Controller inputs with @Valid, if a JSR-303 Provider is present on the classpath.
- HttpMessageConverter support for @RequestBody method parameters and @ResponseBody method return values from @RequestMapping or @ExceptionHandler methods.
This is the complete list of HttpMessageConverters set up by mvc:annotation-driven:
- ByteArrayHttpMessageConverter converts byte arrays.
- StringHttpMessageConverter converts strings.
- ResourceHttpMessageConverter converts to/from org.springframework.core.io.Resource for all media types.
- SourceHttpMessageConverter converts to/from a javax.xml.transform.Source.
- FormHttpMessageConverter converts form data to/from a MultiValueMap.
- Jaxb2RootElementHttpMessageConverter converts Java objects to/from XML — added if JAXB2 is present and Jackson 2 XML extension is not present on the classpath.
- MappingJackson2HttpMessageConverter converts to/from JSON — added if Jackson 2 is present on the classpath.
- MappingJackson2XmlHttpMessageConverter converts to/from XML — added if Jackson 2 XML extension is present on the classpath.
- AtomFeedHttpMessageConverter converts Atom feeds — added if Rome is present on the classpath.
- RssChannelHttpMessageConverter converts RSS feeds — added if Rome is present on the classpath.
当然,也可以自己自定义各种配置,具体的方法可以参考springMVC docs;
核心架构的具体流程步骤如下:
- 首先用户发送请求——>DispatcherServlet,前端控制器收到请求后自己不进行处理,而是委托给其他的解析器进行处理,作为统一访问点,进行全局的流程控制;
- DispatcherServlet——>HandlerMapping, HandlerMapping将会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象、多个HandlerInterceptor拦截器)对象,通过这种策略模式,很容易添加新的映射策略;
- DispatcherServlet——>HandlerAdapter,HandlerAdapter将会把处理器包装为适配器,从而支持多种类型的处理器,即适配器设计模式的应用,从而很容易支持很多类型的处理器;
- HandlerAdapter——>处理器功能处理方法的调用,HandlerAdapter将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView对象(包含模型数据、逻辑视图名);
- ModelAndView的逻辑视图名——> ViewResolver, ViewResolver将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术;
- View——>渲染,View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术;
- 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束。
处理的核心代码在DispatcherServlet的doDispatch方法中: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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
////检查是否是请求是否是multipart(如文件上传),如果是将通过MultipartResolver解析
processedRequest = checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// //步骤2、请求到处理器(页面控制器)的映射,通过HandlerMapping进行映射
mappedHandler = getHandler(processedRequest, false);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
//步骤3、处理器适配,即将我们的处理器包装成相应的适配器(从而支持多种类型的处理器)
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
String requestUri = urlPathHelper.getRequestUri(request);
logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//执行处理器相关的拦截器的预处理(HandlerInterceptor.preHandle)
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
try {
//步骤4、由适配器执行处理器(调用处理器相应功能处理方法)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
}
applyDefaultViewName(request, mv);
//执行处理器相关的拦截器的后处理(HandlerInterceptor.postHandle)
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
//步骤5 由ViewResolver解析View(viewResolver.resolveViewName(viewName, locale))
//步骤6 视图在渲染时会把Model传入(view.render(mv.getModelInternal(), request, response);)
//HandlerInterceptor.afterCompletion
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Error err) {
triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
return;
}
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
步骤4调用handle来真正执行响应的controller的方法,在此过程中还会调用convert进行类型转换等操作,若添加了<mvc:annotation-driven/>
的配置,如果maven引用了Jackson 2包,则会调用MappingJackson2HttpMessageConverter
这个converter进行json和对象的间的转换。
步骤5中ViewResolver将把逻辑视图名解析为具体的View,通过这种策略模式,很容易更换其他视图技术,如JSP、freemarker等;步骤6会根据传进来的Model模型数据进行渲染。
在方法的执行前后,还会调用拦截器的相关方法:
拦截器接口:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public interface HandlerInterceptor {
boolean preHandle(
HttpServletRequest request, HttpServletResponse response,
Object handler)
throws Exception;
void postHandle(
HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView)
throws Exception;
void afterCompletion(
HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex)
throws Exception;
}
preHandle:预处理回调方法,实现处理器的预处理(如登录检查),第三个参数为响应的处理器(如我们上一章的Controller实现);返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应;
postHandle:后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null;
afterCompletion:整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中preHandle返回true的拦截器的afterCompletion。
web.xml加载顺序,context-param -> listener -> filter -> servlet;
启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取
紧接着,容器创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文;
容器将
容器创建
构建filter链(如CharacterEncodingFilter、OpenSessionInViewFilter、struts2的StrutsPrepareFilter、StrutsExecuteFilter等);
注意:servlet是单实例多线程;spring bean默认也是单实例的,不过可以通过scope参数来设置为别的;struts2中action是多例的。另外,本文基于spring3.2.2源码来分析的。
servlet、listener、filter的关键方法
Servlet
-init(ServletConfig config)
-service(ServletRequest req, ServletResponse res)
//service再根据请求类型调用doGet、doPost等方法
-destroy()
Listener
ServletContextListener
-contextInitialized ( ServletContextEvent sce )
-contextDestroyed ( ServletContextEvent sce )
ServletRequestListener
-requestInitialized(ServletRequestEvent requestEvent)
-requestDestroyed(ServletRequestEvent requestEvent)
Filter
-init(FilterConfig filterConfig)
-destroy()
-doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
ContextLoaderListener
作用就是启动Web容器时,自动装配spring的配置文件的配置信息;ContextLoaderServlet
:与ContextLoaderListener功能一样;
ContextLoaderListener它继承了javax.servlet.ServletContextListener
接口。ServletContextListener是J2EE Servlet API中的一个标准接口,它能够监听ServletContext对象的生命周期,实际上就是监听Web应用的生命周期。当Servlet容器启动或终止Web应用时,会触发ServletContextEvent事件,该事件由ServletContextListener来处理。
Ioc容器实例化流程如图所示:
实例化Spring IoC容器的过程中,最主要的两个方法是createWebApplicationContext
和configureAndRefreshWebApplicationContext
方法。
createWebApplicationContext
方法用于返回XmlWebApplicationContext实例,即Web环境下的Spring IoC容器。
configureAndRefreshWebApplicationContext
用于配置XmlWebApplicationContext,读取web.xml中通过contextConfigLocation
标签指定的XML文件,实例化XML文件中配置的bean,并在上一步中实例化的容器中进行注册。
完成以上两步的操作后,Spring MVC会将XmlWebApplicationContext实例以属性的方式注册到ServletContext中。此Spring 容器是ROOT上下文,供所有的Spring MVC Servlcet使用。
核心的初始化逻辑都在AbstractApplicationContext的refresh()
方法里:
1 | public void refresh(){ |
obtainFreshBeanFactory()方法会刷新所有BeanFactory子容器,在此会加载、解析Bean的定义。
finishBeanFactoryInitialization()方法用来实例化bean。
bean实例化时机:
若是单例、lazy-init=false且不是抽象类,则实例化,否则等到执行时才实例化;实例化中若有依赖别的bean,需要先实例化依赖的bean。
具体代码逻辑在DefaultListableBeanFactory
的preInstantiateSingletons()
和AbstractBeanFactory
的doGetBean()
,可以参考Spring 框架的设计理念与设计模式分析。
ApplicationContext vs BeanFactory
ApplicationContext的类继承关系如图所示:ApplicationContext
由BeanFactory 派生而来,提供了更多面向实际应用的功能。几乎所有的应用场合我们都直接使用ApplicationContext
而非底层的BeanFactory。在BeanFactory 中,很多功能需要以编程的方式实现,而在ApplicationContext中则可以通过配置的方式实现。
ApplicationContext的主要实现类是ClassPathXmlApplicationContext
和FileSystemXmlApplicationContext
,前者默认从类路径加载配置文件,后者默认从文件系统中装载配置文件。
WebApplicationContext
是专门为Web 应用准备的,WebApplicationContext 扩展了ApplicationContext。
Spring 分别提供了用于启动WebApplicationContext 的Servlet 和Web 容器监听器:
org.springframework.web.context.ContextLoaderServlet;
org.springframework.web.context.ContextLoaderListener;
两者的内部都实现了启动WebApplicationContext 实例的逻辑,我们只要根据Web 容器的具体情况选择两者之一,并在web.xml 中完成配置就可以了。
DispatcherServlet配置在web.xml中,是springmvc的入口,它的继承关系图如下图所示:
1 | public class DispatcherServlet extends FrameworkServlet { |
//初始化默认的Spring Web MVC框架使用的策略(如HandlerMapping)1
2
3
4
5
6
7
8
9
10
11
12protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
}
从如上代码可以看出,DispatcherServlet启动时会进行我们需要的Web层Bean的配置,如HandlerMapping、HandlerAdapter等,而且如果我们没有配置,还会给我们提供默认的配置。
从如上代码我们可以看出,整个DispatcherServlet初始化的过程和做了些什么事情,具体主要做了如下两件事情:
ContextLoaderListener实现ServletContextListener监听器接口,而ServletContextListener只负责监听Web容器的启动和关闭的事件。RequestContextListener实现ServletRequestListener监听器接口,该监听器监听HTTP请求事件,Web服务器接收的每次请求都会通知该监听器。
1)CharacterEncodingFilter:字符编码转换filter,这个filter一定要注意顺序,要放在其它filter前面,要不然可能不起作用;
2) OpenSessionInViewFilter:主要功能是用来把一个Hibernate Session和一次完整的请求过程对应的线程相绑定。跟延迟加载相关。
3) Struts2的入口:StrutsPrepareFilter、StrutsExecuteFilter、StrutsListener
web.xml加载顺序,context-param -> listener -> filter -> servlet;
启动一个WEB项目的时候,WEB容器会]]>
java nio是jdk1.4引入的网络编程API,相比于以前BIO阻塞方式的接口,能大大提高网络通信的效率。
netty是一个网络通信框架,底层基于java NIO,用来简化NIO编程的开发。因为如果基于NIO直接实现网络编程的开发,比较复杂,很容易出错,而使用了netty之后,只需要关注逻辑处理部分就可以了。此外,netty还提供了多种网络协议的封装。
Netty4的beta3加了AIO了,但是到beta9又去掉了,作者的意思是测试下来AIO性能不如NIO,所以没必要用。
NIO和AIO在linux中底层都是依赖于epoll来实现的。NIO的模型应该为同步非阻塞,AIO为异步非阻塞。底层的具体实现可以参考前面的文章。
本文基于netty4.0源码来分析netty的网络通信模型。
在Netty里,Channel是通讯的载体,而ChannelHandler负责Channel中的逻辑处理,channelPipeline可以理解为channel的载体,每个channel都有一个对应的ChannelPipeline。
一个channel包括一个pipeline、TCP参数、一个unsafe等,netty4的channel类图如下所示:
按IO类型可以分为:BIO和NIO。
按数据类型可以分为:byte,message。
按所处位置可以分为:ServerSocket,Socket。用的比较多的为NioServerSocketChannel和NioSocketChannel。
NioServerSocketChannel继承了AbstractNioMessageChannel,而NioSocketChannel继承了AbstractNioByteChannel。
netty通过一序列的ChannelHandler来处理一个任务,比如read一条消息后,需要decode、验证、计算等各个步骤。在handler中,每个步骤可以写成一个handler。这一序列的handler通过ChannelPipeline串联起来,其实就是Intercepting Filter模式。
ChannelPipeline通过一个双向链表将handler连接起来,在ChannelPipeline里有headContext和tailContext两个handler,来分别表示链表的头和尾,通过ChannelPipeline来控制消息的处理流程。
一个IO事件会被ChannelInboundHandler或ChannelOutboundHandler处理,这些handler再通过调用事件传播方法如fireChannelRegistered或bind等传递给相邻的handler,其实就是事件驱动的思想。
Netty的ChannelPipeline包含两条线路:inbound和outbound。inbound表示接收到消息、被动的消息,outbound表示发送的消息、主动的状态改变。一个handler可以包括inbound和outbound的一种或两种。
inbound的事件传播方法包括以下几种情况:
fireChannelRegistered
fireChannelActive
fireChannelRead
fireChannelReadComplete
fireExceptionCaught
fireUserEventTriggered
fireChannelWritabilityChanged
fireChannelInactive
fireChannelUnregistered
outbound的事件传播方法包括以下几种情况:
bind
connect
write
flush
read
disconnect
close
deregister
例如,有一个称为p的ChannelPipeline,添加了如下的handler:
1 | p.addLast("1", new InboundHandlerA()); |
则现在的链表结构为:head<->5<->4<->3<->2<->1<->tail (注意,每次添加handler都是插入到tail的前面),inbound的执行顺序为5、2、1,而outbound的执行顺序为3、4、5.
下图为从源码注解截取的pipeline流程图:
handler的类图如下所示:
HeadContext和TailContext为pipeline的handler链表中的表头和表尾。此外,netty还提供了很多协议(如protobuf、mqtt、socks等等)的handler用来处理encode、decode等的功能。
buffer的缓存分配器和buffer类图如下所示:
除了可分配定长的buffer外,netty还提供了AdaptiveRecvByteBufAllocator,用来根据本次读取的字节数对下次缓冲接收区的容量进行动态的分配。具体如何分配可参考参考文献3的文档。为了提高效率,netty还提供了基于内存池的缓冲区重用策略(可参考Netty系列之Netty高性能之道一文)。
此外,netty还采用了”Zero-Copy-Capable”机制。我们知道一个报文在网络上传输有可能被拆分成多个,这些被拆分的报文对接收到的上层的逻辑是没有意义的,在netty中,通过将这些buffer组合起来,成为一个channelbuffer,变成一个有意义的报文。当然,zero-copy的含义不止这个,netty的zero-copy机制的具体体现参见李林锋大神的Netty系列之Netty高性能之道一文。如果说NIO的Buffer和Netty的ChannelBuffer最大的区别的话,就是前者仅仅是传输上的Buffer,而后者其实是传输Buffer和抽象后的逻辑Buffer的结合。
例如CompositeChannelBuffer是由多个ChannelBuffer组合而成的,CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写。当然,要真正zero-copy可能要底层系统支持,下文是stackoverflow上的关于OS级别的zero-copy和netty的区别:
OS-level zero copy involves avoiding copying memory blocks from one location to another (typically from user space to kernel space) before sending data to the hardware driver (network card or disk drive) or vice versa.
Netty zero copy is talking about optimizing data manipulation on Java level (user-space only). Their ChannelBuffer allows to read contents of multiple byte buffers without actually copying their content.
In other words, while Netty works only in user space, it is still valid to call their approach “zero copy”.
However, if OS does not use or support true zero copy, it is possible that when data created by Netty-powered program will be sent over the network, data would still be copied from user space to kernel space, and thus true zero-copy would not be achieved.
对于应用服务器,一个主要规律就是,CPU的处理速度是要远远快于IO速度的,如果CPU为了IO操作(例如从Socket读取一段数据)而阻塞显然是不划算的。好一点的方法是分为多进程或者线程去进行处理,但是这样会带来一些进程切换的开销。应用业务向一个中间人注册一个回调(event handler),当IO就绪后,就这个中间人产生一个事件,并通知此handler进行处理。这种回调的方式,也体现了“好莱坞原则”(Hollywood principle)-“Don’t call us, we’ll call you”。
我们如何知道IO就绪这个事件,谁来充当这个中间人?Reactor模式的答案是:由一个不断等待和循环的单独进程(线程)来做这件事,它接受所有handler的注册,并负责先操作系统查询IO是否就绪,在就绪后就调用指定handler进行处理,这个角色的名字就叫做Reactor。在NIO中Reactor的核心是Selector。
Reactor模式里,操作系统只负责通知IO就绪,具体的IO操作(例如读写)仍然是要在业务进程里阻塞的去做的,而Proactor模式则更进一步,由操作系统将IO操作执行好(例如读取,会将数据直接读到内存buffer中),而handler只负责处理自己的逻辑,真正做到了IO与程序处理异步执行。所以我们一般又说Reactor是同步IO,Proactor是异步IO。
如下是netty4.0源码的一个server端的例子(EchoServer):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
29EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc()));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
下面是client端的例子: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// Configure the client.
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
if (sslCtx != null) {
p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
}
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(new EchoClientHandler());
}
});
// Start the client.
ChannelFuture f = b.connect(HOST, PORT).sync();
// Wait until the connection is closed.
f.channel().closeFuture().sync();
} finally {
// Shut down the event loop to terminate all threads.
group.shutdownGracefully();
}
在server端,可以看到有两个NioEventLoopGroup,这两个其实就是两组线程组,称为boss和worker。boss和worker里面都可以包含多个NioEventLoop线程,一般boss设置1个线程就够了,一个线程绑定一个端口,而worker默认的线程数量为cpu个数的2倍。每个EventLoop线程有一个selector和queue,该线程会轮询绑定到此selector的channel,收到绑定的事件后,会触发发事件通知handler处理。
在初始化时,会将NioServerSocketChannel放进boss线程池中的一个eventLoop线程,让eventLoop关联的select轮询,处理client的connect。boss负责接收connect,接收到connect后,new一个NioSocketChannel,并放进worker的一个线程中,让对应的selector轮询处理read、write等操作。
线程模型如下图所示:
注:一个channel包含一个pipeline、TCP参数、unsafe等,一个worker包含多个EventLoop线程,一个EventLoop包含一个selector、一个queue。
上述只是netty常用的一种线程模型,netty可以根据不同的配置,演变出其他的线程模型(可以参考Netty系列之Netty高性能之道一文)。
此外,netty还采用了很多方法来提高netty的性能,如高效的并发编程、高性能的序列化框架、灵活的TCP参数配置能力等。
下面附上网上一个大神(海浪儿)总结的上文所述例子的执行过程,具体源码的解析可以参考参考文献2.
服务端依次发生的步骤
- 建立服务端监听套接字ServerSocketChannel,以及对应的管道pipeline;
- 启动boss线程,将ServerSocketChannel注册到boss线程持有的selector中,并将注册返回的selectionKey赋值给ServerSocketChannel关联的selectionKey变量;
- 在ServerSocketChannel对应的管道中触发channelRegistered事件;
- 绑定IP和端口
- 触发channelActive事件,并将ServerSocketChannel关联的selectionKey的OP_ACCEPT位置为1。
- 客户端发起connect请求后,boss线程正在运行的select循环检测到了该ServerSocketChannel的ACCEPT事件就绪,则通过accept系统调用建立一个已连接套接字SocketChannel,并为其创建对应的管道;
- 在服务端监听套接字对应的管道中触发channelRead事件;
- channelRead事件由ServerBootstrapAcceptor的channelRead方法响应:为已连接套接字对应的管道加入ChannelInitializer处理器;启动一个worker线程,并将已连接套接字的注册任务加入到worker线程的任务队列中;
- worker线程执行已连接套接字的注册任务:将已连接套接字注册到worker线程持有的selector中,并将注册返回的selectionKey赋值给已连接套接字关联的selectionKey变量;在已连接套接字对应的管道中触发channelRegistered事件;channelRegistered事件由ChannelInitializer的channelRegistered方法响应:将自定义的处理器(譬如EchoServerHandler)加入到已连接套接字对应的管道中;在已连接套接字对应的管道中触发channelActive事件;channelActive事件由已连接套接字对应的管道中的inbound处理器的channelActive方法响应;将已连接套接字关联的selectionKey的OP_READ位置为1;至此,worker线程关联的selector就开始监听已连接套接字的READ事件了。
- 在worker线程运行的同时,Boss线程接着在服务端监听套接字对应的管道中触发channelReadComplete事件。
- 客户端向服务端发送消息后,worker线程正在运行的selector循环会检测到已连接套接字的READ事件就绪。则通过read系统调用将消息从套接字的接受缓冲区中读到AdaptiveRecvByteBufAllocator(可以自适应调整分配的缓存的大小)分配的缓存中;
- 在已连接套接字对应的管道中触发channelRead事件;
- channelRead事件由EchoServerHandler处理器的channelRead方法响应:执行write操作将消息存储到ChannelOutboundBuffer中;
- 在已连接套接字对应的管道中触发ChannelReadComplete事件;
- ChannelReadComplete事件由EchoServerHandler处理器的channelReadComplete方法响应:执行flush操作将消息从ChannelOutboundBuffer中flush到套接字的发送缓冲区中;
客户端依次发生的步骤
- 建立套接字SocketChannel,以及对应的管道pipeline;
- 启动客户端线程,将SocketChannel注册到客户端线程持有的selector中,并将注册返回的selectionKey赋值给SocketChannel关联的selectionKey变量;
- 触发channelRegistered事件;
- channelRegistered事件由ChannelInitializer的channelRegistered方法响应:将客户端自定义的处理器(譬如EchoClientHandler)按顺序加入到管道中;
- 向服务端发起connect请求,并将SocketChannel关联的selectionKey的OP_CONNECT位置为1;
- 开始三次握手,客户端线程正在运行的select循环检测到了该SocketChannel的CONNECT事件就绪,则将关联的selectionKey的OP_CONNECT位置为0,再通过调用finishConnect完成连接的建立;
- 触发channelActive事件;
- channelActive事件由EchoClientHandler的channelActive方法响应,通过调用ctx.writeAndFlush方法将消息发往服务端;
- 首先将消息存储到ChannelOutboundBuffer中;(如果ChannelOutboundBuffer存储的所有未flush的消息的大小超过高水位线writeBufferHighWaterMark(默认值为64 * 1024),则会触发ChannelWritabilityChanged事件)
- 然后将消息从ChannelOutboundBuffer中flush到套接字的发送缓冲区中;(如果ChannelOutboundBuffer存储的所有未flush的消息的大小小于低水位线,则会触发ChannelWritabilityChanged事件)
java nio是jdk1.4引入的网络编程API,相比于以前BIO阻塞方式的接口,能大大提高网络通信的效率。
netty是一个网络通信框架,底层基于java NIO,用来简化NIO编程的开发。]]>
1.阻塞与非阻塞
阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
2.同步与异步
所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
例如 ajax请求(异步): 请求通过事件触发->服务器处理(这是浏览器仍然可以作其他事情)->处理完毕。
一个IO操作其实分成了两个步骤:发起IO请求(是否阻塞等待数据,来区分阻塞与非阻塞IO)和实际的IO操作(将数据从内核空间拷贝到用户空间,是否阻塞用来区分同步还是异步)。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此下文将要介绍的IO模型中阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO,IO模型中的异步IO就是异步的。
Linux的内核将所有外部设备都可以看做一个文件来操作,那么我们对与外部设备的操作都可以看做对文件进行操作。我们对一个文件的读写,都通过调用内核提供的系统调用;内核给我们返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,指向内核中一个结构体(文件路径,数据区等一些属性)。
1.阻塞I/O(blocking I/O)
以socket为例,在进程空间中调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。
2.非阻塞I/O (nonblocking I/O)
recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞IO模型进行轮询检查这个状态,看内核是不是有数据到来。
3. I/O复用(select 和poll) (I/O multiplexing)
Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select;这样select/poll可以帮我们侦测许多fd是否就绪。但是select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限。linux还提供了一个epoll系统调用,epoll是基于事件驱动方式,而不是顺序扫描,当有fd就绪时,立即回调函数rollback。
4.信号驱动I/O (signal driven I/O (SIGIO))
首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
5.异步I/O (asynchronous I/O (the POSIX aio_functions))
告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动IO是由内核通知我们何时尅启动一个IO操作,异步IO模型则是由内核通知我们IO操作何时完成。
五种IO模型的比较:
1.select
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:
2.poll
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
3.epoll
epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
epoll的优点:
注:本文所用的图片皆来自《unix网络编程卷1:套接字联网API》一书。
quartz是一个用JAVA实现的开源的任务调度框架。quartz可以用来创建简单或复杂的任务调度,它包括了许多企业级的功能,如支持JTA transactions和集群等。quartz是现在最流行的JAVA任务调度框架。
quartz具有如下的特点:
此外,quartz是spring的默认调度框架,能够很容易的与spring集成。本文基于spring3.2.2和quartz2.2.1来讨论。注意 spring3.1以下的版本必须使用quartz1.x系列,3.1以上的版本才支持quartz 2.x,因为quartz1.x和quartz2.x有些地方不兼容。
quartz调度核心元素:
quartz集群配置:
quartz集群是通过数据库表来感知其他的应用的,各个节点之间并没有直接的通信。只有使用持久的JobStore才能完成Quartz集群。
数据库表:以前有12张表,现在只有11张表,现在没有存储listener相关的表,多了QRTZ_SIMPROP_TRIGGERS表:
Table name | Description |
---|---|
QRTZ_CALENDARS | 存储Quartz的Calendar信息 |
QRTZ_CRON_TRIGGERS | 存储CronTrigger,包括Cron表达式和时区信息 |
QRTZ_FIRED_TRIGGERS | 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的Trigger组的信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关Scheduler的状态信息,和别的Scheduler实例 |
QRTZ_LOCKS | 存储程序的悲观锁的信息 |
QRTZ_JOB_DETAILS | 存储每一个已配置的Job的详细信息 |
QRTZ_SIMPLE_TRIGGERS | 存储简单的Trigger,包括重复次数、间隔、以及已触的次数 |
QRTZ_BLOG_TRIGGERS | Trigger作为Blob类型存储 |
QRTZ_TRIGGERS | 存储已配置的Trigger的信息 |
QRTZ_SIMPROP_TRIGGERS |
QRTZ_LOCKS就是Quartz集群实现同步机制的行锁表,包括以下几个锁:CALENDAR_ACCESS 、JOB_ACCESS、MISFIRE_ACCESS 、STATE_ACCESS 、TRIGGER_ACCESS。
若quartz是配置在spring中,当服务器启动时,就会装载相关的bean。SchedulerFactoryBean实现了InitializingBean接口,因此在初始化bean的时候,会执行afterPropertiesSet方法,该方法将会调用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)创建Scheduler。SchedulerFactory在创建quartzScheduler的过程中,将会读取配置参数,初始化各个组件,关键组件如下:
ThreadPool:一般是使用SimpleThreadPool,SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。在SimpleThreadPool中有三个list:workers-存放池中所有的线程引用,availWorkers-存放所有空闲的线程,busyWorkers-存放所有工作中的线程;
线程池的配置参数如下所示:
1 | org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool |
JobStore:分为存储在内存的RAMJobStore和存储在数据库的JobStoreSupport(包括JobStoreTX和JobStoreCMT两种实现,JobStoreCMT是依赖于容器来进行事务的管理,而JobStoreTX是自己管理事务),若要使用集群要使用JobStoreSupport的方式;
另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:
整个启动流程如下图:
线程的主要逻辑代码如下:
1 | while (!halted.get()) { |
线程执行流程如下图所示:
任务调度执行过程中,trigger的状态变化如下图所示:
下面这些原因可能造成 misfired job:
执行流程:
misfireHandler线程执行流程如下图所示:
初始化:
failedInstance=failed+self+firedTrigger表中的schedulerName在scheduler_state表中找不到的(孤儿)
线程执行:
每个服务器会定时(org.quartz.jobStore.clusterCheckinInterval这个时间)更新SCHEDULER_STATE表的LAST_CHECKIN_TIME,若这个字段远远超出了该更新的时间,则认为该服务器实例挂了;
注意:每个服务器实例有唯一的id,若配置为AUTO,则为hostname+current_time
线程执行的具体流程:
clusterManager线程执行时序图如下图所示:
quartz是一个用JAVA实现的开源的任务调度框架。quartz可以用来创建简单或复杂的任务调度,它包括了许多企业级的功能,如支持JTA transactions和集群等。quartz是现在最流行的JA]]>
1. ASCII
ASCII(American Standard Code for Information Interchange,美国标准信息交换代码),是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。标准ASCII 码也叫基础ASCII码,使用7 位二进制数来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符。其最高位(b7)用作奇偶校验位,后128个称为扩展ASCII码。许多基于x86的系统都支持使用扩展(或“高”)ASCII。扩展ASCII 码允许将每个字符的第8位用于确定附加的128个特殊符号字符、外来语字母和图形符号。
2. ISO-8859-1
ISO-8859-1编码是单字节编码,向下兼容ASCII,其编码范围是0x00-0xFF,0x00-0x7F之间完全和ASCII一致,0x80-0x9F之间是控制字符,0xA0-0xFF之间是文字符号。
3. GB2312
全称《信息交换用汉字编码字符集·基本集》,是双字节编码。GB 2312的出现,基本满足了汉字的计算机处理需要,它所收录的汉字已经覆盖中国大陆99.75%的使用频率。对于人名、古汉语等方面出现的罕用字,GB 2312不能处理,这导致了后来GBK及GB 18030汉字字符集的出现。
4. GBK
GBK全称《汉字内码扩展规范》,它的出现是为了扩展 GB2312,加入更多的汉字,包括繁体字。它的编码是和 GB2312 兼容的。
注意:gbk和gb2312的中文都是两个字节,英文(半角的)一个字节。
5. GB18030
是我国的强制标准,它可能是单字节、双字节或者四字节编码,它的编码与 GB2312 编码兼容,这个虽然是国家标准,但是实际应用系统中使用的并不广泛。
unicode
虽然通过使用不同字符集,我们可以在一台机器上查阅不同语言的文档,但是我们仍然无法解决一个问题:在一份文档中显示所有字符。为了解决这个问题,我们需要一个全人类达成共识的巨大的字符集,这就是Unicode字符集。它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。
虽然每个字符在Unicode字符集中都能找到唯一确定的编号(字符码,又称Unicode码),但是决定最终字节流的却是具体的字符编码,如utf-16或utf-8。例如同样是对Unicode字符“A”进行编码,UTF-8字符编码得到的字节流是0x41,而UTF-16(大端模式)得到的是0x00 0x41。
6. UTF-16
UTF-16 是 Unicode 字符在计算机中存取方法的一种具体编码。UTF-16支持Unicode全字符集的编解码,采用了变长编码,最少使用2个字节,如果要编码BMP以外的字符,则需要4个字节结对。当然,一般用BMP字符就够了,因此在网上很多都写utf-16是两个字节。
7. UTF-8
UTF-8应该是目前应用最广泛的一种Unicode编码方案。由于UCS-2/UTF-16对于ASCII字符使用两个字节进行编码,存储和处理效率相对低下,并且由于ASCII字符经过UTF-16编码后得到的两个字节,高字节始终是0x00,很多C语言的函数都将此字节视为字符串末尾从而导致无法正确解析文本。
对于ASCII字符的编码使用单字节,和ASCII编码一摸一样,这样所有原先使用ASCII编解码的文档就可以直接转到UTF-8编码了。对于其他字符,则使用2-4个字节来表示,其中,首字节前置1的数目代表正确解析所需要的字节数。
UTF-16 与 UTF-8 都是处理 Unicode 编码,它们的编码规则不太相同,相对来说 UTF-16 编码效率最高,字符到字节相互转换更简单,进行字符串操作也更好。它适合在本地磁盘和内存之间使用,可以进行字符和字节之间快速切换,如 Java 的内存编码就是采用 UTF-16 编码。但是它不适合在网络之间传输,因为网络传输容易损坏字节流,一旦字节流损坏将很难恢复,相比较而言 UTF-8 更适合网络传输,对 ASCII 字符采用单字节存储,另外单个字符损坏也不会影响后面其它字符,在编码效率上介于 GBK 和 UTF-16 之间,所以 UTF-8 在编码效率上和编码安全性上做了平衡,是理想的中文编码方式。
在JVM内,从class文件加载的源码全部以UNICODE编码。在内存中倒腾String等数据是编码无关的,但是程序本身难免牵涉到外部文件的读写(如xml,properties文件等)、与数据库的交互、网络数据流读写等。这样就会造成很多非unicode编码的字符存在于JVM中,这也就是乱码出现的根本原因所在。
在使用IDE进行开发时,比如ECLIPSE,IDEA等,可以指定源文件(.java)的编码格式,此处的编码格式是指Java文件自身的编码。而xml、jsp、html等文件是能够自身描述文件编码的。xml可以通过encoding来指定编码方式,而html通过content-type方式来指定,如果打开这些文件,就会调用相应的编码方式来处理。但是经过javac命令编译后,生成的.class文件毫无疑问都是Unicode编码。
jvm在各个阶段都编码方式如下图所示:
在存储java源文件时,是按照java文件的编码对文件进行编码,然后存储在磁盘中。当要对java源文件进行编译时,用源文件的编码对其进行解码,编译生成的class文件为unicode的编码方式。最终jvm加载class文件执行到jvm中,在jvm中的编码方式也是unicode。
在java中,主要用以下两个方法来对String进行编解码:
例如如下的例子:1
2
3String a="中文";//当然你可以以unicode的形式写成String a="\u4E2D\u6587";
byte[] bs = a.getBytes("gbk");
String b= new String(bs,"iso-8859-1");//如果这里使用gbk编码进行解码的话,会自然的得到原来的a。
可以看出这个时候已经为乱码了,不过由于没有信息丢失,所以还是可以恢复成中文的。
恢复的过程为 String c = new String(b.getBytes("iso-8859-1"),"gbk")
;
这种方式中间多了两次编解码,不推荐使用。
用户从浏览器端发起一个 HTTP 请求,需要存在编码的地方是 URL、Cookie、Parameter。服务器端接受到 HTTP 请求后要解析 HTTP 协议,其中 URI、Cookie 和 POST 表单参数需要解码,服务器端可能还需要读取数据库中的数据,本地或网络中其它地方的文本文件,这些数据都可能存在编码问题,当 Servlet 处理完所有请求的数据后,需要将这些数据再编码通过 Socket 发送到用户请求的浏览器里,再经过浏览器解码成为文本。这些过程如下图所示:
1. request编码
URL的几个组成部分如下图所示:
上图中以 Tomcat 作为 Servlet Engine 为例,它们分别对应到下面这些配置文件中:
Port 对应在 Tomcat 的 <Connector port="8080"/>
中配置,而 Context Path 在 <Context path="/examples"/>
中配置,Servlet Path 在 Web 应用的 web.xml 中的。注意这里是在浏览器里直接输入 URL 所以是通过 Get 方法请求的,如果是 POST 方法请求的话,QueryString 将通过表单方式提交到服务器端。
a.URI
一般来说,URL只能使用英文字母、阿拉伯数字和某些标点符号,不能使用其他文字和符号,这是因为网络标准RFC 1738做了硬性规定:
“只有字母和数字[0-9a-zA-Z]、一些特殊符号”$-_.+!*’(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。”
这意味着,如果URL中有汉字,就必须编码后使用。但是麻烦的是,RFC 1738没有规定具体的编码方法,而是交给应用程序(浏览器)自己决定。这导致”URL编码”成为了一个混乱的领域。由于不同浏览器可能采取不同的编码方式,因此url最好不要有中文字符;对于那些用js或模拟浏览器方式请求的(如httpclient),可以将url进行编码再提交,这样就能够得到控制。
在java中有URLEncoder.encode(String s,String enc)/URLDecoder.decode(String s,String enc)
等方法,在javascript中有escape/unescape
,encodeURI/decodeURI
,encodeURIComponent/decodeURIComponent
等方法。
b.POST
POST 表单参数传递方式是通过 HTTP 的 BODY 传递到服务端的。当我们在页面上点击 submit 按钮时浏览器首先将根据 ContentType 的 Charset 编码格式对表单填的参数进行编码然后提交到服务器端。
2. request解码
在服务器收到浏览器发来的请求后,需要对一些内容进行解码:
a.URI
对 URL 的 URI 部分进行解码的字符集是在 connector 的 <Connector URIEncoding=”UTF-8”/>
中定义的,如果没有定义,那么将以默认编码 ISO-8859-1 解析。所以如果有中文 URL 时最好把 URIEncoding 设置成 UTF-8 编码。
b.GET
GET方式的请求,QueryString 的解码字符集要么是 Header 中 ContentType 中定义的 Charset 要么就是默认的 ISO-8859-1,要使用 ContentType 中定义的编码就要设置 connector 的 <Connector URIEncoding=”UTF-8” useBodyEncodingForURI=”true”/>
中的 useBodyEncodingForURI 设置为 true。这个配置项的名字有点让人产生混淆,它并不是对整个 URI 都采用 BodyEncoding 进行解码而仅仅是对 QueryString 使用 BodyEncoding 解码。设置了URIEncoding=“UTF-8”这样也可以。若useBodyEncodingForURI和URIEncoding同时设置了,useBodyEncodingForURI优先级高于URIEncoding。
c.POST
在服务器端同样也是用 ContentType 中字符集进行解码。所以通过 POST 表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过 request.setCharacterEncoding(charset) 来设置。post方式的编码跟useBodyEncodingForURI和URIEncoding没关系。
d.header
Header中传递的其它参数如 Cookie、redirectPath。对 Header 中的项进行解码也是在调用 request.getHeader 是进行的,如果请求的 Header项没有解码则调用 MessageBytes 的 toString 方法,这个方法将从 byte 到 char 的转化使用的默认编码也是 ISO-8859-1,而我们也不能设置 Header 的其它解码格式,所以如果你设置 Header 中有非 ASCII 字符解码肯定会有乱码。
3. response编码
当用户请求的资源已经成功获取后,这些内容将通过 response 返回给客户端浏览器,这个过程先要经过编码再到浏览器进行解码。这个过程的编解码字符集可以通过 response.setCharacterEncoding 来设置,它将会覆盖 request.getCharacterEncoding 的值,并且通过 Header 的 Content-Type 返回客户端。服务器按照response.setCharacterEncoding—contentType—pageEncoding的优先顺序,对要发送的数据进行编码。
4. response解码
浏览器接受到返回的 socket 流时将通过 Content-Type 的 charset 来解码,如果返回的 HTTP Header 中 Content-Type 没有设置 charset,那么浏览器将根据 Html 的 <meta HTTP-equiv="Content-Type" content="text/html; charset=GBK" />
中的 charset 来解码。如果也没有定义的话,那么浏览器将使用默认的编码来解码。
注意:
在java web开发中经常会添加filter来设置字符编码,如下所示:
1 | <filter> |
在CharacterEncodingFilter里面,会执行request.setCharacterEncoding('UTF-8')
的操作。该filter要设置在其他filter之前,否则可能不起作用,因为解码是在 request.getParameter 方法第一次被调用时进行的,如果其他filter通过request获取了参数,那么就解码了,再给request设置编码就不起作用了。
request.setCharacterEncoding('UTF-8')
或者请求header中的content-type中的编码都是针对请求体的,不影响get方式的请求。<%@page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
, pageEncoding和contentType都可以设置JSP源文件和响应正文中的字符集编码,但:1. ASCII
ASCII(American Standard Code for Information Interchange,美国标准信息交换代码),是基于拉丁字母的一套]]>
首先,给系统盘扩容是采用EaseUS Partition Master工具,免费版本的就足够了。打开软件,可以看到如下磁盘的分区信息:
若要扩大C盘的大小,需要先从与C盘临近的盘释放一些空间出来,然后再将这些空间合并到C盘。具体步骤如下:
其次,由于改变了磁盘的分区,双系统的启动项会出现问题,在启动电脑时,会出现如下的问题:1
2error:unknown filesystem
grub rescue>
具体解决方式如下:
查看分区
1 | grub rescue>ls |
寻找Ubuntu所在分区
1 | grub rescue>ls (hd0,msdos1) |
如果是unknown filesystem继续试下一个分区,直到找到Ubuntu所在分区,我的是在(hd0,msdos6)
修改启动分区:
1 | grub rescue>set root=(hd0,msdos6) |
其中可以用set命令来查看配置信息。到这里如果一切正常,继续,否则说明Ubuntu所在分区不正确,先找好再继续。
进入启动菜单:
1 | grub rescue>normal |
进入启动菜单后,立马按C进入命令行模式(如果直接进入系统,那么下次启动系统的时候启动项还是没有修复):
1 | grub>set root=(hd0,msdos6) |
进入Ubuntu修复grub:
1 | sudo update-grub |
重启,完成。
事务必需满足ACID(原子性、一致性、隔离性和持久性)特性,缺一不可:
原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做。如在银行中A给B转账:
1 | update account set money= money - 100 where name='A'; |
上述两条操作要么都执行,要么都不执行,否则会产生纠纷。在数据库管理系统(DBMS)中,默认情况下一条SQL就是一个单独事务,事务是自动提交的。只有显式的使用start transaction开启一个事务,才能将一个代码块放在事务中执行。保障事务的原子性是数据库管理系统的责任,为此许多数据源采用日志机制。
在实际项目开发中数据库操作一般都是并发执行的,即有多个事务并发执行,并发执行就可能遇到问题,目前常见的问题如下:
为了解决这些并发问题,需要通过数据库隔离级别来解决,在标准SQL规范中定义了四种隔离级别:
隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读 | 是 | 是 | 是 | 是 |
提交读 | 否 | 否 | 是 | 是 |
可重复读 | 否 | 否 | 否 | 是 |
序列化 | 否 | 否 | 否 | 否 |
隔离级别越高,数据库事务并发执行性能越差,能处理的操作越少。因此在实际项目开发中为了考虑并发性能一般使用提交读隔离级别,它能避免丢失更新和脏读,尽管不可重复读和幻读不能避免,但可以在可能出现的场合使用悲观锁或乐观锁来解决这些问题。sqlserver和oracle的默认隔离级别都是提交读,而mysql的innodb引擎的默认隔离级别为可重复读,且mysql通过一些机制来达到可重复读也能够避免幻读的情况。
本文只是介绍了事务的基本理论,后面等学完了相关的知识后,再专门介绍mysql的事务隔离级别的实现方法,以及spring锁的底层实现机制。
]]>事务必需满足ACID(原子性、一致性、隔离性和持久性)特性,缺一不可:
Jersey 是 JAX-RS 的参考实现,Jersey1是JAX-RS的参考实现,Jersey2是JAX-RS2的参考实现。Jersey包含三个主要部分。
基于jersey开发rest的流程如下:
通过注解的方式定义rest service类,核心的部分如下:
1 | @Path("/library") |
在上述rest service中,可以通过GET方式访问project-path/library/books获取图书馆的所有书籍,以GET方式访问project-path/library/book/123获取ISBN的id为123的书的信息,其中请求头中的Accept类型需包含application/json类型的,url中的123将作为isbn的值,getBook(ISBN id)的参数值即为isbn的值。
在上述实例中,各个资源的URI命名都是名词,且通过请求方式来进行区分请求的资源,如addBook(Book book)和remobeBook(String id)方法虽然请求的URI相同,但是一个是POST一个是DELETE方式,这样就能通过请求方法来简单的辨别出rest service的功能。此外,上述的Book实体需要在类上添加@XmlRootElement
,ISBN作为参数需要带有一个String类型的构造函数或者一个包含接收单个字符串参数的valueOf静态方法。
添加web.xml配置
1 | <web-app> |
添加rest的servlet,url-pattern可以添加前缀,如果配置成
此外,还有其他的配置方法,如自定义Application类,用来注册service和provider,这样能够更灵活的控制service。通过这种方式实现的,需要在web.xml中添加javax.ws.rs.Application
参数的配置,或者在自定义的Application类中添加@ApplicationPath
注解。用注解方式来实现就可以不用web.xml配置了。
1 | Client c = Client.create(); |
当然,jersey还可以和spring集成,要和spring集成,只需要添加一个jersey-spring的包,并修改web.xml中配置的servlet类为com.sun.jersey.spi.spring.container.servlet.SpringServlet
,很简单吧。
jersey2的包名和jersey1不一样,且jersey2提供了jax-rs2的实现,有异步调用、过滤器拦截器等功能,本文不详细介绍,下文将给出一个实例的地址,写的实例托管在bitbucket中,bitbucket也是国外的,不过在国内访问速度相对于github速度快些,且bitbucket还提供免费的私有仓库(无仓库个数限制,一个仓库最大容量1G,最大协作人数5人)。
最后,给出本实例的代码地址:
Jersey 是 JAX-RS 的参考实现,Jersey1是JAX-RS的参考实现,Jersey2是JAX-RS2的参考实现。Jersey包含三个主要部分。]]>
JAX-RS(Java API for RESTful Web Service,JSR-311)是Java提供用于开发RESTful Web服务基于注解(annotation)的API,在Java EE 6中发布,旨在定义一个统一的规范,使得Java程序员可以使用一套固定的接口来开发REST应用,避免了依赖第三方框架,同时JAX-RS使用POJO编程模型和基于注解的配置并集成JAXB,从而有效缩短了REST应用的开发周期,JSR-311开始于2007年2月,至今发布了两个最终版本1.0,1.1。Java EE7已经发布并且包含了最新的JAX-RS 2.0版本,它是Marek Potociar和Santiago Pericas-Geertsen领导的JSR-339实现。JAX-RS2.0主要的新特性包括:客户端API、异步、HATEOAS(超媒体)、注解、校验、过滤器和处理器(Handler)、内容协商。
JAX-RS定义的包结构如下,包含近五十多个接口,注解和抽象类:
JAX-RS提供了一些标注将一个资源类,一个POJO Java类,封装为Web资源。标注包括:
Web 资源作为一个资源类来实现,对资源的请求由资源类的方法来处理。资源类或资源方法被打上了 Path 标注,Path 标注的值是一个相对的 URI 路径,用于对资源进行定位,路径中可以包含任意的正则表达式以匹配资源。和大多数 JAX-RS 标注一样,Path 标注是可继承的,子类或实现类可以继承超类或接口中的 Path 标注。
资源类是POJO,使用JAX-RS标注来实现相应的Web资源。资源类分为根资源类和子资源类,区别在于子资源类没有打在类上的Path 标注,根资源是由JAX-RS运行时实例化,子资源是由应用本身实例化。资源类的实例方法打上了 Path 标注,则为资源方法或子资源定位器,区别在于子资源定位器上没有任何 @GET、@POST、@PUT、@DELETE 或者自定义的 @HttpMethod。
如下例中,对widgets/offers的GET请求直接被WidgetsResource资源类的资源方法getDiscounted处理,而对widgets/xxx的GET请求则被WidgetResource资源类的getDetail方法处理1
2
3
4
5
6
7
8
9
10
11
12
13
14
15@Path(“widgets”)
public class WidgetsResource {
@GET
@Path(“offers”)
public WidgetList getDiscounted() {…}
@Path(“{id}”)
public WidgetResource findWidget(@PathParam(“id”) String id) {
return new WidgetResource(id);
}
}
public class WidgetResource {
public WidgetResouce(String id) {…}
@GET
public Widget getDetails() {…}
}
JAX-RS 中涉及的资源方法参数的标注包括:@PathParam、@MatrixParam、@QueryParam、@FormParam、@HeaderParam、@CookieParam、@DefaultValue 和 @Encoded。
未被标注的参数(称为实体参数)用于映射请求的实体部分;实体部分与Java类型之间的转换是由实体Provider提供。JAX-RS 规定资源方法中只允许有一个参数没有打上任何的参数标注。
只有public方法才能作为资源方法。
资源方法合法的参数类型包括:
资源方法合法的返回值类型包括:
对于错误处理,资源方法可以抛出非受控异常 WebApplicationException 或者返回包含了适当的错误码集合的 Response 对象。
JAX-RS运行时通过应用提供的Provider类进行扩展;Provider是一个被@Provider标注并实现了一个或多个JAX-RS接口的类。实体Provider用于在请求/响应实体与Java类型之间进行映射,有两种:MessageBodyReader(请求实体映射到Java类型)和MessageBodyWriter(Java类型映射到响应实体)。默认一个JAX-RS应用中每个Provider只有一个实例。
MessageBodyReader接口定义了JAX-RS运行时与那些提供了将实体映射到Java类型功能的组件之间的关系,一个提供该功能的类需要实现MessageBodyReader接口并被@Provider标注。
JAX-RS实现处理请求实体到Java方法参数映射的逻辑步骤如下:
MessageBodyWriter接口定义了JAX-RS运行时与那些提供了将Java类型映射到实体功能的组件之间的关系,一个提供该功能的类需要实现MessageBodyWriter接口并被@Provider标注。
JAX-RS实现处理Java返回类型到响应实体的逻辑步骤如下:
JAX-RS规范只是定义API,真正开发RESTful Web服务需要引入具体实现,具体实现由第三方提供,目前主要的实现有:
在JavaEE7中包含了JAX-RS2.0,在JAX-RS2.0中包含了JAX-RS1的语法,同时新增了一些新的特性:
客户端API
JAX-RS 1.0是一个严格的服务端API。有一些实现提供了不同程度的客户端支持。JAX-RS 2.0添加了“生成器(builder)”工具用于从客户端调用Web服务。以下为一个样例:
1 | // Get instance of Client |
异步处理
在JAX-RS 1.0中,发起调用的客户端必须等待服务端的响应。2.0引入了异步的支持。这样的话就允许客户端发起一个RESTful的请求,并得到一个Future或an InvocationCallback,当响应完成的时候会获得通知。
内容协商
更为丰富的参数注解@Accepts和@Produces能够让你优先安排请求/响应的格式。
1 | Path("widgets2") |
针对上述的请求,因为”text/plain”和”text/html”都匹配,但是”text/html”的因子比较高,因此会返回”text/html”类型的。
下一篇将以jersey为例,举例说明JAX-RS的具体使用方法。
]]>JAX-RS(Java API for RESTful Web Service,JSR-311)是Java提供用于开发RESTful Web服务基于注解(annotation)的API,在Java EE 6中发布]]>
REST 是英文 Representational State Transfer 的缩写,有中文翻译为“具象状态传输”。REST 这个术语是由 Roy Fielding 在他的博士论文 《 Architectural Styles and the Design of Network-based Software Architectures 》中提出的。REST 并非标准,而是一种开发 Web 应用的架构风格,可以将其理解为一种设计模式。REST 基于 HTTP,URI,以及 XML 这些现有的广泛流行的协议和标准,伴随着 REST,HTTP 协议得到了更加正确的使用。
相较于基于 SOAP 和 WSDL 的 Web 服务,REST 模式提供了更为简洁的实现方案。目前,越来越多的 Web 服务开始采用 REST 风格设计和实现,真实世界中比较著名的 REST 服务包括:Google AJAX 搜索 API、Amazon Simple Storage Service (Amazon S3)等。REST 指的是一组架构约束条件和原则。满足这些约束条件和原则的应用程序或设计就是 RESTful。
总的来说,REST对于资源型服务接口来说很合适,同时特别适合对于效率要求很高,但是对于安全要求不高的场景。而SOAP的成熟性可以给需要提供给多开发语言的,对于安全性要求较高的接口设计带来便利。
]]>REST 是英文 Representational State Transfer 的缩写,有中文翻译为“具象状态传输”。REST 这个术语是由 Roy Fielding 在他的博士论文 《 Architectural Styles an]]>
树状数组(BIT ,binary indexed tree)是一种查询和修改的时间复杂度都为log(n)的数据结构。传统的数组连续元素求和的时间复杂度为O(n),而树状数组通过将线性结构转换成伪树状结构,将复杂度降为log(n)。
给定数组A,我们定义数组C,满足C[i]=A[i-2^k+1]+…+A[i],其中k为i转成二进制后末尾的0的个数,i从1开始计算,则我们称C为树状数组。其中2^k = i&(i^(i-1)),即i与i-1做异或运算再与i做与运算。
数组C的具体含义如下图所示:
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
…
C[7]=A[7];
C[8]=A[1]+…+A[8];
分析上面的几组式子可知,当i为奇数时,C[i]=A[i] ;当i为偶数时,就要看i的因子中最多有二的多少次幂。例如,6的因子中有2的一次幂,等于2,所以C[6]=A[5]+A[6](由六向前数两个数的和),4的因子中有2的两次幂,等于4,所以C[4]=A[1]+A[2]+A[3]+A[4](由四向前数四个数的和) 。当我们修改A[i]的值时,只需要沿着C[i]往跟结点上溯,修改这条路上的所有结点即可,这种操作最坏情况下的时间复杂度为树的高度即log(n),比线性结构的数组的复杂度O(n)快。
1 | //求2^k |
[1]. http://hawstein.com/posts/binary-indexed-trees.html
[2]. http://dongxicheng.org/structure/binary_indexed_tree/
[3]. http://www.cnblogs.com/zhangshu/archive/2011/08/16/2141396.html
树状数组(BIT ,binary indexed tree)是一种查询和修改的时间复杂度都为log(n)的数据结构。传统的数组连续元素求和的时间复杂度为O(n),而树状数组通过将线性结构转换成伪树状结构,将复杂度降为log(n)。
给定数组A,我们定义数组C,满足C[i]=A[i-2^k+1]+…+A[i],其中k为i转成二进制后末尾的0的个数,i从1开始计算,则我们称C为树状数组。其中2^k = i&(i^(i-1)),即i与i-1做异或运算再与i做与运算。
]]>书上观察者模式的定义如下:
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
本文引用书上的例子。有各种各样的鸭子,不同的鸭子行为可能不一样,叫声不一样、飞行行为不一样、外观不一样等等。
鸭子的主体: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
29public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {
}
public void setFlyBehavior (FlyBehavior fb) {
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
abstract void display();
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public void swim() {
System.out.println("All ducks float, even decoys!");
}
}
Duck中包含两个实例变量“flyBehavior”与“quackBehavior”,声明为接口类型,每个鸭子对象都会动态的设置这些变量以在运行时引用正确的行为类型(如:FlyWithWings,Quack等)。
鸭子行为的接口:1
2
3
4
5
6public interface FlyBehavior {
public void fly();
}
public interface QuackBehavior {
public void quack();
}
鸭子行为的实现:1
2
3
4
5
6
7
8
9
10public class FlyWithWings implements FlyBehavior {
public void fly() {
System.out.println("I'm flying!!");
}
}
public class Quack implements QuackBehavior {
public void quack() {
System.out.println("Quack");
}
}
具体鸭子的实现:1
2
3
4
5
6
7
8
9
10
11public class RedHeadDuck extends Duck {
public RedHeadDuck() {
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
public void display() {
System.out.println("I'm a real Red Headed duck");
}
}
测试类:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class MiniDuckSimulator {
public static void main(String[] args) {
MallardDuck mallard = new MallardDuck();
RubberDuck rubberDuckie = new RubberDuck();
DecoyDuck decoy = new DecoyDuck();
ModelDuck model = new ModelDuck();
mallard.performQuack();
rubberDuckie.performQuack();
decoy.performQuack();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
书上观察者模式的定义如下:
]]>策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
以前用的是iteye的博客,后来想迁移到博客园,不过在将文章导到博客园时没有成功,就一直用的iteye。原来博客上的很多文章都是转载的(转载的在文章里都有注明出处),也经常没有更新,一方面是由于最近比较忙,都忘了有博客的存在了;另一方面是没有养成写博客的习惯,总觉得麻烦。
当然,我深知写博客的好处,特别是作为一名程序猿,保持写博客的习惯是很有必要的。开通此博客,希望能够多写一些自己原创的文章,记录学习过程中的点点滴滴。
在搭建此博客的过程中也遇到了种种问题,或是请教培哥、XXL,或是找度娘、google。当然,最主要参考的内容是 zipperary的博客。在搭建过程中遇到的较大的问题mark如下:
仅以此文mark自己的新博客。
更新:
博客主题改为jacman。
2015.10.11更新:
原先的博客代码是托管在github上的,最近发现github拦截百度的爬虫,因此百度就无法将博客加入索引。现将博客迁到gitcafe上,gitcafe是国内的平台,速度较快。
再次更新:
gitcafe不稳定,再托管到github上。
以前用的是iteye的博客,后来想迁移到博客园,不过在将文章导到博客园时没有成功,就一直用的iteye。原来博客上的很多文章都是转载的(转载的在文章里都有注明出处),也经常没有更新,一方面是由于最近比较忙,都忘了有博客的存在了;另一方面是没有养成写博客的习惯,总觉得麻烦。
]]>