幫大家複習開發一個 Web API 提供給前端使用主要流程如下:
- 建立 Java Web Appication 並提供 RESTful Web Service
- 透過 War 檔發佈至容器: 將程式發佈至伺服器供前端存取
- 讓撰寫的 RESTful Web Service 存取實體資料庫
在前一篇文章整理了網路上教學範例實作心得 透過 Java 開發 Web API (RESTful API),這篇文章將進一步整理 Jersey 在使用上需要了解的六個地方:
- 常用的 Annotation
- Filters & Interceptors
- API 權限管理
- NameBinding 和 Dynamic Binding
- JAX-RS Response & Error Message
- Maven 設定
什麼是 Jersey?
Jersey 是基於 JAX-RS 來實現 RESTful Web Service 的一個 Framework
- 使用大量的 Annotation 來簡化撰寫
- 支援 XML (jaxb) 及 JSON (jackson)
使用 Jersey 開發 RESTful API 完成後,將應用程式放到任何一種像是 jettyt、Tomcat、JBoss 就能完成 http server 的架設。
Jersey RESTful API 簡單範例
假設今天想要開的 API 路徑是 http://localhost:{PORT}/{專案名稱}/rest/test
- @ApplicationPath: 設定應用程式層級的路徑,這個例子中為
rest
,透過 Annotation 來幫你分配底層的 Servelt - @Path(“/test”): 定義功能路徑 test
- @GET: 定義 HTTP Method 方法為 Get
@ApplicationPath("/rest")
public class TomcatApplication extends ResourceConfig{
public TomcatApplication(){
property(ServerProperties.WADL_FEATURE_DISABLE, "true");
property(ServerProperties.PROVIDER_PACKAGES, "com.web.tomcat.jersey,com.web.tomcat.filter,com.web.tomcat.interceptor");
}
}
// test 則是用 @Path 來設置:
@Path("/test")
public class TestRestPractice {
@GET
public String hello() {
return "hello >////<";
}
}
Jersey 常用的 Annotation
底下是常用的 Annotation:
- @ApplicationPath: 應用程式層級路徑
- @PATH: 定義路徑
- @GET, @POST, @PUT, @DELETE: http 方法
- @Consumes, @Produces: 定義 MIME
- @PathParam: http://localhost:你開的洞 XD/專案名稱/rest/test/我是 PathParam
- @FormParam: 可以用 REST client 製造一個 Form 的 POST
- @QueryParam: http://localhost:你開的洞 XD/專案名稱/rest/test?我是 QueryParam = 我的值
- @DefaultValue: 也可以修飾 POJO 裡的變數
- @CookieParam
- @HeaderParam
進階款:
- @Provider
- @Priority: 利用
@Priority
來標註優先權 - @PreMatching
- @NameBinding
- @RolesAllowed
用在其他 Annotation 上的 Annotation
@Retention
確定標註的生命周期, 用一個 Enum 的 RetentionPolicy 參數設定@Documented
文檔化@Target
表示標註適用的範圍,也用 Enum 的 EnumType 的參數設定@SessionScope
@Inject
針對資料的標註,@JsonProperty 改變 JSON 名稱
Jersey 資料傳輸格式 (@Consumes、@Produces)
Jersey 支援直接將 POJO 轉成 XML (jaxb) 及 JSON (jackson) 的功能,只需要使用兩種 @ 設定即可。
- @Consumes:
@Consumes("text/plain")
- @Produces:
@Produces({"application/xml", "application/json"})
@POST
@Consumes("text/plain")
public void postClichedMessage(String message) {
}
@GET
@Produces({"application/xml", "application/json"})
public String doGetAsXmlOrJson() {
}
Jersey API 權限管理
一樣是透過 Annotation 針對 API 做權限上的管理
- @PermitAll
- @RolesAllowed
底下範例針對同個路徑讓一般使用者、管理員取得不同的資料
@Path("/")
@PermitAll
public class Resource {
@RolesAllowed("user")
@GET
public String get() { return "GET"; }
@RolesAllowed("admin")
@POST
public String post(String content) { return content; }
@Path("sub")
public SubResource getSubResource() {
return new SubResource();
}
}
Jersey Filters & Interceptors
Filters & Interceptors 用途是在 API 的資料流中進行把關和處理
- Filter 可以針對傳入的 request 或即將送出的 response 的參數操作,像是 header 判斷、HTTP Method 修改等等,自訂 Server Filters 需要 override 以下兩個方法:
- ContainerRequestFilter
- ContainerResponseFilter
- Interceptors 攔截器可以針對 input/output 串流做處理,常見的像是壓縮
- WriterInterceptor
- ReaderInterceptor
當一個 Client 發出請求後,執行的粗略順序如下:
- ClientRequestFilters
- ReaderInterceptor
- WriterInterceptor
- ClientResponseFilters
@Pre-matching 範例
提前攔截並更改方法,如未加標註則預設都為 Post-matching 僅做判斷後篩選
@PreMatching
public class PreMatchingFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext)
throws IOException {
// change all PUT methods to POST
if (requestContext.getMethod().equals("PUT")) {
requestContext.setMethod("POST");
}
}
}
NameBinding 和 Dynamic Binding
加上 @Provider
則為預設啟動,若需建立 filter 及 interceptors 與特定 RESTful Services 之間的關連,會利用 @NameBinding
再加上自定標註,或是使用 Dynamic Binding。
NameBinding 範例
@NameBinding
@Retention(value =RetentionPolicy.RUNTIME)
public @interface Test {
}
--------------------------------------------------------
@Provider
@Test
public class TestFilter2 implements ContainerRequestFilter,
ContainerResponseFilter {
//
}
--------------------------------------------------------
@Test
@GET
public String hello() {
return "hello >////<";
}
Dynamic Binding 範例
- 包在 my.package.admin 裡的
@Provider
public class MyDynamicFeature implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
String resourcePackage = resourceInfo.getResourceClass().getPackage().getName();
Method resourceMethod = resourceInfo.getResourceMethod();
if ("my.package.admin".equals(resourcePackage)
&& resourceMethod.getAnnotation(GET.class) != null) {
context.register(LoggingFilter.class);
}
}
}
- HelloWorldResource.class 裡的
public class CompressionDynamicBinding implements DynamicFeature {
@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
if (HelloWorldResource.class.equals(resourceInfo.getResourceClass())
&& resourceInfo.getResourceMethod()
.getName().contains("VeryLongString")) {
context.register(GZIPWriterInterceptor.class);
}
}
}
JAX-RS Response & Error Message
Return Response 可以設定 status
.status(Response.Status.OK)
//200.status(Response.Status.CREATED)
// 201.status(Response.Status.NO_CONTENT)
// 204- the resource method’s return type is void
- the value of the returned entity is null
.seeOther()
//303.notModified()
//304.temporaryRedirect()
//307.status(Response.Status.FORBIDDEN)
//403.status(Response.Status.CONFLICT)
//409- 取到後可以判斷 Response response = request.get();
- Assert.assertTrue(response.getStatus() == 200);
Maven 設定
Maven 透過 Project Object Model 的設置,也就是 pom.xml 會定義以下這些東西:
- 版本相關資訊
<modelVersion>4.0.0</modelVersion>
<groupId>test</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
- 定義 Project 的 layout
<sourceDirectory>src</sourceDirectory>
<resources>
<resource>
<directory>src</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>properties</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
- 定義相關 dependencies
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
<version>2.16</version>
</dependency>
</dependencies>
加入 dependencies 後會自動產生 jar, war 相關的檔案,少去自己加參考的過程,但需要了解 dependencies 是針對什麼而加,像是 POJO to JSON 需要 MOXy 或是 Jackson:
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-moxy</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
除了 pom.xml 外,在 JAX-RS Application 有個 class 叫 ResourceConfig 提供了註冊資源的方式。
ApplicationPath("/rest")
public class TomcatApplication extends ResourceConfig{
public TomcatApplication() {
property(ServerProperties.WADL_FEATURE_DISABLE, "true");
property(ServerProperties.PROVIDER_PACKAGES,
"com.web.tomcat.jersey,com.web.tomcat.filter,com.web.tomcat.interceptor");
register(XXX.class);
}
}
final Application application = new ResourceConfig()
.packages("org.glassfish.jersey.examples.linking")
.register(DeclarativeLinkingFeature.class);
舉例來說要利用 Jersey 上下傳檔案,可能就需要額外註冊以下的資訊:
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
</dependency>
要記得到 ResourceConfig 註冊
packages("com.rest.resource");
register(MultiPartFeature.class);
Log4J
了解了 Jersey 的相關設定後,使用 Log4J 可以大大幫助我們在開發上的除錯。
- POM 檔設定相關相依性
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.4</version>
</dependency>
- import 相關 Library
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class HelloWorld {
private static final Logger logger = LogManager.getLogger("HelloWorld");
public static void main(String[] args) {
logger.info("Hello, World!");
}
}
- XML 設定
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<!-- Console 為 console 顯示 log 格式的設定-->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n" />
</Console>
<!-- DailyFile 為以日為單位儲存的 log 檔設定 -->
<RollingFile name="DailyFile" fileName="logs/jarvis-log.log"
filePattern="logs/jarvis-log-%d{yyyy-MM-dd}~%i.log">
<PatternLayout>
<Pattern>[%-5level] %d{yyyy-MM-dd HH:mm:ss} [%t] %c{1} - %msg%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
<SizeBasedTriggeringPolicy size="100 MB" />
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.apache.log4j.xml" level="info" />
<Root level="debug">
<AppenderRef ref="Console" />
<AppenderRef ref="DailyFile" />
</Root>
</Loggers>
</Configuration>
喜歡這篇文章,請幫忙拍拍手喔 🤣