顯示具有 jersey 標籤的文章。 顯示所有文章
顯示具有 jersey 標籤的文章。 顯示所有文章

2020年10月11日 星期日

[Java] Invalid HTTP method: PATCH

最近系統需要使用 Netty4,所以把衝突的 Netty3 拆掉,然後就出現了例外。

pom.xml

<dependency>
   <groupId>com.ning</groupId>
   <artifactId>async-http-client</artifactId>
   <version>1.9.40</version>
   <exclusions>
      <exclusion>
         <groupId>io.netty</groupId>
         <artifactId>netty</artifactId>
      </exclusion>
   </exclusions>
</dependency>

client

Request request = new RequestBuilder("POST")
      .setUrl(url)
      .setBody(body)
      .setHeader("Content-Type", contentType)
      .setRequestTimeout(timeout)
      .build();
Future&lg4;Response> future = client.executeRequest(request);

console log

[AsyncHttpClient-Callback] DEBUG c.n.h.c.p.jdk.JDKAsyncHttpProvider - Invalid HTTP method: PATCH
java.net.ProtocolException: Invalid HTTP method: PATCH
        at java.net.HttpURLConnection.setRequestMethod(HttpURLConnection.java:440)
        at sun.net.www.protocol.http.HttpURLConnection.setRequestMethod(HttpURLConnection.java:553)
        at sun.net.www.protocol.https.HttpsURLConnectionImpl.setRequestMethod(HttpsURLConnectionImpl.java:388)
        at com.ning.http.client.providers.jdk.JDKAsyncHttpProvider$AsyncHttpUrlConnection.configure(JDKAsyncHttpProvider.java:530)
        at com.ning.http.client.providers.jdk.JDKAsyncHttpProvider$AsyncHttpUrlConnection.call(JDKAsyncHttpProvider.java:227)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at java.lang.Thread.run(Thread.java:748)

可以看到 com.ning:async-http-client 原本是走 Netty3,拿掉之後改走 HttpURLConnection 才是出現例外的主因。 找到的解法有幾個:

  • 使用 head X-HTTP-Method-Override:測試不可行。
  • 複寫 HttpURLConnection.methods:測試不可行,payload 帶不過去。
  • 尋找已解決的 JDK (待確認)
  • 改用新版 org.asynchttpclient:async-http-client;因為底層用的是 Netty4。
  • 改用其他 http client。

雖然 Jersey 也會發生相同問題,但是修改之後可以正常使用。

webTarget.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
Response response = webTarget.path(url)
      .request(MediaType.APPLICATION_JSON)
      .method("PATCH", Entity.json(payload.toString()));

參考資料:

2018年8月23日 星期四

[Java] test Jersey APIs

如果 APIs 裡面用了大量過濾器、constraint,一般的單元測試很難一併測試,Jersey 提供了測試框架就可以很容易一起測試。 其實就是跑一個伺服器起來去呼叫 API。

// build.gradle
dependencies {
    testCompile (
        'org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-grizzly2:2.27'
    )
}

因為我用 Junit5,所以需要稍微調整一下。

import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.TestProperties;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ResourceTest extends JerseyTest {

    @Override
    protected Application configure() {
        enable(TestProperties.DUMP_ENTITY);
        enable(TestProperties.LOG_TRAFFIC);
        return new ResourceConfig(HelloResource.class);
    }

    @Override
    @BeforeEach
    public void setUp() throws Exception {
        // patch junit4
        super.setUp();
    }

    @Override
    @AfterEach
    public void tearDown() throws Exception {
        // patch junit4
        super.tearDown();
    }

    @Test
    void test() {
        Response response = target().path("/hello").request().get(); 
        Assertions.assertEquals(200, response.getStatus());
    }
}

參考資料:

2018年8月22日 星期三

[Java] Jersey 使用 javax.validation.constraints.*

常常會需要使用 @NotNull、@NotBlank,但是不會運作,參考官網之後的做法:

// build.gradle
dependencies {
    compile (
        'org.glassfish.jersey.ext:jersey-bean-validation:2.27',
        'org.hibernate:hibernate-validator:6.0.12.Final',
        'javax.el:javax.el-api:3.0.0',
    )
}

設定回傳訊息。

    @GET
    @Path("/hello")
    public Response getMessage(@NotBlank(message = "empty name") @QueryParam("name") String name) { ... }

開啟變數,改成自己處理錯誤,不使用 Servlet server 回傳訊息。

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;

@ApplicationPath("/")
public class WebApplication extends ResourceConfig {

    public WebApplication() {
        ...
        // ValidationExceptionMapper catchable
        property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
    }
}

import java.util.List;
import java.util.stream.Collectors;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class ValidationExceptionMapper implements ExceptionMapper {

    @Override
    public Response toResponse(ValidationException e) {
        List msgs = null;
        if (e instanceof ConstraintViolationException) {
            List msgs = ((ConstraintViolationException) e).getConstraintViolations()
                    .stream()
                    .map(ConstraintViolation::getMessage)
                    .collect(Collectors.toList());
        } else {
            ...
        }

        return Response.status(Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON).entity(msgs).build();
    }

}

