为什么要用Provider
provider是在程序运行时自动调用的一些类,起到配置与修改http请求与回应的作用,是JAX-RS里不可或缺的一项组件。从上一讲可以看到,Resource组件已经能够完全实现RESTful API,在JAX-RS标准里无非就是添加一些注解的事。然而,实现功能只是最基本的要求,要做出企业级的应用,必须符合一些标准和规范,才能最大程度地提高团队的开发效率,并从最佳实践的角度去吻合市场的需求。RESTful API的一些设计规范,就必须要使用到provider去做配置。
几种provider的介绍
所有类名上添加了@provider注解的类,都属于provider组件。下面将介绍一下常用的provider。
ContainerRequestFilter,ContainerResponseFilter
过滤器,顾名思义,是对进入服务器与从服务器输出的数据进行过滤。由于RESTful API实际上是操作http协议,那么过滤器便是对http request或http response的header或body进行过滤,作出一些修改,或判断是否拒绝该http request或http response。
containerRequestFilter的核心是filter方法,接收ContainerRequestContext对象,这个对象封装了http request。一般在这个方法里对header进行操作。ContainerResponseFilter的核心也是filter方法,接收ContainerResponseContext和ContainerResponseContext对象,这个对象封装的则是http response,也主要针对其heaer操作。
下面从两个主要的设计规范点出发,看看怎么用JAX-RS的过滤器来实现。
- HTTP方法重载。上一讲提到,RESTful API涉及的HTTP方法有GET,POST,PUT,PATCH,DELETE这几种,而有些http proxy只能用GET和POST方法。一般的解决方法是将PUT,PATCH和DELETE请求也用POST方法发出,但在header里添加X-HTTP-Method-Override这个key,来存储真正的方法名。那么服务器端怎么利用http header,来进行方法重新匹配的呢?这就需要用到ContainerRequestFilter了。下面请看代码:123456789101112public class HttpMethodOverrideEnabler implements ContainerRequestFilter {public void filter(ContainerRequestContext requestContext) throws IOException {String override = requestContext.getHeaders().getFirst("X-HTTP-Method-Override");if (override != null) {requestContext.setMethod(override);}}}
先来看@PreMatching注解。所有的provider都对应特定的资源类或者资源方法,当http request到达,会首先根据其URI和HTTP方法类型去匹配相应的Resource组件类的某个方法,这在上一讲已经讲过。一般情况下所有ContainerRequestFilter的执行时机是在方法匹配之后,但是我们的需求是在方法匹配之前对方法进行重载,根据X-HTTP-Method-Override里的方法名进行重匹配。这就需要用到@PreMatching注解了,它能把过滤器的执行时机提前到方法匹配之前。filter方法里代码也很简单,就是从requestContext里提取header里的X-HTTP-Method-Override字段,如果不为空则用setMethod方法进行重匹配。
- 对访问频率进行限制。我们提供的RESTful API服务允许开发者访问,但必须设置一定的安全措施,防止被滥用,被过度调用,被恶意攻击,因此必须对资源的访问频率进行限制。在限制的同时,我们也应该提供给调用者一些信息,让他们获知每周期的调用限制数,每周期内的剩余次数,以及到下一个周期的剩余时间。这些的实现同样可以用过滤器以及header来实现。请看代码:
|
|
|
|
由于http协议是无状态的,而用户的访问频率是需要保存的状态,因此需用cookie来实现。cookie主要存储两个值,一个是本周期内还可以访问的次数,一个是本周期结束的时间。利用这两个值,在ContainerRequestFilter内判断是否接受这次请求,并在ContainerResponseFilter内更新每次请求过后cookie的值。这里有一个坑需要特别注意,ContainerResponseContext用getCookies()方法获取的cookie是只读的,不能修改,如果要更新cookie必须直接在http response的header里写入Set-Cookie字段。这也是http response携带cookie的规范做法。并且,NewCookie对象在创建时候必须指定路径。因为相同路径和域名的cookie是保存在同一个文件夹里的,一般是取工程路径,以表示相同工程内使用的cookie,不然在添加cookie的时候即便key一样也会插入新数据,而不会覆盖原来的cookie,达不到更新的效果。
反馈给用户的信息是保存在response的header里的,X-Rate-Limit-Limit表示每个累计周期内的访问次数最大是多少;X-Rate-Limit-Remaining表示目前的累计周期内还剩余多少次可以用;X-Rate-Limit-Reset表示还有多少秒钟累计周期就到期了,可以开始下一个周期了。这些header信息都是RESTful API的设计规范里包含的。
如果用户访问频率超出限制,需要抛出error code为429的http response,429在http标准里是表示Too Many Requests。这也是RESTful API的规范之一。充分利用错误码来表达操作的结果,设计RESTful API的时候对每一条http response都必须返回相应的错误码,不得有误。更详细的error code规范会在后面介绍,这里先提一下。
MessageBodyWriter和MessageBodyReader
这两类是进行对象的序列化与反序列化的。在基本的RESTful API开发中并不常用,因为如上一讲所说,利用@XmlRootElement这个JAXB注解便可轻松解决xml与json的序列化与反序列化,只有更高层次的要求才需要自己实现这两个类。我也并没有怎么使用过,对这两个类的认识也仅停留在官方文档的介绍,日后如果有机会实践过后,有更深入的理解再行补充。
ContextResolver
前面说过,RESTful API规范里数据传输的格式是json,而JAX-RS序列化后的json格式往往是没有缩进和换行的,这就使得json文本不方便阅读。RESTful API规范要求json必须按照pretty print的方式输出。实现pretty print一种比较简单的方法是使用Jackson框架,利用ObjectMapper类将对象映射成相应的json文本,并且其pretty print模式。下面请看代码:
|
|
其中objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true)便是让输出使用pretty print。注意的是,使用Jackson框架必须要在Application的实现类里注册相应资源。
|
|
resources.add(JacksonFeature.class)这一句是不能省的。
WriteInterceptor,ReadInterceptor
拦截器,与过滤器的作用有很多相似的地方。不同之处在于,过滤器一般用来处理header,拦截器则用来处理http request与response的body里的entity的输入输出流。可以利用拦截器对输入流进行统一的解码或解压缩,或者对输出流进行统一的编码或压缩。
RESTful API规范里要求对response同一使用GZIP压缩,把pretty print处理后的json文本压缩处理,节省带宽,节约流量。
|
|
上面的代码是利用WriteInterceptor进行GZIP压缩的示例,十分简单,ReadInterceptor类似,只是换成操作输入流。只需要在输出流外再包装一层GZIPOutputStream的包装流便可。注意必须在header里添加Content-Encoding字段,这样浏览器才知道怎么解压缩。
上面最值得一提的是context.proceed()这个方法。所有拦截器必须调用这个方法,调用这个方法后会自动调用下一个拦截器,直到到达拦截器链末端的最后一个拦截器,此时该拦截器调用proceed()方法后会调用MessageBodyWriter或MessageBodyReader去进行序列化或反序列化。
拦截器以及MessageBodyWriter和MessageBodyReader都是在entity不为空的情况下才会调用的,若response不返回数据或者request的body不携带数据,则这些类都不会被使用。
provider的调用顺序
- pre-matching ContainerRequestFilters
- post-matching ContainerRequestFilters
- ReaderInterceptor
- MessageBodyReader
- ContainerResponseFilters
- WriterInterceptor
- MessageBodyWriter
资源绑定
默认情况下所有的http请求的到来都会让所有provider类自动运行相应的方法,但我们可以通过资源绑定的方式,让某些provider只会被某些request触发。主要有两种方式:Name binding和Dynamic binding
- Name binding
通过@NameBinding注解实现:
- 定义注解,例如:
|
|
- 为provider添加该注解:
|
|
- 为相应的资源方法或资源类添加该注解:
|
|
|
|
当访问带有@Tracked注解的方法或类时,带有@Tracked的provider便会在相应的资源方法调用前后自动触发。从而实现资源绑定。
- Dynamic binding
动态绑定是通过DynamicFeature接口实现的,代码如下:
|
|
configure()方法里接收两个参数,ResourceInfo对象用来对request要访问的资源进行过滤,FeatureContext对象用来对相应的provider进行动态注册。
异常处理以及返回Response对象
如果没有错误正常返回,一般直接返回结果对象或者什么也不返回即可,这时不需要额外配置response的header或status code。但是,若抛出异常或者结果不正确,则需要返回相应的status code。这时候便需要我们构造Response对象返回。
JAX-RS的异常处理建议统一使用WebApplicationException类或者使用ExceptionMapper类来实现。
WebApplicationException
直接抛出WebApplicationException:
|
|
接收的参数可以是用相应status code构造的Response对象,也可以直接用status code作为参数。此时JAX-RS便会构造出相应status code的http response报文。
ExceptionMapper
如果在程序运行期间抛出异常,导致不能返回正确结果,可以用ExceptionMapper的实现类来捕捉特定的异常。代码如下:
|
|
ExceptionMapper可以定义成任何异常类的处理类,在toResponse()方法里进行异常处理,并构造Response对象返回。有这么一个机制来统一进行异常处理,使得代码管理十分清晰,行文十分优雅。
正常返回Response对象
即便是在资源方法里正常返回,但如果除了传输数据外,还需要携带header,cookie等信息,则也需要构造Response对象返回,正常数据则作为Response对象里的GenericEntity对象保存。
在正常的业务逻辑里,我们读取一些数据的时候往往需要翻页信息,页数以及每页的数据项数在RESTful API规范里往往是作为http request的query params传输的,而相应的http response也通常会在header里返回上一页,下一页,第一页以及最后一页数据的URI,方便用户继续访问。下面就以这一个规范点作为需求,看看代码里怎么构造翻页信息的Response对象的:
|
|
GenericEntity保存需要返回的body里的数据。在header里添加上相应页面资源的URI,并且让X-Total-Count返回总的数据项数。这些信息都是提供给用户的。Response的ok()方法接收entity参数,并且构造status code为200的http response返回给用户。
总结
这一讲主要讲的是JAX-RS的provider组件,并涉及了一些异常处理和构造Response对象的内容,其实主要就是围绕http request和http response展开的。希望读者能够体会到,RESTful API归根结底就是http报文,http报文又可以分为header和body。对header和body,RESTful API都有一定的规范对其进行限制,而实现这些规范的手段便是provider,异常处理以及Response对象。RESTful API必须对每一个接口的request和response的header和body有清楚的定义。手段方法是其次,RESTful API的思想与规范才是根本。下一讲会把一些还没涉及的设计规范讲一下,但用到的技术基本就是这一讲和上一讲Resource和Provider组件的内容了。