ttps://www.tony-bro.com/posts/890461404/
恰巧有機會用到Jersey這個RESTful Webservice框架,項目看起來比較老舊,有點臟亂差,遂整合到SpringBoot上,并且同時配上Swagger作為API文檔解決方案。從完全不了解Jersey到整合完成耗時并不長,總得來說還是非常方便的。
說起來Java總是一堆規范,這回也不例外,Jersey同樣是JAX-RS規范的標準開源實現。JAX-RS即Java API for RESTful Web Services,屬于Java EE6的一部分,現如今已經發展到2.1版本(JSR311、JSR370)。本篇不討論Jersey本身的實現,因此大部分的應用情景使用的就是JAX-RS的接口和注解。
如今web中的概念頗多,MVC、WebServie、SOA、微服務等等,尤為理不清的就是這WebService,在UDDI、WSDL(WADL)的概念下迷失。但是刨開有的沒的,說到底就是Request和Response,在如今前后端分離的情境下MVC的V基本被砍光,剩下的MC和除去自描述的WebService在應用接口的開發上個人認為已經極其相似,使用的時候也可以發現兩者形式也基本一樣。那么用慣了SpringMVC后,學習使用Jersey并不會有什么障礙,縱觀發展可以發現SpringMVC在完善支持返回json結構體,而Jersey現如今也支持MVC模板,殊途同歸。
Jersey目前也是兩個大版本1.x和2.x,2.x也不是什么新鮮事物了,因此就以2.x為準。關于Jersey的入門推薦參看https://www.jianshu.com/nb/21274943這里面的幾篇文章,講的算比較不錯了。本篇就相當于濃縮再濃縮,詳細了解還是參考官方文檔最為穩妥。
在RESTful WebService中,資源是核心,整個應用圍繞資源的映射和處理。與WebMVC所對應的便是請求的映射即RequestMapping,兩者只是解釋的入口點不同。對應的示例代碼如下:
// Spring MVC
@RequestMapping(value="/comments" ,method=RequestMethod.GET)
@ResponseBody
public JsonResult getComments(@RequestParam(value="articleid") Long articleid,
@RequestParam(value="pn",defaultValue="1") Integer pn){
// some implementation
}
// JAX-RS
@Path("/comments")
@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonResult getComments(@QueryParam(value="articleid") Long articleid,
@QueryParam("pn") @DefaultValue("1") Integer pn){
// some implementation
}
簡單列一下主要注解:
JAX-RS | 描述 | SpringMVC |
@Path | 配置路徑,可以置于類和方法上 | @RequestMapping |
@PathParam | 路徑變量的獲取,@Path指定的路徑同樣可以配置{param} | @PathVariable |
@GET、@POST等 | 描述請求方法,相同功能的還有@HttpMethod | @GetMapping等 |
@CookieParam、@FormParam等 | 快速綁定不同類型的參數 | |
@DefaultValue | 參數的默認值 | @RequestParam(default="…") |
@Consumes、@Produces | 定義Http接收、返回類型 |
對于表單請求,Jersey額外提供了@BeanParam注解,可以將@FormParam注解在Bean的屬性上,然后直接使用@BeanParam接收參數。也可以直接通過MultiValuedMap<String, String>來接收參數。
相對特殊的@MatrixParam可以用于接收/resource;pageSize=10;currentPage=2`這個請求中的pageSize和currentPage,這樣使用的意圖是將每一個分頁看做一個資源,分頁信息不是參數而是資源的一部分。
JAX-RS中提供了應用類javax.ws.rs.core.Application,定義了應用的基本組件信息,為了方便配置,Jersey提供了一系列繼承了Application的ResourceConfig類,可以使用此類來快速配置。如果需要配置Jersey的根路徑,在Servlet3.0以上的環境下可以使用注解@ApplicationPath("BasePath")注解在配置類上。
任何Web框架都會涉及信息的轉換,用于將請求信息轉換成可編程的對象,例如SpringMVC的MessageConverter。在JAX-RS中定義了兩個接口:MessageBodyWriter和MessageBodyReader,通過實現這兩個接口來處理輸入輸出的轉換,查看繼承關系可以看到Jersey默認提供的一些轉換器。
JAX-RS定義了兩個面向切面的工具Filter和Interceptor,注意這個Filter和Servlet的Filter并不相同,行為上也不一樣,JAX-RS規范下的Filter和Interceptor都是單向的,請求歸請求,響應歸響應。過濾器一般用來處理頭部信息且分為客戶端過濾器和服務端過濾器,服務端核心接口為ContainerRequestFilter和ContainerResponseFilter;而攔截器主要用于修改實體的內容,比如編碼解碼等,核心接口為WriterInterceptor和ReaderInterceptor。
需要注意如果請求不存在的地址,則RequestFilter不會執行,而只要有響應ResponseFilter就會執行,這實際上涉及到Filter的匹配時機,匹配時機分為PreMatch和PostMatch,默認為PostMatch。整個請求在服務端的執行順序如下:
支持使用注解@Context注入一些特定的Web對象來輔助處理,包括:
可以置于方法參數上,也可以置于類成員上,同樣是通過動態代理來實現。
為支持上傳和下載需要引入jersey-media-multipart包,并且在配置類中注冊MultiPartFeature。
上傳時可以使用@FormDataParam,可注解在InputStream和FormDataContentDisposition上,用于獲取文件數據和文件描述。
下載時可以使用javax.ws.rs.core.Response直接攜帶文件構建出適合的響應。
// 上傳
@POST
@Path("image1")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public String upload(@FormDataParam("file") InputStream fileInputStream,
@FormDataParam("file") FormDataContentDisposition disposition) {
File upload=new File(ctx.getRealPath("/upload"), disposition.getFileName());
try {
// handle input stream...
} catch (IOException e) {
e.printStackTrace();
}
return "success";
}
// 下載
@GET
@Path("/images2/{name}")
public Response showImg(@PathParam("name") String imageName) throws IOException {
File f ;
// find and get file...
if (!f.exists()) {
return Response.status(Status.NOT_FOUND).build();
} else {
return Response.ok(f).header("Content-disposition", "attachment;filename=" + imageName)
.header("Cache-Control", "no-cache").build();
}
}
JAX-RS提供了一個標準的異常類WebApplicationException(繼承RuntimeException)可以在資源方法、provider、StreamingOutput中拋出這個異常讓Jersey統一處理。WebApplicationException有很多構造方法來滿足指定異常信息,但是在實際使用過程中比較難滿足定制的需求。
此外還提供了ExceptionMapper接口,該接口只有一個方法:Response toResponse(E exception)即針對指定類型的異常如何生成Response對象,這樣就完成了異常類型和返回對象的映射。
@Provider
public class ErrorHandler implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(ResponseEntity.status(500).body(exception.getMessage()+"-"+this))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
自Jersey2.x以來,Jersey的DI(依賴注入)默認由HK2這個符合J2EE標準的框架來實現。如果要整合SpringBoot,首先需要整合Spring容器。這一步同樣已經提供好了解決方案,引入jersey-spring包即可,本質上是由HK2提供的Spring-Bridge來完成轉換的。
整合Spring后,值得注意的是Jersey本身會掃包,但為了讓Spring來管理相關的組件仍然需要在組件上增加注解@Component(純注解配置模式)。此外在Spring管理下組件的默認行為變為單例。
SpringBoot官方提供了Jersey的自動配置類JerseyAutoConfiguration和依賴包spring-boot-starter-jersey。注意只需要引入spring-boot-starter-jersey而不需要再額外引入spring-boot-starter-web,Jersey和SpringMVC不需要同時存在,spring-boot-starter-jersey已經引入了足夠的依賴項來啟動一個基本的應用服務。
可以看到額外包含了Json序列化和參數校驗特性。
JerseyAutoConfiguration這自動配置類中的內容也很簡潔,主要包含3個部分:
搭建完工程后,可以選擇直接定制注入一個ResourceConfig,也可以讓組件實現ResourceConfigCustomizer來分散配置。
Jersey獲取組件是通過掃包或者手動注冊,SpringBoot有額外提醒不推薦使用Jersey的掃包功能,因為在war包環境下有可能出現掃包失敗,詳見原文地址。
然后整合就基本上完成了,可配的屬性不是特別多,主要包括選擇filter還是servlet、servlet啟動時加載(load-on-startup)、以及初始化參數init。
雖然說WebService有自描述的功能,可以配合客戶端來使用WSDL、WADL,但是可讀性不好,如果是用來編寫應用的接口,就更加額外需要編寫相關的接口文檔。Swagger的動態特性非常不錯,同時對RESTful風格的接口支持尤其出色,因此使用它作為接口文檔解決方案。
不同于與SpringMVC整合時有方便的spring-fox來便捷處理,與Jersey整合時需要額外進行一些操作,但也不麻煩。目前Swagger已經發展到3系列,不過應用還不是特別廣,所以仍選用了swagger2。引入依賴:
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-jersey-jaxrs</artifactId>
<version>1.5.22</version>
</dependency>
然后在Jersey的配置類中注冊swagger組件,并且定制swagger內容
@Component
@ApplicationPath("your_base_path")
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
this.packages("com.demo.jerseyboot");
this.register(MultiPartFeature.class);
this.property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
this.configureSwagger();
}
private void configureSwagger(){
// 注冊兩個JAX-RS組件
this.register(ApiListingResource.class);
this.register(SwaggerSerializers.class);
// swagger定制
BeanConfig config=new BeanConfig();
config.setConfigId("數據服務接口文檔");
config.setTitle("數據服務接口文檔");
config.setVersion("v1");
config.setContact("tony");
config.setSchemes(new String[] { "http" });
config.setBasePath("your_base_path");
config.setResourcePackage("com.demo.jerseyboot");
config.setPrettyPrint(true);
config.setScan(true);
}
}
查看ApiListingResource這個類,可以看到swagger注冊了一個接口,請求/swagger.json或者/swagger.yaml就可以獲取到API文檔的信息數據。
可以看到真正起作用的是swagger-jaxrs包下的內容,因此如果需要使用swagger3,引入swagger-jaxrs2即可。
擁有文檔信息后還需要使用Swagger-UI來進行可視化展現。為了方便,采用webjars技術來引入相關的靜態網頁資源。引入依賴:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>swagger-ui</artifactId>
<version>3.22.0</version>
</dependency>
在這個jar包內包含了Swagger-UI網頁的各種資源,查看index.html可以發現其中調用獲取接口信息的地址默認是https://petstore.swagger.io/v2/swagger.json,因此需要將其拷貝出來復制在類路徑下,修改其中靜態資源的引用路徑和接口信息的路徑。
此時就需要處理靜態資源的訪問,由于不使用SpringMVC,那么也就沒辦法直接使用其提供的ResourceHttpRequestHandler,需要額外配置Web容器的靜態資源訪問。首先配置Jersey的根路徑,不要配置為/防止沖突,然后配置容器或者Servlet。
對于webjars資源的訪問,SpringBoot已經做了默認配置,獲取webjar資源路徑的方法位于org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory#getUrlsOfJarsWithMetaInfResources,查看AbstractServletWebServerFactory的子類中使用該方法的位置就可以看到對webjars資源的配置操作。
如果使用Jetty作為容器,那么可以選擇使用Jetty提供的ResourceHandler
@Component
public class JettyResourceCustomizer implements WebServerFactoryCustomizer<ConfigurableJettyWebServerFactory>, Ordered {
@Override
public void customize(ConfigurableJettyWebServerFactory factory) {
factory.addServerCustomizers(new JettyServerCustomizer() {
@Override
public void customize(Server server) {
ResourceHandler staticHandler=new ResourceHandler();
staticHandler.setDirectoriesListed(true);
staticHandler.setWelcomeFiles(new String[]{"index.html"});
ContextHandler context0=new ContextHandler();
context0.setContextPath("/api-doc");
context0.setBaseResource(Resource.newClassPathResource("static"));
context0.setHandler(staticHandler);
// 注意不要覆蓋原有的Handler
Handler[] handlers=server.getHandlers();
Handler[] newHandlers=Arrays.copyOf(handlers, handlers.length + 1);
newHandlers[handlers.length]=context0;
ContextHandlerCollection contexts=new ContextHandlerCollection();
contexts.setHandlers(newHandlers);
server.setHandler(contexts);
}
});
}
@Override
public int getOrder() {
// 確保在原生handler配置之后執行
return 100;
}
}
如果使用嵌入式Tomcat容器,則會稍微麻煩一些,嵌入式Tomcat的文檔很少,最后參看SpringBoot對Webjars資源的配置找到了解決方案
@Component
public class TomcatStaticResourceCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers(new TomcatContextCustomizer() {
@Override
public void customize(Context context) {
WebResourceRoot root=context.getResources();
// 此時資源root為null
System.out.println(root);
// 使用tomcat的listener進行配置
context.addLifecycleListener(new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.PRE,"/api-doc", this.getClass().getResource("/"),"/static"
);
}
}
});
}
});
}
}
例如將index.html拷貝至如下的位置,并且配置Jersey的根路徑為/api
修改其中的內容
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="../webjars/swagger-ui/3.22.0/swagger-ui.css" >
<link rel="icon" type="image/png" href="../webjars/swagger-ui/3.22.0/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="../webjars/swagger-ui/3.22.0/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="../webjars/swagger-ui/3.22.0/swagger-ui-bundle.js"> </script>
<script src="../webjars/swagger-ui/3.22.0/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload=function() {
// Begin Swagger UI call region
const ui=SwaggerUIBundle({
url: "../api/swagger.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui=ui
}
</script>
</body>
</html>
訪問host:port/api-doc/index.html就可以看到自動生成的API文檔。
說實話Jersey來整合SpringBoot個人感覺意義并不是特別大,SpringBoot講求一個便捷快速,Jersey本身同樣也很輕巧,且SpringMVC擺在這里,頗有些食之無味,棄之可惜的感覺。整合SpringBoot后最大的好處還是IOC、AOP以及Spring生態的支持以及其他工具的快速整合,由此可見生態的重要性。
總體來說JAX-RS這套API挺不錯的,該有的都有,對比SpringMVC可以發現Web開發的共同之處,確有裨益。
Web應用開發是企業開發的重要領域,Spring Boot 1.X的Web容器管理方式基于Servlet容器技術棧。Servlet容器主要基于同步阻塞I/O架構,HTTP請求和線程是一對一的關系,主要是TPR模型,即一個請求對應一個線程。主要的業務邏輯也是基于命令式的編程模式。以Spring MVC框架為主,Web容器方面以Tomcat為主,也可以通過自動配置功能改為Jetty/UnderTow容器。
Spring Boot 2.X主要基于異步非阻塞I/O架構,HTTP請求基于收斂的線程模型,網絡層使用基于Reactor的I/O多路復用模式,業務邏輯基于函數式編程模式,以Spring WebFlux為主要框架。在Web容器方面可以基于Servlet 3.0的異步模式,默認情況下使用Netty作為容器。本節我們主要以Spring Boot 1.X講解嵌入式Web容器的啟動和加載原理,在進階篇的響應式編程中將介紹Spring 5及Spring Boot 2.X的響應式框架WebFlux對Web應用服務的支持。
Spring Boot對Web項目的支持主要是Spring Boot對Spring MVC框架的繼承。Spring MVC框架是一個基于Servlet容器標準的Web容器框架實現,Spring Boot向Spring MVC提供開箱即用的Starter:springboot-starter-web。
Spring Boot應用中利用自動配置功能,只需要在pom.xml文件中加入下面的Web依賴,就可以直接啟動一個Web服務:
Spring Web MVC 框 架 使 用 特 定 的 @Controller 或 者@RestController 注 解 的 Bean 作 為 處 理 HTTP 請 求 的 端 點 , 通 過@RequestMapping注解將控制器中的方法與HTTP請求進行映射,示例如下:
Spring Boot為Spring MVC提供了自動配置功能,包含如下主要配置特性。
● 自動配置ViewResolver引入ContentNegotiatingViewResolver組件功能。示例:在應用中添加ViewResolver組件用來匹配HTML靜態頁面,如果沒有匹配成功,則返回false,由其他ViewResolver繼續嘗試匹配。ContentNegotiatingViewResolver會組合所有的視圖解析器,代碼如下。
● 自動注冊Converter、GenericConverter、Formatter Bean。
示例:將頁面提交數據轉化為后臺數據,實現格式化,代碼如下。
● 對HttpMessageConverters的支持。
示例:Spring Boot可以為HttpMessageConverters類添加自定義轉換類,通過這種方式可以將所有的HttpMessageConverters的Bean添加到Converter列表,覆蓋默認的轉換器列表,代碼如下。
● 自動注冊MessageCodeResolver。
● 自動使用ConfigurableWebBindingInitializer Bean。
● 使用WebMvcConfigurerAdapter類型的Bean來定制化配置。
默認情況下,Spring Boot會以/src/main/resources/static作為查找靜態資源的文件路徑,如果想自定義靜態資源映射目錄,需要重寫addResourceHandlers來添加指定路徑,重寫addResourceLocations來指定靜態資源路徑。
總之,我們可以根據自己的意愿,對默認的Spring MVC的組件配置加以修改,方法也很簡單,通過在IoC容器中注冊新的同類型Bean來替換即可。如果你希望完全接管Spring MVC的所有相關配置,可以添加自己的@Configuration,并使用@EnableWebMvc注解實現定制化配置。
JAX-RS和Jersey框架
如果你喜歡JAX-RS和REST風格的編程模型,可以使用下面的Starter替代Spring MVC框架,Spring支持Jersey 1.X和Jersey 2.X等技術框架。這里我們只介紹Spring Boot對Jersey 2.X的支持,在pom.xml文件中加入下面的依賴:
Spring Boot對Jersey的配置有三種主要方式。在開始不同的配置方式前,我們注冊一個端點對象資源,示例代碼如下:
● 第一種方式,創建一個自定義的ResourceConfig:
● 第二種方式,返回一個ResourceConfig類型的@Bean:
● 第三種方式,配置一組ResourceConfigCustomizer對象。
Spring Boot提供了ResourceConfigCustomizer接口,讓我們更靈活地對ResourceConfig對象進行配置。要使用該接口,我們需要先注釋掉前面兩節中提到的相關代碼,然后創建一個類:
默 認 情 況 下 , Jersey 將 以 Servlet 的 形 式 注 冊 一 個
ServletRegistrationBean 類 型 的 @Bean 。 它 的 名 字 為
jerseyServletRegistration,該Servlet默認會延遲初始化。
你可以通過spring.jersey.servlet.load-on-startup自定義配置
Jersey組件。通過創建相同名字的Bean,可以禁用或覆蓋框架默認的
Bean。設置spring.jersey.type=filter可以使用Filter的形式代替
Servlet , 相 應 的 @Bean 類 型 變 為 jerseyFilter-Registration , 該
Filter有一個@Order屬性,你可以通過spring.jersey.filter.order
設 置 該 屬 性 。 Servlet 和 Filter 在 注 冊 時 都 可 以 使 用
spring.jersey.init.*定義一個屬性集合并將其傳遞給init參數進行
初始化。
內嵌容器的配置
Spring Boot 的 另 一 大 特 性 就 是 支 持 內 嵌 的 Web 容 器 , 包 括Tomcat、Jetty和UnderTow服務器,大多數開發者只需要使用合適的Starter來獲取一個完全配置好的實例即可,內嵌服務器默認監聽8080端口的HTTP請求。spring-boot-starter-web默認使用Tomcat作為Web容器,你可以在pom.xml中去除spring-boot-starter-tomcat依賴,然后 引 入 spring-boot-starter-jetty 或 者 spring-boot-starterundertow模塊作為替代Web容器方案。Starter還提供了以“server.”為前綴的配置項對嵌入式容器配置進行修改。配置項的加載和定制化鉤子加載過程如下。
1.自動化配置嵌入式容器
2.初始化TomcatEmbeddedServletContainerFactory的Bean對象
3.定制化Bean擴展邏輯
EmbeddedServletContainerCustomizerBeanPostProcessor在加載Bean后開始初始化配置項PostProcessor的處理邏輯:
4.配置文件加載
從配置文件中,你可以加載配置文件對象的配置值。如果配置文件中沒有相關配置項,將使用默認代碼設定配置。
5.Web容器定制化
如果你需要對Web容器進行更深入的定制,可以使用對應的Factory自動化配置Tomcat容器,它是初始化的關鍵流程和步驟,代碼示例如下:
下圖是Spring Boot啟動過程中Tomcat容器完成自動配置的類圖結構。我們在最新的Spring Boot下查看Tomcat的相關配置,發現有兩個自動裝配類,分別包含了三個定制器,還有一個工廠類。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。