參考資料:

2018年8月2日 星期四

[Java] test Jersey injection

參考 StackOverflow:

// src/test/java/example/HellpResourceTest.java
package example;

import javax.inject.Inject;
import javax.ws.rs.core.Response;

import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.ServiceLocatorUtilities;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import example.HelloResource;

class HelloResourceTest {
    @Inject
    private HelloResource resource;
    private ServiceLocator locator;

    @BeforeEach
    void setUp() {
        Binder resourceBinder = new AbstractBinder() {
            @Override
            public void configure() {
                // bind resource
                bindAsContract(HelloResource.class);
                // bind mock repository
                bind(new MockHelloRepository()).to(HelloRepository.class);
            }
        };
        locator = ServiceLocatorUtilities.bind(repositoryBinder, resourceBinder);
        locator.inject(this);
    }

    @AfterEach
    void tearDown() {
        if (locator != null) {
            locator.shutdown();
        }
    }

    @Test
    @DisplayName("Test Hello API")
    void doTest() {
        Assertions.assertNotNull(resource);
        Response response = resource.getHello("World");
        System.out.println(response);
        Assertions.assertEquals("Hello World", response);
    }

}

用到 org.glassfish.hk2:hk2-locator:2.5.0, org.glassfish.hk2:hk2-utils:2.5.0

參考資料:

2018年7月25日 星期三

[Java] Swagger3 + JAX-RS Jersey

首先加入依賴包 swagger-jersey2-jaxrs

// build.gradle
dependencies {
    compile 'io.swagger:swagger-jersey2-jaxrs:1.5.20'
}

然後註冊 swagger,

// src/main/java/demo/WebApplication.java
package demo;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class WebApplication extends ResourceConfig {
    public WebApplication() {
        packages("<MY-PACKAGE>"); // "demo"

        register(io.swagger.jaxrs.listing.ApiListingResource.class);
        register(io.swagger.jaxrs.listing.SwaggerSerializers.class);
    }
}

接著寫 swagger annotation,

// src/main/java/demo/Hello.java
package demo;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(value="Hello World API")
@Path("/hello")
public class Hello {
    @ApiOperation(value = "say hello", response = String.class)
    @GET()
    public String hello() {
        return "hello";
    }
}

然後打開 url: http://localhost:8080/<PROJECT>/hello/swagger.json 確認是否正常。

沒問題之後,下載 Swagger UI dist 檔案到 src/main/webapp/static/, 將 index.html 裡面的 url https://petstore.swagger.io/v2/swagger.json 修改成剛剛的 http://localhost:8080/<PROJECT>/hello/swagger.json。

並且修改 web.xml 使可以讀取 Swagger UI,

