Eclipse 啟動發生 libjvm.dylib does not contain the JNI_CreateJavaVM symbol

Eclipse/SpringToolSuites4 啟動發生 libjvm.dylib does not contain the JNI_CreateJavaVM symbol

更新 openjdk 後,無法開啟 Eclipse/SpringToolSuites4,錯誤訊息如下 :

The JVM shared library "/Users/foo/Library/Java/JavaVirtualMachines/adopt-openjdk-11.0.12/Contents/Home/bin/../lib/server/libjvm.dylib" does not contain the JNI_CreateJavaVM symbol.

看起來應該是更新的 openjdk(adopt-openjdk-11.0.12)有問題,編輯 Eclipse/SpringToolSuites4 的 Info.plist,指定要執行的 Java 路徑(原先正常的 adopt-openjdk-11.0.11)即可。

<!-- Info.plist 搜尋 -vm 字串 --> 

<string>-vm</string><string>/Users/foo/Library/Java/JavaVirtualMachines/adopt-openjdk-11.0.11/Contents/Home/bin/java</string>

為什麼要使用 Java Optional 以及 Optional 常用的方法有哪些

你用過 Java Optional 嗎?

本文假設讀者你已經用過 Optional 或是介接過回傳 Optional 的 API,知道 Optional 主要是解決 java.lang.NullPointerException(剛好是本站用的網址 XD),但還是不太知道為什麼要用 Optional!

換個角度看 Java Optional

我覺得站在開發 API 的角度來看這個問題,就很清楚為何要使用 Optional!

  • 如果回傳的結果可能為 null 就該用 Optional<T>。
  • 反之,不可能回傳 null 就跟以前一樣就用 T。

使用你 API 的人,就可以根據這個資訊,知道呼叫這個方法需不需要處理 null 的問題,而不是每個方法都要多寫處理 null 的邏輯,或是沒處裡就會發生 java.lang.NullPointerException。

// 回傳 People 代表這方法絕對會回傳一個我爸
public People getMyFather()

// 回傳 <Optional>People 代表可能回傳空物件
public <Optional>People getMyFather()

如果今天你寫的系統,每個人都必定會有個爸爸,那回傳 People 的 getMyFather() 符合你的意圖,而回傳 <Optional>People 的 getMyFather() 因為有可能回傳空物件的語意,所以就不適合用此方法。

若你寫的系統允許父不詳的狀況,那回傳 <Optional>People 的 getMyFather() 就是比較好的寫法。

我看到第一個回傳 People 的 getMyFather(),我很清楚我不用處理 null 的狀況,也就不用多寫處理 null 的邏輯。而第二個方法我就需要用 isPresent() 檢查回傳的 Optional<People>是否為一個空物件。

Java Optional 常用的方法整理

下面程式碼說明 of、ofNullable 這兩個方法:

Foo foo = null;

// of()不接受傳入 null,此處會發生 NullPointerException
Optional<Foo> optionalFoo1 = Optional.of(foo);

// ofNullable()可以傳入 null,此處會得到一個空物件 
Optional<Foo> optionalFoo2 = Optional.ofNullable(foo);

下面程式碼說明 ifPresent、get、getElse、orElseGet、orElseThrow:

// 從 getBar 取得一個 Optional<Bar>
// 由回傳 Optional<Bar> 來看
// 我們預期有可能收到空物件
Optional<Bar> optionalBar = getBar();

// 用 ifPresent + get 的寫法跟原本檢查 null 的寫法一樣差
if (optionalBar.ifPresent()) // 檢查是否為空物件
{
  Bar bar = optionalBar.get(); // 取得包在裏頭的 bar
  bar.doSometing();
}

// 當 optionalBar 裏頭包的是 null,會建立一個新的 Bar 物件並回傳
Bar bar = optionalBar.getElse(new Bar());

// 當 optionalBar 裏頭包的是 null,會建立一個新的 Bar 物件並回傳
Bar bar = optionalBar.orElseGet(() -> { 
    // 這邊可以多做一些事,比如印 Log
    return new Bar();
}));

// 當 optionalBar 裏頭包的是 null,會丟出一個自訂的例外
Bar bar = optionalBar.orElseThrow(() -> new MyException("WTF")));

結語

開發 API 的人使用 Optional 來說明回傳的物件是不是有可能為 null,讓使用 API 的人看到介面就知道是否要處理 null。

而 getElse() 能改善以往檢查是否為空,如果不為空就...的寫法,讓程式碼更簡潔更容易閱讀。

三分鐘讀懂 Encode 編碼、Encrypt 加密、Hash 雜湊

Encode、Encrypt、Hash 的差別

