UTF-8 有很多字元是 BIG5 無法支援,但是有時候又需要使用 BIG5,就需要特別轉成 HTML Entities。
mb_substitute_character('entity');
echo mb_convert_encoding('許功蓋開飛機♥⬅️', 'BIG5', 'UTF-8');
// 許功蓋開飛機♥⬅️
UTF-8 有很多字元是 BIG5 無法支援,但是有時候又需要使用 BIG5,就需要特別轉成 HTML Entities。
mb_substitute_character('entity');
echo mb_convert_encoding('許功蓋開飛機♥⬅️', 'BIG5', 'UTF-8');
// 許功蓋開飛機♥⬅️
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。
參考資料:
參數要檢查 not null 有幾個做法:
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。
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)
}
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)
}
但是這不直覺。
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
參考資料:
如果 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());
}
}
參考資料:
常常會需要使用 @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(); } }
參考資料:
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();
}
}
參考資料:
使用 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
}
參考資料:
最近系統需要使用 Netty4,所以把衝突的 Netty3 拆掉,然後就出現了例外。 pom.xml <dependency> <groupId>com.ning</groupId> <artifactId>as...