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()));

參考資料:

2020年9月30日 星期三

[Java] hot reload Yaml

高可用條件之一是能夠 hot reload (熱重載)。簡單的做法是每次使用的時候就是讀檔,使用 Properties 或是 snakeyaml。另一種方法是使用 commons-configuration2 套件提供的功能:

public static ReloadingFileBasedConfigurationBuilder<YAMLConfiguration> getBuilder(File file) {
   Parameters params = new Parameters();
   ReloadingFileBasedConfigurationBuilder<YAMLConfiguration> builder =
      new ReloadingFileBasedConfigurationBuilder<>(YAMLConfiguration.class)
         .configure(params.fileBased().setFile(file));

   // 每次讀取時檢查
   builder.addEventListener(ConfigurationBuilderEvent.CONFIGURATION_REQUEST,
      event -> builder.getReloadingController().checkForReloading(null));

   // 每分鐘檢查
   PeriodicReloadingTrigger trigger = new PeriodicReloadingTrigger(builder.getReloadingController(),
      null, 1, TimeUnit.MINUTES);
   trigger.start();

   return builder;
}

@Test
public void testHotReload() throws Exception {
   File tmpFile = File.createTempFile("test", ".yml");
   tmpFile.deleteOnExit();

   String raw1 = "sample:\n    property: test-1\n";
   Files.write(tmpFile.toPath(), raw1.getBytes());

   ImmutableConfiguration config1 = getBuilder(tmpFile).getConfiguration();
   Assert.assertEquals("test before hot reload", "test-1", config1.getString("sample.property"));

   // NOTE. File#setLastModified() only guarantees that the file modification time is accurate to the second, so
   // the modification must be delayed by 1 second.
   Thread.sleep(1000);
   String raw2 = "sample:\n    property: test-2\n";
   Files.write(tmpFile.toPath(), raw2.getBytes());

   ImmutableConfiguration config2 = getBuilder(tmpFile).getConfiguration();
   Assert.assertEquals("test after hot reload", "test-2", config2.getString("sample.property"));
}

參考資料:

2020年2月13日 星期四

[PHP] 網頁顯示 UTF-8 轉 BIG5

UTF-8 有很多字元是 BIG5 無法支援,但是有時候又需要使用 BIG5,就需要特別轉成 HTML Entities。

mb_substitute_character('entity');
echo mb_convert_encoding('許功蓋開飛機♥⬅️', 'BIG5', 'UTF-8');
// 許功蓋開飛機♥⬅️

2019年9月10日 星期二

[Java] Jackson binding Date , Instant

Jackson 可以對 Date 綁定,缺點是 Json 用了毫秒。由於一般儲存到資料庫都是用 Unix 時間戳,計算單位是秒,這時候其實不適用。

// Foo.java
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
@Data
class Foo {
   @JsonProperty("create_time")
   private Date createTime;

   public static void main(String[] args) throws Throwable {
      Foo foo = new Foo();
      foo.setCreateTime(new Date());

      ObjectMapper mapper = new ObjectMapper();
      String s = mapper.writeValueAsString(foo); // {"create_time":1568024055895}

      Foo foo2 = mapper.readValue(s, Foo.class); // Foo(createTime=Mon Sep 09 18:14:15 CST 2019)
      foo.equals(foo2); // true
   }
}

所以需要自己寫轉換類。

// Foo.java
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@Data
class Foo {
    @JsonProperty("create_time")
    @JsonDeserialize(using = DefaultDateDeserializer.class)
    @JsonSerialize(using = DefaultDateSerializer.class)
    private Date createTime;
}

// DefaultDateDeserializer.java
import java.io.IOException;
import java.util.Date;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
class DefaultDateDeserializer extends JsonDeserializer {
    @Override
    public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return new Date(p.getLongValue() * 1000);
    }
}

// DefaultDateSerializer.java
import java.io.IOException;
import java.util.Date;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
class DefaultDateSerializer extends JsonSerializer {
    @Override
    public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeNumber(value.toInstant().getEpochSecond());
    }
}

這時候的結果如下,會發現 Json 是我們要的,但是最後是 false。 原因是創建 Date 實例含有毫秒,不過轉到 Json 的時候我們去掉了毫秒; 從 Json 轉回 Date 則損失了這些毫秒,所以兩者才會不相等。

{"create_time":1568024055}
Foo(createTime=Mon Sep 09 18:14:15 CST 2019)
false

那麼接著要把 Date 改成 Instant,因為 Date 很多方法都棄用了,Instant 有很多方法比較好用,而且轉成 ISO8601 或是 epoch timestamp 比較方便。