Encode 編碼Encrypt 加密Hash 雜湊
簡單定義原本的資料用不同的詮釋方式產生新的資料利用金鑰來保護資料 將資料用公式算出一個無法反推回原本資料的結果
是否能還原資料?知道編碼方式即可還原資料知道加密方式
+ 取得金鑰即可還原資料
無法還原資料
舉例摩斯密碼
網址編碼
Base64
AES(對稱加密)
RSA(非對稱加密)
md5
SHA1
SHA256
Encode、Encrypt、Hash 對照表

密碼要怎麼保存

如果今天在資料庫要保存一個密碼:

  1. 將密碼編碼後保存:被駭客知道編碼方式,就能還原成原本密碼。
  2. 將密碼加密後保存:被駭客取得金鑰以及加密方式,就能還原成原本密碼。
  3. 將密碼雜湊後保存:網路上有人用字典搭配各種雜湊演算法計算過後產生對照表,如果使用者用的密碼是人可閱讀的、常見密碼,就可以從對照表反查出原本密碼。

你可以制定密碼規則,強迫使用者建立比較難想到的密碼(比如要有大小寫、特殊符號、數字),這樣的密碼就比較不會從對照表被反查出來,但會造成使用者要建立自己也記不太住的密碼。

另外一種方式,就是使用者可以建立他們記得住的密碼,然後系統會在密碼後方加上由系統產生的特殊字串(就是所謂的 Salt),達到不造成使用者困擾,又能建立少見密碼的效果。

結論:

將密碼加鹽,雜湊後保存。

Windows 工作排程器如何處理還在執行中的工作

Windows 工作排程器 > 設定 > 如果工作已在執行中,下列規則將會套用:(If the task is already running, then the following rule applies)

之前用 Windows 工作排程器 都是用來執行會自動關閉的程式/Script,所以沒有設定上圖的這個選項『如果工作已在執行中,下列規則將會套用:(If the task is already running, then the following rule applies)』都沒有關係,因為舊工作很快就執行完成,執行新工作的時候不會看到還在執行中的舊工作。

這次嘗試要每天重啟一個會持續執行的程式,因為該程式發生問題並不會自己關閉,在設定上遇到了一些問題,先來解釋一下上面圖片的四個選項:

  • 不要啟動新執行個體(Do not start a new instance):預設選項。準備執行新工作時,如果舊工作還在執行,那就不會執行新工作。
  • 以平行方式執行新執行個體(Run a new instance in parallel):不管舊工作是否還在執行,都會執行新工作。
  • 佇列新執行個體(Queue a new instance):準備執行新工作時,如果舊工作還在執行,會將新工作放入一個佇列等待,等舊工作執行完畢才會執行新工作。
  • 停止現有的執行個體(Stop the existing instance):準備執行新工作時,如果舊工作還在執行,會先將舊工作關閉再執行新工作。

首先要注意的是,不要用手動執行工作的方式來驗證,你會發現每次新工作都會執行成功,但實際上自動跑的時候,行為結果都跟你想的不一樣。可以利用反覆設定僅一次的觸發時間,等到觸發時間到來驗證看看新工作是否有順利執行。

以下針對我這次的問題做紀錄,我想要定期重新執行一個 exe 檔案(因為它會發生錯誤且不會結束),四個選項觀察到的結果如下:

  • 不要啟動新執行個體(Do not start a new instance):不會執行新工作,所以排程時間過了,還是只會看到原先執行的 exe 檔案。
  • 以平行方式執行新執行個體(Run a new instance in parallel):會執行新工作,所以會看到越來越多 exe 檔案被執行。
  • 佇列新執行個體(Queue a new instance):會將新工作放入一個佇列等待,但因為原先執行的 exe 檔案不會結束,所以排程時間過了,還是只會看到原先執行的 exe 檔案。
  • 停止現有的執行個體(Stop the existing instance):沒有正確關閉原先執行的 exe 檔案(bug?),所以會看到越來越多 exe 檔案被執行。

最終的做法是,在排程工作中加入 taskkill 指令(用來刪除原先執行的 exe 檔案) + 佇列新執行個體(Queue a new instance),確保新工作總是會被執行,而執行新工作的第一個步驟就是刪除執行中的 exe 檔案,再重新執行 exe 檔案。

MultiBio zkemkeeper.CZKEMClass 無法監聽即時事件

MultiBio700 門禁考勤人臉指紋機

介接 MultiBio 門禁考勤人臉指紋機時,利用官網提供的 zkemkeeper SDK,能正確讀取設備上的打卡資料,但想要即時接收打卡事件時,完全照抄官方範例,卻怎樣都收不到設備即時回傳的事件。最大的差別在於官方範例是一個 Windows Forms 程式,我正在開發的是一個主控台(Console)程式。

可能原因是 zkemkeeper SDK 收到設備即時回傳事件後,使用的訊息派送機制跟 Windows Forms 程式會用到的 Message Loop 相關。所以嘗試在一開始就用 Application 提供的 Run() 方法,執行目前執行緒的標準應用程式 Message Loop。

