2018年4月24日 星期二

Logback 的 Log 篩選設定

平常比較容易遇到的有幾種案例:

  1. 我想要 logback 輸出所有的 log,但特定某些 package 想要隱藏 DEBUG 和 TRACE
  2. 我想要 logback 只輸出 INFO 以上的 log,但特定某些 package 想要輸出所有 log
  3. 我想要 logback 只輸出 INFO 以上的 log,但特定某些 package 想要輸出所有 log,而且某個 package 的 INFO 以上 log 要輸出到別的 appender

之前開發時,大多用到的是第一種,但偶而想要第二和第三種時,花了不少時間才調整出來
最後發現一開始對設定的認知其實有點不太正確。

2018年4月11日 星期三

Spring Data JPA 的 JOIN 操作

其實在 Spring 上要做 JOIN,還挺麻煩的…
如果可以的話,我覺得能不要做就不要做比較好 XD

不過真的需要的時候,如果是需要 SELECT 時做 JOIN,推薦可以參考 [1-2] 的說明。
實務應用的話,假設以 logback 輸出到 SQL Server 的格式作為範例
想要一次取得整個 log 包含相關的 property 和 stack trace,以下的 Entity 範例是能夠套用。

logback 輸出 SQL Server 的 schema

schema 可以參考 [3],以下為 schema 的內容:

CREATE TABLE logging_event 
  ( 
    timestmp          DECIMAL(20) NOT NULL,
   	formatted_message VARCHAR(4000) NOT NULL,
    logger_name       VARCHAR(254) NOT NULL,
    level_string      VARCHAR(254) NOT NULL,
    thread_name       VARCHAR(254),
    reference_flag    SMALLINT,
    arg0              VARCHAR(254),
    arg1              VARCHAR(254),
    arg2              VARCHAR(254),
    arg3              VARCHAR(254),
    caller_filename   VARCHAR(254) NOT NULL,
    caller_class      VARCHAR(254) NOT NULL,
    caller_method     VARCHAR(254) NOT NULL,
    caller_line       CHAR(4) NOT NULL,
    event_id          DECIMAL(40) NOT NULL identity,
    PRIMARY KEY(event_id) 
  ) 

CREATE TABLE logging_event_property 
  ( 
    event_id          DECIMAL(40) NOT NULL, 
    mapped_key        VARCHAR(254) NOT NULL, 
    mapped_value      VARCHAR(1024), 
    PRIMARY KEY(event_id, mapped_key), 
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
  ) 

CREATE TABLE logging_event_exception 
  ( 
    event_id         DECIMAL(40) NOT NULL, 
    i                SMALLINT NOT NULL, 
    trace_line       VARCHAR(254) NOT NULL, 
    PRIMARY KEY(event_id, i), 
    FOREIGN KEY (event_id) REFERENCES logging_event(event_id) 
  )
Entity

假設我的 Entity 物件名字是 Activity、ActivityProperty、ActivityStackTrace,分別定義如下:

Activity

@Entity(name = "logging_event")
public class Activity implements Serializable {
  @Id
  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "timestmp")
  private Long timestmp;
  
  @Column(name = "formatted_message")
  private String message;
  
  @Column(name = "logger_name")
  private String logger;
  
  @Column(name = "level_string")
  private String level;
  
  @Column(name = "thread_name")
  private String thread;
  
  @Column(name = "reference_flag")
  private Integer reference;
  
  @Column(name = "arg0")
  private String arg0;
  
  @Column(name = "arg1")
  private String arg1;
  
  @Column(name = "arg2")
  private String arg2;
  
  @Column(name = "arg3")
  private String arg3;
  
  @Column(name = "caller_filename")
  private String callerFile;
  
  @Column(name = "caller_class")
  private String callerClass;
  
  @Column(name = "caller_method")
  private String callerMethod;
  
  @Column(name = "caller_line")
  private String callerLine;
  
  @OneToMany(
      mappedBy = "eventId",
      cascade = CascadeType.ALL,
      fetch = FetchType.EAGER)
  private List<ActivityProperty> properties;
  
  @OneToMany(cascade = CascadeType.ALL)
  @JoinColumn(name = "event_id", referencedColumnName = "event_id")
  @LazyCollection(LazyCollectionOption.FALSE)
  private List<ActivityStackTrace> stackTraces;
}

ActivityProperty

@Entity(name = "logging_event_property")
public class ActivityProperty implements Serializable {
  private static final long serialVersionUID = -6532428503805543364L;

  @Id
  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "mapped_key")
  private String key;
  
  @Column(name = "mapped_value")
  private String value;
}

ActivityStackTrace

@Entity(name = "logging_event_exception")
public class ActivityStackTrace implements Serializable {
  private static final long serialVersionUID = -50856985039937262L;

  @EmbeddedId
  private ActivityStackTraceId id;
  
  @Column(name = "trace_line")
  private String line;
}

ActivityStackTraceId

@Embeddable
public class ActivityStackTraceId implements Serializable {
  private static final long serialVersionUID = -4496556483758827756L;

  @Column(name = "event_id")
  private Long eventId;
  
  @Column(name = "i")
  private Integer i;
}