// build.gradle
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.9"

// Foo.java
import java.time.Instant;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@Data
class Foo {
    @JsonProperty("create_time")
    @JsonDeserialize(using = DefaultInstantDeserializer.class)
    @JsonSerialize(using = DefaultInstantSerializer.class)
    private Instant createTime;

   public static void main(String[] args) throws Throwable {
      Foo foo = new Foo();
      foo.setCreateTime(Instant.now());

      ObjectMapper mapper = new ObjectMapper();
      String s = mapper.writeValueAsString(foo); // {"create_time":1568024055}

      Foo foo2 = mapper.readValue(s, Foo.class); // Foo(createTime=2019-09-09T10:14:15Z)
      foo.equals(foo2); // true
   }
}

// DefaultInstantDeserializer.java
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
@SuppressWarnings("serial")
class DefaultInstantDeserializer extends InstantDeserializer {
    public DefaultInstantDeserializer() {
        super(Instant.class,
                DateTimeFormatter.ISO_INSTANT,
                Instant::from,
                a -> Instant.ofEpochMilli(a.value),
                a -> Instant.ofEpochSecond(a.integer, a.fraction),
                null,
                true
        );
    }

    @Override
    public Instant deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (Objects.isNull(parser.getText()) || parser.getValueAsLong() == 0) {
            return null;
        }
        return super.deserialize(parser, context);
    }
}

// DefaultInstantSerializer.java
import java.io.IOException;
import java.time.Instant;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
class DefaultInstantSerializer extends InstantDeserializer {
    @Override
    public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeNumber(value.getEpochSecond());
    }
}

DefaultInstantSerializer 繼承 InstantDeserializer 是因為這樣可以接受多種格式,例如 1568024055.000000000。

參考資料:

2019年7月4日 星期四

[Java] 檢查參數 not null

參數要檢查 not null 有幾個做法:

  1. Java 1.4 的 Assertions
  2. void assert4(Object o) {
       assert o != null;
       // Exception in thread "main" java.lang.AssertionError
    
       assert o != null : "o is null";
       // Exception in thread "main" java.lang.AssertionError: o is null
    }
    

    但是 JVM 預設關閉,所以要記得帶參數 -enableassertions

  3. Java 7 的 Objects#requireNonNull()
  4. void java7(Object o) {
       Objects.requireNonNull(o);
       // Exception in thread "main" java.lang.NullPointerException
       //         at java.util.Objects.requireNonNull(Objects.java)
    
       Objects.requireNonNull(o, "o is null");
       // Exception in thread "main" java.lang.NullPointerException: o is null
       //         at java.util.Objects.requireNonNull(Objects.java)
    }
    
  5. Java 8 的 Optional#of()
  6. void java8(Object o) {
       Optional.of(o);
       // Exception in thread "main" java.lang.NullPointerException
       //         at java.util.Objects.requireNonNull(Objects.java)
       //         at java.util.Optional.(Optional.java)
       //         at java.util.Optional.of(Optional.java)
    }
    

    但是這不直覺。

  7. Lombok 的 @NonNull
  8. void lombok(@lombok.NonNull Object o) {
       // Exception in thread "main" java.lang.NullPointerException: o is marked @NonNull but is null
    }
    

個人偏好:Lombok > Java Assertions > Objects#requireNonNull() > Optional#of()

實務上:Lombok = Objects#requireNonNull() > Optional#of() > Java Assertions

參考資料:

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月19日 星期日

[Java] use JavaDoc to generate Swagger resource

Swagger 寫在注解讓整個代碼變得很複雜,所以找到有人寫在 JavaDoc 的方式,首先 Gradle 的設定如下:

// build.gradle
apply plugin: 'java'

configurations {
    doclet
}

dependencies {
    doclet (
        'com.tenxerconsulting:swagger-doclet:1.1.3',
        'javax.ws.rs:javax.ws.rs-api:2.1'
    )
}

task generateRestApiDocs(type: Javadoc) {
    source = sourceSets.main.allJava
    destinationDir = reporting.file('rest-api-docs')
    options.classpath = configurations.doclet.files.asType(List)
    options.docletpath = configurations.doclet.files.asType(List)
    options.doclet = 'com.tenxerconsulting.swagger.doclet.ServiceDoclet'
    options.addStringOption('apiVersion', '1')
    options.addStringOption('docBasePath', "/$project.name/static/swagger-ui")
    options.addStringOption('apiBasePath', "/$project.name")
    options.addBooleanOption('skipUiFiles', false)
}