記得要用 .NET Framwork 專案,而不是 .NET / .NET Core 專案(C# 工程師可能才知道我在說什麼),要不然沒有 System.Windows.Forms.Application 可以用,完整程式碼如下:

Thread thread = new Thread(() =>
{
  zkemkeeper.CZKEMClass axCZKEM1 = new zkemkeeper.CZKEMClass();

  bool bIsConnected = false; // the boolean value identifies whether the device is connected
  int idwErrorCode = 0;
  bIsConnected = axCZKEM1.Connect_Net("x.x.x.x", Convert.ToInt32("4370"));
  if (bIsConnected == true)
  {
    iMachineNumber = 1;// In fact,when you are using the tcp/ip communication,this parameter will be ignored,that is any integer will all right.Here we use 1.
    bool regEventResult = axCZKEM1.RegEvent(iMachineNumber, 65535);//Here you can register the realtime events that you want to be triggered(the parameters 65535 means registering all)
  }
  else
  {
    axCZKEM1.GetLastError(ref idwErrorCode);
    Console.WriteLine(String.Format("Unable to connect the device, ErrorCode={1}", idwErrorCode.ToString()));          
  }

  axCZKEM1.OnAttTransactionEx += new    zkemkeeper._IZKEMEvents_OnAttTransactionExEventHandler(axCZKEM1_OnAttTransactionEx); // 註冊監聽事件

  Application.Run(); 
});

thread.IsBackground = true;
//thread.SetApartmentState(ApartmentState.STA); // 我註解掉也可以運作!
thread.Start();

Console.ReadLine(); // 接收到輸入才會結束程式

參考

Java 常用的 Functional Interface:Consumer、Function、Predicate、 Supplier

在看 Functional Interface 之前,先看看以下例子,如果我們只能寫一個方法,要在一個字串集合內,找到預期的字串,同時要能支援完全比對/忽略大小寫的話,寫出來的程式碼大概長的是以下這個樣子:


/*
* expect:是要比對的字串。
* ignoreCase:true 代表比對可以忽略大小寫。
*/
public boolean findAnyMatch(List<String> list, String expect, boolean ignoreCase)
{
  if (ignoreCase) // 若任一個字串符合傳入的字串(忽略大小寫),就回傳 true。
  {
    for (String string : list)
    {
      if (expect.equalsIgnoreCase(string))
      {
        return true;
      }
    }
  }
  else // 若任一個字串符合傳入的字串(要完全一樣),就回傳 true。 
  {
    for (String string : list)
    {
      if (expect.equals(string))
      {
        return true;
      }
    }
  }
		
  return false;
}

...

System.out.println(foo.findAnyMatch(list, "Moe", false)); // true
System.out.println(foo.findAnyMatch(list, "Jack", false)); // false
System.out.println(foo.findAnyMatch(list, "moe", true)); // true

原本的寫法除了又臭又長之外,還有多重巢狀結構,不容易閱讀。

同樣的方法改用 lamda 表達式作為 findAnyMatch 傳入參數的寫法,程式碼如下,是不是短了很多?甚至還可以在不更動 findAnyMatch 的情況下,找出是否有包含 "o" 的字串。


/*
* predicate:是一個只定義一個抽象方法 interface,告訴傳入的 Lambda 表達式會收到一個參數,然後要回傳一個 boolean。
*/
public boolean findAnyMatch(List<String> list, Predicate<String> predicate)
{
  // 若任一個字串符合傳入的 predicate 檢查的條件,就回傳 true。
  return list.stream()
    .filter(predicate) // 傳入的 lambda 會收到一個字串,回傳 boolean
    .findFirst()
    .isPresent();
}

...

System.out.println(foo.findAnyMatch(list, (s) -> s.equals("Moe"))); // true
System.out.println(foo.findAnyMatch(list, (s) -> s.equals("Jack"))); // false
System.out.println(foo.findAnyMatch(list, (s) -> s.equalsIgnoreCase("moe"))); // true
System.out.println(foo.findAnyMatch(list, (s) -> s.contains("o"))); // true

所以大致的演進流程是:用傳入的參數來影響方法內的行為 > 直接把行為(lambda)當作參數。同樣的效果用傳入匿名類別當作參數也可以做到,但傳入 lambda 的寫法看起來比較短、比較好閱讀。

Java 把 lambda 寫法常用到的 Interface 先定義出來,我們在導入 lamda 寫法改寫原有方法時,就不用重複定義這些常出現的介面。比如上面用到的 Predicate 介面,它預期接收一個字串類型物件,然後返回一個 boolean 值。

以下整理出程式碼常見到的 Functional Interface:

Functional Interface輸入回傳
PredicateT 類型物件boolean
ConsumerT 類型物件不用回傳
FuctionT 類型物件R 類型物件
SupplierT 類型物件
BiPredicateT 類型物件 + U 類型物件boolean
BiConsumerT 類型物件 + U 類型物件不用回傳
BiFuctionT 類型物件 + U 類型物件R 類型物件

Intellij IDEA 啟動跳出 Unsupported Java Version

升級完 Intellij IDEA 後,要開啟 Intellij IDEA 就跳出了 Unsupported Java Version 這個警告,在 Console 下查看 Java 版本是 openjdk version "11.0.2" 2019-01-15,版本大於 Java 11,應該不是環境的問題。

執行以下指令排除此問題:

rm ~/Library/Application\ Support/JetBrains/IntelliJIdea2020.3/idea.jdk

特殊註解:TODO、FIXME、XXX

最早接觸到這些特殊註解是在寫 Eclipse 的時候,有些自動產生的程式碼就會自己補上 //TODO,在 Tasks 分頁就能顯示出這些備註,點擊就直接跳到程式碼上相當方便。

實際上在 Preferences > Java > Compiler > Task Tags 下可以看到預設只支援 TODO、FIXME 和 XXX 這三種特殊註釋。

除了 TODO、FIXME 和 XXX 外,我也列出了我比較少用,但在其他地方有看到的特殊註釋:

  • TODO:知道要實作什麼功能,但還沒開始寫的程式碼。比如://TODO 接上 LDAP 認證
  • FIXME:發現比較少發生的 bug,但當下沒時間改。比如://FIXME 對方回傳結果缺乏 foo 欄位,會造成 NullPointerException。
  • XXX:很醜但可以正常運作的程式碼,我會先用這個註釋標註,未來有空回來優化。比如://XXX 目前用 iterator 的方式掃描集合,可改用 Stream 的方式。
  • 少用
    • NOTE:說明這段程式碼怎麼運作。因為等於在寫一般的單行註解,所以我沒用過。
    • HACK:這裡用非正規的方法實作功能(Android 開發常用 reflection 機制呼叫沒開放的功能)。因為跟 XXX 類似,所以我很少用。
    • BUG:這裡有 bug。因為跟 FIXME 類似,所以我很少用。

Eclipse 啟動發生 Caused by: java.lang.ClassNotFoundException: javax.annotation.PreDestroy

因為開發環境的 SpringToolSuite4 要求更新 Java,更新到 Java 11 之後,反而 SpringToolSuite3 就打不開了,啟動就跳出下圖:

Eclipse 無法啟動

按照指示查看 log,看起來是跟 Java 9 引入的 module 功能相關,造成找不到這個 PreDestroy 這個類別。


java.lang.NoClassDefFoundError: javax/annotation/PreDestroy
        at org.eclipse.e4.core.internal.di.InjectorImpl.disposed(InjectorImpl.java:426)
        at org.eclipse.e4.core.internal.di.Requestor.disposed(Requestor.java:154)
        at org.eclipse.e4.core.internal.contexts.ContextObjectSupplier$ContextInjectionListener.update(ContextObjectSupplier.java:78)
        at org.eclipse.e4.core.internal.contexts.TrackableComputationExt.update(TrackableComputationExt.java:111)
        at org.eclipse.e4.core.internal.contexts.TrackableComputationExt.handleInvalid(TrackableComputationExt.java:74)
        at org.eclipse.e4.core.internal.contexts.EclipseContext.dispose(EclipseContext.java:176)
        at org.eclipse.e4.core.internal.contexts.osgi.EclipseContextOSGi.dispose(EclipseContextOSGi.java:106)
        at org.eclipse.e4.core.internal.contexts.osgi.EclipseContextOSGi.bundleChanged(EclipseContextOSGi.java:139)
        at org.eclipse.osgi.internal.framework.BundleContextImpl.dispatchEvent(BundleContextImpl.java:903)
        at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
        at org.eclipse.osgi.framework.eventmgr.ListenerQueue.dispatchEventSynchronous(ListenerQueue.java:148)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEventPrivileged(EquinoxEventPublisher.java:213)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEvent(EquinoxEventPublisher.java:120)
        at org.eclipse.osgi.internal.framework.EquinoxEventPublisher.publishBundleEvent(EquinoxEventPublisher.java:112)
        at org.eclipse.osgi.internal.framework.EquinoxContainerAdaptor.publishModuleEvent(EquinoxContainerAdaptor.java:156)
        at org.eclipse.osgi.container.Module.publishEvent(Module.java:476)
        at org.eclipse.osgi.container.Module.doStop(Module.java:634)
        at org.eclipse.osgi.container.Module.stop(Module.java:498)
        at org.eclipse.osgi.container.SystemModule.stop(SystemModule.java:202)
        at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle$EquinoxSystemModule$1.run(EquinoxBundle.java:165)
        at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: java.lang.ClassNotFoundException: javax.annotation.PreDestroy cannot be found by org.eclipse.e4.core.di_1.6.1.v20160712-0927
        at org.eclipse.osgi.internal.loader.BundleLoader.findClassInternal(BundleLoader.java:410)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:372)
        at org.eclipse.osgi.internal.loader.BundleLoader.findClass(BundleLoader.java:364)
        at org.eclipse.osgi.internal.loader.ModuleClassLoader.loadClass(ModuleClassLoader.java:161)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 21 more