其中在 Activity 要連結 ActivityProperty 和 ActivityStackTrace 時,都是以 @OneToMany 來標注
用來表示一個 Activity 可以有多個 ActivityProperty 和 ActivityStackTrace。
此外,由於 ActivityStackTrace 並非是單一 ID,而是 Composite Primary Key 的形式
因此要利用 @Embeddable來組合。這部份可以參考 [4]。
同時,使用了 @Embeddable 之後,如果 JOIN 操作依然使用 mappedBy 的話,會出現 [5] 提到的錯誤
目前其實還沒有搞很懂這個錯誤是什麼意思,不過姑且使用 [5] 的作法,改成用 LazyCollection 可以解決。
未來如果有把問題搞懂的話,再回來追加補充了 @@a

除此之外,還得稍微注意一下,上述的 JOIN 寫法,有可能導致 [6] 描述的問題
也就是當要做資料刪除時,Hibernate 產生的 SQL Statement 會用很沒效率的方式在刪除關聯
而且某些狀況下有可能會刪除沒有預期要刪除的資料。

參考資料
  1. JPA,在@OneToMany里加入mappedBy属性
  2. hibernate基于注解的维护权反转:@OneToMany(mappedBy=)
  3. mssql.sql
  4. The best way to map a Composite Primary Key with JPA and Hibernate
  5. Hibernate cannot simultaneously fetch multiple bags
  6. Why you should avoid CascadeType.REMOVE for to-many associations and what to do instead
  7. @JoinColumn 详解

QuerySyntaxException: Path expected for join

參考資料
  1. HQL ERROR: Path expected for join

2018年3月14日 星期三

在 eclipse 使用 AutoValue

AutoValue 是個可以方便用來產生 POJO 程式碼的套件
網路上有蠻多教學文的~這裡暫時先不紀錄 XD

在 eclipse 上使用 AutoValue 時,會遇到本來應該自動產生的 AutoValue_<CLASS_NAME> 一直說找不到
而原因是在 eclipse 上預設是不會自動進行 annotation processing
因此要讓 AutoValue 可以正常在 eclipse 上運作的話,必須先在 eclipse 安裝 m2e-apt 套件 [2]。

具體來說,就是在 eclipse 上打開 Eclipse MarketPlace(Help –> Eclipse MarketPlace…),然後搜尋 m2e-apt 來安裝。
安裝完以後,在 Window –> Preferences –> Maven –> Annotation Processing 裡的最上面那個區塊
改成選擇 Automatically configure JDT APT 的選項。

之後再做一次 Maven Update 讓專案重新編譯就可以了。

參考資料
  1. Maven AutoValue Errors
  2. m2e-apt

2018年3月9日 星期五

(書籤) Java 繁體與簡體轉換

雖然繁體和簡體的轉換有一些開源專案了,但他們大多都加了一個額外的功能:詞彙代換
例如「database」臺灣稱為「資料庫」,中國則稱為「数据库」
在有些狀況,自動做這個轉換很好,但有時候其實我不想要它幫我轉換……Orz

於是就找了點資料,大體上好像都是自己實作,可能也是因為這問題難度太低,不值得做成一個開源專案…..??
不過在 CSDN 上看到有人 [1] 很好心地做了看起來比較可靠的文字比對,並整理出共 2,579 個繁體和簡體不同字的對照。

裡面有完整的程式碼,有需要的人可以直接參閱 [1]。

當然 [1] 的實作方法很土法煉鋼,依據需求是能夠做些調校的,不過那就看各自的需求自己解決吧!XD

不過需要注意的是,因為這個實作單純只看每個字而已,所以如果是繁轉簡後再簡轉繁,跑出來的結果有可能跟原始字串不同
例如「限制」轉成簡體會是「限制」,但再轉回繁體會變成「限製」。
這是因為繁體對簡體有多對一的問題,實際上得依照詞彙決定該用哪個字,才能更精準一點地轉換。

題外話,[1] 整理出來的數據蠻有意思的:

  1. 全部的漢字有 20,881 個。
  2. 繁簡不同字的有 2,579 個。
  3. 繁轉簡有多對一現象的只有 44 個。
  4. 另外有 3 個簡轉繁時有多對一現象。
參考資料
  1. Java 汉字繁体转简体

2018年3月6日 星期二

使用 Jest 搜尋 Elasticsearch

Elasticsearch 官方本來是有提供官方的 SDK 可以用來操作 Elasticsearch
不過…官方版的 SDK 在存取 Elasticsearch 時,是把自己當作 Elasticsearch Cluster 的一員來看待
因此會有包括 heartbeat 等行為在 SDK 裡自動發生。
理論上這麼做的好處是,SDK 可以看見整個 Cluster 的狀態,因此可以比較妥善地選擇存取的節點
不過代價就是維持 Elasticsearch 連線所需要的資源更多、連線速度感覺好像變慢
此外,對於跑在 AWS Lambda 的程式來說,有不少潛在的缺點。
在 Elasticsearch 官方的描述 [1] 中,有建議了可以考慮使用 Jest [2],因此就來試試它了。

2018年2月6日 星期二

Logback 輸出 Log 到 CloudWatch

本來沒有特別使用 CloudWatch 的習慣
不過應用程式放在 VM 裡,如果不控管的話,Log 總有一天會把 VM 硬碟塞爆
而如果去設定控制大小,則又會有關鍵 Log 可能被洗掉的問題。
剛好最近寫不少 AWS Lambda 的程式,覺得 Log 丟去 CloudWatch 其實也還蠻方便的 XD
就找了一下該怎麼做這件事。