public final class Hello {

    /**
     * Get user toggle.
     *
     * @param name your name
     * @return http response
     * @requiredParams name
     * @paramsDefaultValue name world
     * @responseType String
     * @responseMessage 200 ok `String
     * @responseMessage 500 internal error `javax.ws.rs.WebApplicationException
     */
    @GET
    @Path("/{name}")
    public Response getHello(@PathParam("name") String name) {
        return Response.Ok("Hello " + name).build();
    }
}

參考資料:

[Java] JavaDoc cannot find Lombok symbols

使用 Lombok 之後,要產生 JavaDoc 會出現以下的警告訊息:

...
Foo.java:3: error: cannot find symbol
@Log4j2
 ^
  symbol: class Log4j2
javadoc: warning - Class Data not found.
javadoc: warning - Class AllArgsConstructor not found.
javadoc: warning - Class Getter not found.
javadoc: warning - Class Log4j2 not found.

一個簡單的解決方法就是把 Lombok 標記解譯,

// build.gradle
apply plugin: 'io.franzbecker.gradle-lombok' version '1.8'

import io.franzbecker.gradle.lombok.task.DelombokTask
task delombok(type: DelombokTask) {
    ext.outputDir = file("$buildDir/delombok")
    outputs.dir(outputDir)
    sourceSets.main.java.srcDirs.each {
        inputs.dir(it)
        args(it, "-d", outputDir)
    }
}

javadoc {
    dependsOn delombok
    source = delombok.outputDir
    failOnError = false
}

參考資料:

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

參考資料:

[Java] JUnit with DynamoDBLocal

先設定 build.gradle:

// build.gradle
repositories {
    maven {
        // dynamodb local url
        url 'https://s3-us-west-2.amazonaws.com/dynamodb-local/release'
    }

    testCompile (
        'org.junit.jupiter:junit-jupiter-api:5.2.0',
        'com.amazonaws:DynamoDBLocal:1.11.119'
    )

    testRuntime (
        // junit5
        'org.junit.jupiter:junit-jupiter-engine:5.2.0',
        // dynamodb local
        'com.almworks.sqlite4java:libsqlite4java-linux-i386:1.0.392',
        'com.almworks.sqlite4java:libsqlite4java-osx:1.0.392'
    )
}
// link native sqlite4java
task copyNativeDeps(type: Copy) {
    from (configurations.testCompile) {
        include '*.dylib'
        include '*.so'
        include '*.dll'
    }
    into 'build/libs'
}

test {
    useJUnitPlatform()
    dependsOn copyNativeDeps
    doFirst {
        // setup sqlite4java path
        systemProperty 'java.library.path', 'build/libs'
    }
    testLogging {
        showStandardStreams = true
    }
}

接下來就是測試:

package example;

import java.util.ArrayList;
import java.util.List;

import org.junit.jupiter.api.Test;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
import com.amazonaws.services.dynamodbv2.model.KeyType;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;

public class DynamoDBLocalTest {

    @Test
    public void createTable() throws Exception {
        AmazonDynamoDB client = DynamoDBEmbedded.create().amazonDynamoDB();
        DynamoDB dynamodb = new DynamoDB(client);
        String tableName = "Employee";

        List keySchema = new ArrayList();
        keySchema.add(new KeySchemaElement().withAttributeName("Name").withKeyType(KeyType.HASH));

        List attributeDefinitions = new ArrayList();
        attributeDefinitions.add(new AttributeDefinition().withAttributeName("Name").withAttributeType("S"));

        CreateTableRequest request = new CreateTableRequest().withTableName(tableName).withKeySchema(keySchema)
                .withProvisionedThroughput(
                        new ProvisionedThroughput().withReadCapacityUnits(1L).withWriteCapacityUnits(1L))
                .withAttributeDefinitions(attributeDefinitions);

        System.out.println("Issuing CreateTable request for " + tableName);
        Table table = dynamodb.createTable(request);
        System.out.println("Waiting for " + tableName + " to be created...this may take a while...");
        table.waitForActive();

        System.out.println(dynamodb.getTable(tableName).describe());
    }

}

參考資料:

系列:

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年7月22日 星期日

AWS Lambda + API Gateway

承上文 [Java] AWS Lambda 入門, 這次要在前面掛上 API Gateway。

首先設定好 API Gateway 串到之前的 Lambda,測試的時候會出現 Status 502 的狀況,