解決方法:以下方法為 Eclipse / SpringToolSuite 通用,到 Eclipse / SpringToolSuite 資料夾下編輯 eclipse.ini 或 STS.ini,在 -vmargs 之前加上 -vm 參數指定啟動使用的 Java,下方範例我指定到我原本安裝的 Java 8 其 bin 資料夾下的 java 檔案。

另外要注意,-vm 是一列,指定路徑也是單獨一列,記得換行!


... 忽略
-vm
/Library/Java/JavaVirtualMachines/jdk1.8.0_40.jdk/Contents/Home/bin/java
-vmargs
... 忽略

iTerm2 管理 SSH 帳號密碼

在 Mac 上用 iterm2 管理所有會連線的主機,這樣就不用每次在記 IP / 帳號 / 密碼,首先先安裝 sshpass,再去 iterm2 設定 Profile(一個 Profile 可以想成一個主機),詳細步驟如下:

安裝 sshpass

打開命令列,利用 homebrew 來安裝 sshpass:


brew install https://raw.githubusercontent.com/kadwanev/bigboybrew/master/Library/Formula/sshpass.rb

設定 iTerm2

打開 iTerm2,Preferences > Profiles 分頁,在 Command 區域將選項設為 Command ,旁邊文字欄填入連線資訊。


