singleton是一個保證一個類別只會產生一個物件的設計模式,可應用在file system等系統。使用singleton有一個比較明顯的缺點,因為需要透過static method取得物件,所以無法透過junit或Mockito去mock物件,會比較難寫單元測試(不過在Mockito 3.4.0之後,就可以mock static method,也許可以解決這個問題)。
在Java 1.5之前,有兩種方式可以實作singleton,這兩種方式都是先把constructor設為private,Java 1.5之後,可以用enum實作singleton,下面會一一介紹每一種方法。
public class FileUploader {
public static final FileUploader INSTANCE = new FileUploader();
private FileUploader() {}
public void uploadFile(String filePath) {
// Upload file to server
System.out.println("File uploaded: " + filePath);
}
}
class main {
public static void main(String[] args) {
FileUploader uploader = FileUploader.INSTANCE;
uploader.uploadFile("myfile.txt");
}
}
這個方法會比方法一更彈性一點,如果這個class之後有其他new instance的需求,可以透過一些參數判斷,更彈性的讓使用者決定是要拿原本的instance或者new instance。
public class FileUploader {
private static final FileUploader INSTANCE = new FileUploader();
private FileUploader() {}
public static FileUploader getInstance() { return INSTANCE; }
public void uploadFile(String filePath) {
// Upload file to server
System.out.println("File uploaded: " + filePath);
}
}
class main {
public static void main(String[] args) {
FileUploader uploader = FileUploader.getInstance();
uploader.uploadFile("myfile.txt");
}
}
上述的兩種方法都有漏洞,可以用AccessibleObject.setAccessible的方式繞過去,產生第二個新的instance。以FileUploader 為例,只要把Constructor的Accessible設為true,在拿Constructor去呼叫newInstance ,就可以取得一個全新的instance,用== 檢查即可確認第二個instance是不是新的。
import java.lang.reflect.Constructor;
class Main {
public static void main(String[] args) {
FileUploader uploader = FileUploader.getInstance();
try {
Class<? extends FileUploader> uploaderClass = uploader.getClass();
Constructor<? extends FileUploader> fileUploaderConstructor = uploaderClass.getDeclaredConstructor();
fileUploaderConstructor.setAccessible(true);
FileUploader newUploader = fileUploaderConstructor.newInstance();
boolean isSame = (uploader == newUploader);
System.out.println("uploader and newUploader is same? " + isSame);
} catch (Exception e) {
e.printStackTrace();
}
uploader.uploadFile("myfile.txt");
}
}
compile完和執行之後,發現uploader和newUploader是兩個不同的instance,就可以確認AccessibleObject.setAccessible可以利用singleton的漏洞,破解了singleton保證一個類別只會產生一個物件的原則。
那麼,要如何避免上述兩個singleton的寫法,被破壞產生第二個instance呢?可以在FileUploader的constructor檢查是不是已經有instance,如果有就拋出exception,不讓使用者透過constructor去new instance。
public class FileUploader {
private static final FileUploader INSTANCE = new FileUploader();
private FileUploader() {
if (INSTANCE != null) {
throw new IllegalStateException("Instance already exists");
}
}
public static FileUploader getInstance() { return INSTANCE; }
public void uploadFile(String filePath) {
// Upload file to server
System.out.println("File uploaded: " + filePath);
}
}
在java 1.5版之後,有一個簡單的方法可以實作singleton,就是使用Enum並定義type,即可完成實作singleton。這個方法比較簡便也比較沒有上面兩個方法的問題。
public enum FileUploader {
INSTANCE;
public void uploadFile(String filePath) {
// Upload file to server
System.out.println("File uploaded: " + filePath);
}
}
class Main {
public static void main(String[] args) {
FileUploader uploader = FileUploader.INSTANCE;
uploader.uploadFile("myfile.txt");
}
}