Execution failed due to configuration error: Malformed Lambda proxy response
主要原因是 Lambda 回傳給 API Gateway 有特定格式,以下用 Node 當作範例:
exports.handler = (event, context, callback) => {
    var response = {
        "statusCode": 200, //可略,默認為200
        "headers": { //可略
            "my_header": "my_value"
        },
        "body": JSON.stringify(event),
        "isBase64Encoded": false //可略,默認為非
    };
    callback(null, response);    
};
也就是說需要回傳 Json 物件,同時把資料放在 "body" 裡面,API Gateway 才會顯示正常。 在 Lambda 上測試的結果要像這樣:
{
  "statusCode": 200,
  "headers": {
    "my_header": "my_value"
  },
  "body": "{\"key3\":\"value3\",\"key2\":\"value2\",\"key1\":\"value1\"}",
  "isBase64Encoded": false
}
Java 的回傳格式也必須相同。

參考資料:

系列:

[Java] AWS Lambda + DynamoDB

承上文 [Java] AWS Lambda 入門, 這次添加存取 DynamoDB。

首先建立一個 DynamoDB 表單 "Employee",同時建立一筆資料。

接下來進入 IAM 建立一個 Role,這裡直接選擇 "AWS service", 在 "Choose the service that will use this role" 選擇 "Lambda", 然後就可以點擊按鈕 "Next: Permissions";
在 "Attach permissions policies" 裡面, 勾選 "AmazonDynamoDBFullAccess","AWSLambdaDynamoDBExecutionRole", 然後點擊按鈕 "Next: Review";
輸入 Role name,就可以建立一個 Role 了。

接下來回到之前建立的 Lambda,往下拉到 "Execution role",選取剛剛建立的 Role 就成功了。

可以參考官方文件用 node 簡單測試一下,

// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set the region 
AWS.config.update({region: 'ap-northeast-1'});

exports.handler = (event, context, callback) => {
  // Create the DynamoDB service object
  ddb = new AWS.DynamoDB({apiVersion: '2012-10-08'});

  var params = {
    TableName: 'Employee',
    Key: {
      'EmpId' : {S: '1000'}
    }
    //, ProjectionExpression: 'EmpId'
  };

  // Call DynamoDB to read the item from the table
  ddb.getItem(params, function(err, data) {
    if (err) {
      console.log("Error", err);
    } else {
      console.log("Success", data.Item);
      callback(null, data);
    }
  });
};

回到之前的 Java Project,修改一下:

package example;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.document.DynamoDB;
import com.amazonaws.services.dynamodbv2.document.Item;
import com.amazonaws.services.dynamodbv2.document.Table;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Hello implements RequestHandler {
    @Override
    public String handleRequest(Integer input, Context context) {
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
        DynamoDB dynamoDB = new DynamoDB(client);
        Table table = dynamoDB.getTable("Employee");
        Item item = table.getItem("EmpId", "10000");
        System.out.println(item.toJSONPretty());
        return item.toJSON();
    }
}

另外 Lambda 也提供直接轉換物件的方式,我們不用再去處理 Json 映射物件的問題。

package example;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import lombok.Data;

public class Hello implements RequestHandler {
    @Override
    public String handleRequest(Integer input, Context context) {
        AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
        DynamoDBMapper dynamodbMapper = new DynamoDBMapper(client);
        Employee result = dynamodbMapper.load(Employee.class, "10000");
        System.out.println(result);
        return result.toString();
    }

    @Data
    @DynamoDBTable(tableName = "Employee")
    class Employee {
        @DynamoDBHashKey(attributeName = "EmpId")
        private String id;
        @DynamoDBAttribute(attributeName = "Name")
        private String name;
    }
}

系列:

[Java] AWS Lambda 入門

首先修改 build.gradle。

// build.gradle
apply plugin: 'java'
repositories {
    mavenCentral()
}
dependencies {
    compile (
        'com.amazonaws:aws-lambda-java-core:1.1.0',
        'com.amazonaws:aws-lambda-java-events:1.1.0'
    )
}
task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.compile.Classpath
    }
}
build.dependsOn buildZip
如果打包有發生錯誤,可以刪除 .Classpath 再重新打包嘗試。

接下來寫一隻簡單的範例。

package example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Hello implements RequestHandler<Integer, String> {
    public String myHandler(int myCount, Context context) {
        return String.valueOf(myCount);
    }
    @Override
    public String handleRequest(Integer input, Context context) {
        return "Default " + input.toString();
    }
}

打包後上傳 .jar,"Handler" 改成 "example.Hello::myHandler", 然後在 "Configure test events" 裡面將輸入值改成數字(目前預設是 Json),就可以測試了。