# 連線資訊格式說明
# /usr/local/bin/sshpass -p 密碼 ssh -p Port 帳號@IP

# 範例:連線到 192.168.8.8,Port 是 22,帳號是 root,密碼是 iampassword
/usr/local/bin/sshpass -p iampassword ssh -p 22 root@192.168.8.8

設定畫面如下:

Mock 與 Stub 的差別

假設我們要測試資料庫,除了連接真正的資料庫或是本地端的測試資料庫以外,我們也可以生成假的物件來測試,以下列出 Stub / Mock 這兩種做法的測試案例,大家可以自行體會其中的差別。

Stub


/** 能連接至 MySQL 的實作 */
public class DatabaseMySql implements Database
{
  public boolean connect()
  {
    // 省略
    return true; // 連接真正的的資料庫,真的連上才回傳 true。
  }
}

/** 為了測試用的實作 */
public class DatabaseStub implements Database
{
  public boolean connect()
  {
    return true; // 直接回傳 true。
  }
}

// 分隔線

public void testStub()
{
  Database databaseStub = new DatabaseStub(); // 建立 stub

  System.out.println(databaseStub.connect()); // 印出 true
}

Mock


public void testMock()
{
  Database databaseMock = mock(Database.class); // 使用 mockito 建立 mock
  when(databaseMock.connect()).thenReturn(true); // 當呼叫 connect() 就回傳 true

  System.out.println(databaseStub.connect()); // 印出 true
}

Stub vs Mock

對我來說如果用 Mock 寫法的話:

  • 要替每個測試定義其 databaseMock 物件的輸入輸出。
  • 無需定義 Database 介面。
  • 無需實作 DatabaseStub 類別,複寫所有 Database 介面定義的方法。

我實務上比較常用 Stub 的做法:

  • 因為不用特別引入第三方函式庫就可以實現了。
  • 而且對介面開發而非對實作開發也本來就是一個好的習慣。

關於更專業的解釋,可以參考 Mocks Aren't Stubs 這一篇經典文章。

Java Stream API Cheat Sheet

在 Java 8 之後,可以用 Stream API 來處理資料集合,最大的好處就是巢狀結構消失了,比較容易閱讀、看出商業邏輯,以下紀錄比較常用的操作:

建立 Stream

  • of
  • empty
  • concat
  • generate:產生無限串流 的方法之一。
  • iterate:產生無限串流的方法之二。
// 示範 of
Stream<String> fooBarBazStream = Stream.of("foo", "bar", "baz");

// 示範 empty
Stream<String> emptyStream = Stream.empty(); 

// 示範 concat
Stream<String> stream1 = Stream.of("foo", "bar");
Stream<String> stream2 = Stream.of("baz");
Stream<String> stream3 = Stream.concat(stream1, stream2);

// 示範 generate
Stream<String> helloStream = Stream.generate(() -> "hello"); // 總是回傳 "hello"
String<Double> randomStream = Stream.generate(Math::random); // 每次回傳一個亂數

// 示範 iterate
Integer seed = 1;
Stream<Integer> oneTwoFourEghitStream = Stream.iterate(seed, n -> n*2); // 1, 2, 4, 8...

