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

}

參考資料:

[Java] Invalid HTTP method: PATCH

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