"Handler" 的格式是 "包.類::函數";所以上述可以改成 "example.Hello:: handleRequest" 再測試一次。 另外如果輸入格式是 Json,可以將 Integer 改成 Map<String,String>

備註:Java 預設必須調整記憶體大於 768MB 與 Timeout 5秒,否則都會出現 "Task timed out after 3.00 seconds"。 因為實際執行的日誌顯示最大使用記憶體只達 34MB,推測啟動服務加上載入 .jar 需要這麼多才可以。

參考資料:

系列:

2018年7月9日 星期一

[Eclipse] 使用 Lombok

承上文 [Android Studio] 使用 LombokLombok 是在 POJO 寫些注解,然後在編譯時期幫忙產生冗余代碼。

在 build.gradle 設置:

dependencies {
    ...
    compile 'org.projectlombok:lombok:1.18.+'
}

下載 lombok.jar 放置在 /Applications/Eclipse.app/Contents/MacOS, 在 /Applications/Eclipse.app/Contents/Eclipse/eclipse.ini 後面增加一行:
-javaagent:lombok.jar

參考資料:https://projectlombok.org/setup/android
參考資料:Install Lombok for Eclipse on Mac

Eclipse IDE for Java Developers

Version: Oxygen Release (4.7.0)
Build id: 20170620-1800

2018年5月31日 星期四

[Java] 使用另一個列表進行排序

class Foo {
    private String id;
    public Foo(String id) { this.id = id; }
    public String getId() { return id; }
    @Override
    public String toString() { return id; }
}

List<Foo> list = Arrays.asList(new Foo("1"), new Foo("2"), new Foo("3"), new Foo("4"));
List<String> keys = Arrays.asList("2","1","3","4");

// before Java 8
List<Foo> result1 = new ArrayList<>(list);
Collections.sort(result1, new Comparator() {
    @Override
    public int compare(Foo o1, Foo o2) {
        return keys.indexOf(o1.getId()) < keys.indexOf(o2.getId()) ? -1 : 1;
    }
});

// after Java 8
List<Foo> result2 = list.stream()
    .sorted(Comparator.comparing(e -> keys.indexOf(e.getId())))
    .collect(Collectors.toList());

System.out.println(result1); // [2, 1, 3, 4]
System.out.println(result2); // [2, 1, 3, 4]

2018年5月28日 星期一

[PHP] unique values & resort keys

function calc($t0, $t1)
{
    $v0 = array_sum(explode(' ', $t0));
    $v1 = array_sum(explode(' ', $t1));
    $sub = $v1 - $v0;
    echo "$v1 - $v0 = $sub\n";
    return $sub;
}

$a = [116,116,116,116,116,116,116,116,116,116,116,116,116,116,116,116,117,116,117,116,116,116,116,116,119];

$t0 = microtime();
for ($i = 0; $i < 1E6; $i++) {
    array_values(array_unique($a));
}
$t1 = microtime();
$method1 = calc($t0, $t1);     // output: 1527477436.7125 - 1527477409.434 = 27.278562068939

$t0 = microtime();
for ($i = 0; $i < 1E6; $i++) {
    array_keys(array_flip($a));
}
$t1 = microtime();
$method2 = calc($t0, $t1);     // output: 1527477439.6214 - 1527477436.7126 = 2.908814907074

$t0 = microtime();
for ($i = 0; $i < 1E6; $i++) {
    array_merge(array_flip(array_flip($a)));
}
$t1 = microtime();
$method3 = calc($t0, $t1);     // output: 1527477443.3256 - 1527477439.6215 = 3.7041912078857

有時候直覺的方式會比較慢。

參考資料:http://php.net/manual/en/function.array-unique.php#70786

PHP 5.6.36 (cli) (built: May  9 2018 20:31:47)
Copyright (c) 1997-2016 The PHP Group
Zend Engine v2.6.0, Copyright (c) 1998-2016 Zend Technologies
    with Xdebug v2.3.3, Copyright (c) 2002-2015, by Derick Rethans

2018年5月8日 星期二

[Java] find enum with Java 8 streams

import java.util.EnumSet;

public enum Status {
   PENDING("pending"),
   ACTIVE("active");

   public static final Status fromString(String status) {
      return Stream.of(Status.values)
         .filter(e -> e.status.equals(status))
         .findAny();
         .orElseThrow(() -> new IllegalArgumentException("No enum constant " + status));
   }

   private final String status;

   private Status(String status) {
      this.status = status;
   }

   @Override
   public String toString() {
      return status;
   }

}

參考資料:

[Java] Invalid HTTP method: PATCH

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