中間操作

  • peek:常用來 debug,印出經過元素。
  • limit
  • skip
  • distinct:去除重複
  • sorted
  • filter
  • map:將經過的元素類型轉換成別種元素類型,比如:員工物件轉成姓名字串。
    • mapToInt
    • mapToLong
    • mapToDouble
  • flatMap:將經過元素轉換成該元素內資料集合的元素,比如:一個員工有兩個電話(手機和住家電話),將傳入的三位員工轉成六個電話字串。
    • flatMapToInt
    • flatMapToLong
    • flatMapToDouble
// 示範 distinct
Stream.of("a", "b", "a", "b")
  .distinct()
  .forEach(e -> System.out.print(e)); // 印出 "ab"

// 示範 map
employees.stream()
  .map(e -> e.getName())
  .forEach(name -> System.out.println(name)); // 印出所有員工的姓名

// 示範 flatMap
employees.stream()
  .flatMap(e -> e.getPhoneList().stream()) // 傳入一位員工,發送出多筆電話
  .forEach(phone -> System.out.println(phone)); // 印出所有員工的電話

終止操作

終止操作完了以後,Stream 就不能再使用了。

  • max
  • min
  • findFirst
  • findAny
  • anyMatch:有符合條件的元素回傳 true。
  • allMatch
  • noneMatch
  • orElse
  • count
  • forEach:很像 peek,但 peek 是中間操作,需要一個終止操作才能執行。
  • collect:返回的是集合。
  • toArray:返回的會是陣列。
  • reduce:可以去看一下 Google 提出的 MapReduce 概念
// 示範 collect
List<String> nameList = 
  employees.stream()
    .map(e -> e.getName())
    .collect(Collectors.toList());

Java 安裝在 Docker 容器的記憶體問題

Docker 容器佔用大量記憶體空間

Docker VM 的記憶體使用量很高,進去看16G 的記憶體的機器,才裝12個 Docker 容器,一個一個檢查才發現幾個是跑 Java APP 的容器吃的記憶體特別高(1G~3G 之間),但有些容器都是 Serverless 類型的服務,不應該佔用那麼多記憶體。

Java 支援 Docker 的記憶體限制

在容器內部執行 JVM,預設 Max Heap Size 會是主機記憶體的1/4,而非容器記憶體的1/4(所以我 docker run 設定 memory 參數也沒用)。

在Java 8u131 之後支援了 Docker CPU 和記憶體的限制,在 Java 8 上要使用以下參數:

  • UnlockExperimentalVMOptions:開啟實驗性質的選項。
  • UseCGroupMemoryLimitForHeap:使用 cgroup(control group) 記憶體大小來計算 Max Heap Size ,白話就是偵測到 Docker 容器的記憶體。
  • MaxRAMFraction:將偵測到的記憶體除以這個數字,就是 Max Heap Size。這邊設為1,搭配 UseCGroupMemoryLimitForHeap 的效果就是 Docker 容器的記憶體大小除以1就是 Max Heap Size,白話就是不會超過 Docker 容器設定的記憶體。

# 建立一個記憶體500 MB 有 JRE 8 的容器,執行 Java 指令印出 Max. Heap Size
# 調整 docker run 的 memory 參數,可以影響容器內 JVM 使用的記憶體大小。
docker run --memory 500MB openjdk:8-jre java \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMFraction=1 \
-XshowSettings:vm \
-version

參考:

修正 Dockerfile 以及 docker run

了解上一章節的參數後,首先記得 docker run 一定要加上 memory 參數來限制容器的記憶體。

再來就是將原本執行 Java 程式的命令列(看是寫在 docker run 指令還是寫在 Dockerfile 裡頭)加上參數。這樣就能解決 Docker 容器跑 Java 程式佔用太多記憶體的問題。


# 執行 app.jar
docker run --memory 500MB openjdk:8-jre java \
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMFraction=1 \
-jar path/to/your/app.jar