// src/main/webapp/WEB-INF/web.xml
<web-app ...>
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/static/*</url-pattern>
    </servlet-mapping>
</web-app>
然後就可以使用 http://localhost:8080/<PROJECT>/static/index.html。

參考資料:

2019/07/12 更新:
附註:StackOverflow: What exactly is the ResourceConfig class in Jersey 2?

2018年7月24日 星期二

[Java] JAX-RS on AWS Lambda

AWS Lambda 有提供 proxy framework 掛上 jersey。 首先加上依賴包,

// build.gradle
dependencies {
    compile (
        'com.amazonaws.serverless:aws-serverless-java-container-jersey:1.1.3',
        'org.glassfish.jersey.media:jersey-media-json-jackson:2.27',
        'org.glassfish.jersey.inject:jersey-hk2:2.27'
    )
}

然後接口要換掉 (RequestHandler -> RequestStreamHandler),

package example;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;

import com.amazonaws.serverless.proxy.jersey.JerseyLambdaContainerHandler;
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;

public final class ProxyWithStream implements RequestStreamHandler {

    private static final ResourceConfig jerseyApplication = new ResourceConfig()
            .packages("<PACKAGE-NAME>")
            .register(JacksonFeature.class);

    private static final JerseyLambdaContainerHandler handler = JerseyLambdaContainerHandler
            .getAwsProxyHandler(jerseyApplication);

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        handler.stripBasePath("<API-GATEWAY-PATH>");
        handler.proxyStream(inputStream, outputStream, context);
        outputStream.close();
    }

}
其中 <PACKAGE-NAME> 要輸入 resource 路徑, <API-GATEWAY-PATH> 則是端視在 API Gateway 的設定。 假設 API Gateway 設定 "/v1/foo/{proxy+}", JAX-RS 設定 APIs 為 "/bar/{id}", <API-GATEWAY-PATH> 就要設定為 "/v1/foo"。

接下來 Lambda 測試的 payload 需要更改成 API Gateway 傳給 Lambda 的格式,然後就可以測試了。

{
  "body": "",  // POST/PUT/PATCH body 的來源
  "resource": "/{proxy+}",
  "requestContext": {
    "resourceId": "123456",
    "apiId": "1234567890",
    "resourcePath": "/{proxy+}",
    "httpMethod": "GET",
    "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
    "accountId": "123456789012",
    "identity": {
      "apiKey": null,
      "userArn": null,
      "cognitoAuthenticationType": null,
      "caller": null,
      "userAgent": "Custom User Agent String",
      "user": null,
      "cognitoIdentityPoolId": null,
      "cognitoIdentityId": null,
      "cognitoAuthenticationProvider": null,
      "sourceIp": "127.0.0.1",
      "accountId": null
    },
    "stage": "prod"
  },
  "queryStringParameters": {
    "fields": "name"  // query parameters 的來源
  },
  "headers": {
    "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
    "Accept-Language": "en-US,en;q=0.8",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
    "User-Agent": "Custom User Agent String",
    "Accept-Encoding": "gzip, deflate, sdch"
  },
  "pathParameters": {
    "proxy": "bar/12345"
  },
  "httpMethod": "GET",  // method 的來源
  "stageVariables": {},
  "path": "/v1/foo/bar/12345"  // url mapping 的來源
}

參考資料:

系列:

2018年5月8日 星期二

[Java] running JAX-RS with gretty

開一個專案叫 jersey,然後按下面加入代碼:

// build.gradle
plugins {
    id 'war'
    //id 'org.gretty' version '2.1.0'
    id 'org.akhikhl.gretty' version '2.0.0'
}
// 'jetty7', 'jetty8', 'jetty9', 'jetty9.3', 'jetty9.4', 'tomcat7', 'tomcat8'
//gretty.servletContainer='jetty9'
repositories {
    jcenter()
}
dependencies {
    compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.26'
    compile 'org.glassfish.jersey.inject:jersey-hk2:2.27'
}
// src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
   id="WebApp_ID"
   version="3.0">
   <display-name>My JAX-RS</display-name>
   <servlet>
      <servlet-name>jersey-servlet</servlet-name>
      <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class>
      <init-param>
         <param-name>jersey.config.server.provider.packages</param-name>
         <param-value>demo</param-value>
      </init-param>
   </servlet>
   <servlet-mapping>
      <servlet-name>jersey-servlet</servlet-name>
      <url-pattern>/*</url-pattern>
   </servlet-mapping>
</web-app>
// src/main/java/demo/Hello.java
package demo;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Response;

@Path("/hello")
public class Hello {
    @GET()
    public String hello() {
        return "hello";
    }

    @GET
    @Path("/{name}")
    public Response getMessage(@PathParam("name") String name) {
        String outMsg = "Hello " + name + "!";
        return Response.status(200).entity(outMsg).build();
    }
}

接下來執行 gradle appRun

$ gradle appRun
:prepareInplaceWebAppFolder UP-TO-DATE
:createInplaceWebAppFolder UP-TO-DATE
:compileJava
:processResources NO-SOURCE
:classes
:prepareInplaceWebAppClasses
:prepareInplaceWebApp
:appRun
19:46:58 INFO  Jetty 9.2.24.v20180105 started and listening on port 8080
19:46:58 INFO  JAX-RS Application Demo runs at:
19:46:58 INFO    http://localhost:8080/jersey

> Task :appRun
Press any key to stop the server.
<===========--> 87% EXECUTING [17s]
> :appRun

然後就可以測試了:

  • http://localhost:8080/jersey/hello
  • http://localhost:8080/jersey/hello/John

備註:

  • web.xml 的 "param-value" 必須跟套件名一樣,jersey 才會作用。
  • 用 gretty 試跑的時候,URL 是帶上專案名稱,像這裡是 "/jersey"。放在 Servlet 上面跑的時候則是看檔案名稱,例如 "jersey.war";如果不想要這個路徑,可以改成 "ROOT.war"。
  • web.xml 預設路徑是 src/main/webapp/WEB-INF,但是可以在 Gradle 的 war plugin 設定。

備註2:Servlet 3 之後可以不寫 web.xml,改成繼承 ResourceConfig。

// src/main/java/demo/MyApplication.java
package demo;

import javax.ws.rs.ApplicationPath;
import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
    public MyApplication() {
        packages("demo");
    }
}

備註3:如果 gretty 發生 Java9 相容問題,

Caused by: java.lang.RuntimeException: Error scanning entry META-INF/versions/9/module-info.class from jar file: .gradle/caches/modules-2/files-2.1/...

Caused by: java.lang.RuntimeException: Error scanning entry module-info.class from jar file: .gradle/caches/modules-2/files-2.1/...
可以設定參數 gretty.servletContainer 為 "jetty9.4" 或是 "tomcat8" 測試。

參考資料:

[Java] Invalid HTTP method: PATCH

最近系統需要使用 Netty4,所以把衝突的 Netty3 拆掉,然後就出現了例外。 pom.xml <dependency> <groupId>com.ning</groupId> <artifactId>as...