# Dockerfile
# 省略
CMD ["java", "-Dspring.profiles.active=test", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=1", "-XshowSettings:vm",  "-jar", "path/to/your/app.jar"]

Intellij IDEA 控制台(Console)無法 Search/Find

Console 無法搜尋字串

在 Console 視窗按 ⌘ + F 不會跳出搜尋功能。

因為開發環境會使用 Eclipse 和 Intellij IDEA,不想記兩套快捷鍵,所以 keymap 就設定為 Eclipse(macOS),Eclipse(macOS) 預設 Find 和 Replace 都是空的,原本只替 Replace(通常找到就是想要替代)設定 ⌘ + F,Find 沒有設定熱鍵所以就沒作用。

替 Find 指定快捷鍵

Preferences > Keymap ,選取 Main menu > Edit > Find,指定 Find... 和 Replace... 的熱鍵即可。

  • Find...:⌘ + F
  • Replace...:⌥ + F

MacBook Pro 14"(2021) M1 Max 入手

上次換機是2020/8,距今約兩年半,上一台 MacBook Pro 13" 是未代的 intel i5 / 16G Ram / 500G SSD,當初沒有等一下換 M1 版本,主要是怕程式開發工具有支援度問題,另外我有外接三螢幕需求,需要買到最貴的 M1 Max 才支援三螢幕輸出,這次買的是整修品(M1 Max / 64G Ram / 1T SSD)價格為85300元。

現在這台 MacBook Pro 13" 開發上很夠用,缺點大概是沒用的 touch bar 還有比較容易熱,然後這一兩年有了剪片需求,整個剪輯流程大概要輸出長度20分鐘的1080P影片三次,每次都要等20分鐘以上,現在換上 M1 Max 大概兩分鐘左右就輸出完成了,大大減少我等待的時間。

這次兩台電腦轉移的體驗也很好,同個網段下開啟系統移轉輔助程式,兩個多小時完成,大部分的 APP 跟個人資料都轉移過去了,我是碰到 Chrome 轉移失敗、MS Offiece 沒有轉移這兩個問題而已,重新安裝就解決了!

關於多螢幕部分,完全沒問題可以輸出三個螢幕,目前外接的做法是:

  • MacBook Pro 14" 上的 HDMI 輸出 > 螢幕*1
  • HyperDrive 9-in-1 USB-C HUB 上的 HDMI 輸出 > 螢幕*1
  • JUA365 USB3.0 to HDMI雙外接顯卡 > 螢幕*1

以上供有想要升級 M1 的朋友參考,有問題也可以留言問看看,我懂就幫忙測看看。


2023/2/16 更新

VirtualBox 6.1 不支援 M1/M2,VirtualBox 7.0 才有 Developer preview for macOS / Arm64 (M1/M2) hosts,但根據如何评价 VirtualBox 7.0 Beta 1 支持Apple Silicon?和我實際所有 VM (CentOS、Ubuntu)都無法啟動看來,目前 VirtualBox 並不支援! 

2023/6/30 更新

VirtualBox-7.0.8_BETA4-156879-macOSArm64 不支援 M1/M2

IntelliJ IDEA 卡頓 CPU 使用率超過100%

先說結論,訂閱 Intellij IDEA 前先下載社群版,試著開個專案打打字(中英文都要),看看是否會卡頓,CPU 使用率是否會超過100%,會卡頓就不要訂閱。


IntelliJ IDEA 一打字就開始頓

最近負責的 Spring 專案中會包含 react 的程式碼,Eclipse 上找來找去都沒有免費合用的 plug-in 才能正確高亮提示,心一橫就訂閱了IntelliJ IDEA Ultimate 一年份。

之前開發 Android 都是用 IntelliJ IDEA 社群版,跑起來都很順,但這次的體驗卻非常差,運行起來超級頓,只是打字 CPU 的使用率就飆高到200%以上。

修改記憶體選項

工具列 > Help > Edit Custom VM Options…
# 檔案路徑在 /Applications/IntelliJ IDEA.app/Contents/bin/idea.vmoptions
# 預設值如下:
-Xms128m
-Xmx750m
-XX:ReservedCodeCacheSize=240m
預設值等於只允許 IntelliJ IDEA 使用750m 的記憶體,我筆電記憶體再大都沒有用,修改成最大可使用2g 的記憶體。
# 修改後的值如下:
-Xms1024m
-Xmx2048m
-XX:ReservedCodeCacheSize=1024m

關閉程式碼檢查

右下角會有一個人戴帽子的圖示,點擊後可以調整是否要檢查程式碼,都調整成 None。

降低 JIT Compiler 的 CPU 使用量

叫出 Activity Monitor 觀察 CPU 的使用量。

工具列 > Help > Edit Custom VM Options…
# 檔案路徑在 /Applications/IntelliJ IDEA.app/Contents/bin/idea.vmoptions
# 加入此列
-XX:TieredStopAtLevel=1
JIT Compiler 的 CPU 使用量有明顯降低,但 IntelliJ IDEA 只有微幅改善還是頓(因為還有其他問題占用 CPU)。

修改啟動的 JVM 版本

安裝 Plug-in:Choose Runtime,安裝後 Help > Find Action… 中搜尋”Choose Runtime” 可以開啟設定畫面,選擇你想要的 JVM 版本下載後重啟 Intellij IDEA。

沒有完全排除卡頓問題但已經可以接受

以下方法我都嘗試過的結果:
  • 降低 JIT Compiler CPU 使用率 // 有明顯改善
  • 修改記憶體 // 有些許改善
  • 關閉程式碼檢查 // 有些許改善
  • 修改 JVM // 根據選擇的 JVM 改善的幅度不同
  • 嘗試把不用的 Plug-in 關掉 // 沒改善
  • 嘗試不同版本 2020.1、2019.3、2019.2 // 沒改善
最終我修改的設定如下:
  • 降低 JIT Compiler CPU 使用率
  • 修改記憶體
  • 關閉程式碼檢查
  • 修改 JVM 改用 jbrsdk-8u202
  • Intellij IDEA 2019.3
CPU 使用率大概從原本200+%降到100+%,單純輸入英文沒太大不順的感覺,但輸入中文還是能感受輕微卡頓(我在想是不是輸入中文,Intellij IDEA 嘗試做 auto complete 還是建議輸入所造成)。

參考這篇 High CPU Usage while typing ( goes over 300%),問題依舊還沒有 close,花了錢訂閱了一個頓到不行的 IDE,只怪自己功課做得不夠多。

MySQL DATETIME 與 TIMESTAMP 的差別

 

DATETIME 與 TIMESTAMP 的差別

參考官方的說明

The DATETIME type is used when you need values that contain both date and time information. MySQL retrieves and displays DATETIME values in ‘YYYY-MM-DD HH:MM:SS’ format. The supported range is ‘1000-01-01 00:00:00’ to ‘9999-12-31 23:59:59’.

The TIMESTAMP data type has a range of ‘1970-01-01 00:00:01’ UTC to ‘2038-01-09 03:14:07’ UTC. It has varying properties, depending on the MySQL version and the SQL mode the server is running in.

MySQL converts TIMESTAMP values from the current time zone to UTC for storage, and back from UTC to the current time zone for retrieval. (This does not occur for other types such as DATETIME.)

簡單來說,兩個差別:
  • 支持的日期範圍不同:
    • DATETIME:1000-01-01 00:00:00 到 9999-12-31 23:59:59
    • TIMESTAMP:1970-01-01 00:00:01 UTC 到 2038-01-09 03:14:07 UTC
  • 儲存時對時區的處理:
    • DATETIME
      • 直接儲存不做任何轉換,你傳 2020-01-01 09:00 給 MySQL,MySQL 就當作是 2020-01-01 09:00 儲存。
      • 就算變更了 MySQL 時區,讀取出來也一樣是 2020-01-01 09:00。
    • TIMESTAMP
      • 儲存時會更根據 MySQL 時區先轉換成 UTC 時間,若 MySQL 時區是+8,你傳 2020-01-01 09:00 給 MySQL,MySQL 會轉成 2020-01-01 01:00 +00:00 儲存。
      • 讀取時會根據 MySQL 時區,將時間還原,以上例來說:
        • MySQL 時區還是+8,2020-01-01 01:00 +0000 會轉成 2020-01-01 09:00
        • 若讀取時 MySQL 時區改成+10,2020-01-01 01:00 +0000 會轉成 2020-01-01 11:00

要使用 DATETIME 還是 timestamp

如果你要儲存的日期範圍不在 1970-01-01 00:00:01 UTC 到 2038-01-09 03:14:07 UTC 之間,那就只有 DATETIME 可以選擇。但基本上西元2038年也不是太遠的未來了,所以現在應該都是優先使用 DATETIME。

PS:以上時間轉換只考慮到 MySQL 本身而已。若是你的程式跟 MySQL 串接,比如說 JDBC,JDBC 是會針對 MySQL 時區在儲存/讀取時作轉換的,請拆成兩個階段來思考比較好抓時區的問題。

Java 送出的時間與 MySQL 時間相差13個小時或差14個小時

MySQL 時間相差13或14個小時

Java 送出的時間有設定為台灣時區,但 MySQL 上看到的時間就是差13個小時,確認過 MySQL 主機時區是台灣的時區+8也沒有錯。
-- 在 MySQL 執行以下 SQL 指令,顯示 MySQL 時區以及 MySQL 主機時區
show variables like "%time_zone%";

-- 發現 system_time_zone 是 CST,代表 MySQL 主機時區是 CST
-- 發現 time_zone 是 SYSTEM,代表 MySQL 時區參考 MySQL 主機時區

發生原因在於 CST 可能是中國標準時間(China Standard Time)或是美國中部時間(Central Standard Time),造成 Java 認為 MySQL 時區是美國中部時間。

明確指定 MySQL 時區

MySQL 時區不要參考 MySQL 主機時區,明確指定 MySQL 時區為+8,讓 Java 明確知道要用+8的時區。

# 修改 my.cnf,在 [mysqld] 下增加:
default-time-zone = '+08:00'

# 需要重新啟動 MySQL
-- 重起後,重新在 MySQL 執行以下 SQL 指令,顯示 MySQL 時區以及 MySQL 主機時區
show variables like "%time_zone%";

-- system_time_zone 是 CST
-- time_zone 是 +08:00