Lucene?索引刪除策略源碼解析
Lucene
從今天開(kāi)始,我們要開(kāi)始介紹Lucene中索引構(gòu)建的流程。因?yàn)樗饕龢?gòu)建的邏輯涉及到的東西非常多,如果從構(gòu)建入口IndexWriter來(lái)開(kāi)始介紹,是很難說(shuō)清楚的。所以接下來(lái)按化零為整的方式 ,從構(gòu)建相關(guān)的各個(gè)組件開(kāi)始介紹,盡量每一篇文章都是可以獨(dú)立閱讀,依賴的前置知識(shí)都是我已經(jīng)介紹的內(nèi)容。不管就算如此,還是會(huì)有部分內(nèi)容可能需要結(jié)合整體流程才能明白,對(duì)于這部分的內(nèi)容,大家可以先留個(gè)印象,以后介紹相關(guān)聯(lián)的內(nèi)容時(shí),我會(huì)再重新指出。
今天我們一起來(lái)看看索引文件刪除相關(guān)的。
IndexCommit
Lucene中,需要持久化的索引信息都要進(jìn)行commit操作,然后會(huì)生成一個(gè)segments_N的索引文件記錄此次commit相關(guān)的索引信息。
一次commit生成segments_N之后,就對(duì)應(yīng)了一個(gè)IndexCommit,IndexCommit只是一個(gè)接口,它定義了可以從IndexCommit中獲取哪些信息:
public abstract class IndexCommit implements Comparable<IndexCommit> { // commit對(duì)應(yīng)的segments_N public abstract String getSegmentsFileName(); // commit關(guān)聯(lián)的所有的索引文件 public abstract Collection<String> getFileNames() throws IOException; // 索引所在的Directory public abstract Directory getDirectory(); // 刪除commit,后面會(huì)看到,刪除其實(shí)減少commit關(guān)聯(lián)的索引文件的引用計(jì)數(shù) public abstract void delete(); // commit是否被刪除了 public abstract boolean isDeleted(); // commit關(guān)聯(lián)了幾個(gè)segment public abstract int getSegmentCount(); // segments_N文件中的N public abstract long getGeneration(); // commit可以記錄一些用戶自定義的信息 public abstract Map<String, String> getUserData() throws IOException; // 用來(lái)讀取commit對(duì)應(yīng)的索引數(shù)據(jù) StandardDirectoryReader getReader() { return null; } }
IndexCommit有三個(gè)實(shí)現(xiàn)類:
- CommitPoint
- ReaderCommit
- SnapshotCommitPoint
這個(gè)三個(gè)實(shí)現(xiàn)類都有對(duì)應(yīng)的使用場(chǎng)景,在用到的時(shí)候我會(huì)再詳細(xì)介紹,本文中會(huì)涉及到SnapshotCommitPoint,后面會(huì)詳細(xì)介紹它。
IndexDeletionPolicy
在索引的生命周期中,可以有多次的commit操作,因此也會(huì)生成多個(gè)segments_N文件,對(duì)于這些文件是否要保留還是刪除,lucene中是通過(guò)IndexDeletionPolicy來(lái)管理的。我們先來(lái)看下IndexDeletionPolicy的接口定義:
public abstract class IndexDeletionPolicy { protected IndexDeletionPolicy() {} // 重新打開(kāi)索引的時(shí)候,對(duì)所有commit的處理 public abstract void onInit(List<? extends IndexCommit> commits) throws IOException; // 有新提交時(shí)對(duì)所有commit的處理 public abstract void onCommit(List<? extends IndexCommit> commits) throws IOException; }
從上面我可以看到,索引的刪除策略其實(shí)只在兩個(gè)地方進(jìn)行應(yīng)用,一個(gè)是加載索引的時(shí)候,打開(kāi)一個(gè)舊索引時(shí),根據(jù)當(dāng)前設(shè)置的IndexDeletionPolicy進(jìn)行處理。另一個(gè)是有新的commit產(chǎn)生時(shí),借這個(gè)機(jī)會(huì)處理所有的commit。Lucene中提供的索引刪除策略一共有四種,不過(guò)可以分為三類:
NoDeletionPolicy
NoDeletionPolicy索引刪除策略就是保留所有的commit信息,效果就是你有多少次commit就多少個(gè)segments_N文件,看個(gè)例子:
public class DeletionPolicyTest { private static final Random RANDOM = new Random(); public static void main(String[] args) throws IOException { Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath()); WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setUseCompoundFile(true); indexWriterConfig.setIndexDeletionPolicy(NoDeletionPolicy.INSTANCE); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第一次commit,生成segments_1 indexWriter.commit(); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第二次commit,生成segments_2 indexWriter.commit(); indexWriter.close(); } private static Document getDoc(int... point) { Document doc = new Document(); IntPoint intPoint = new IntPoint("point", point); doc.add(intPoint); return doc; } }
上面的例子中有兩次commit,下圖是NoDeletionPolicy策略進(jìn)行了兩次commit的索引目錄結(jié)構(gòu),可以看到生成了兩個(gè)segments_N文件:
NoDeletionPolicy的代碼實(shí)現(xiàn)非常簡(jiǎn)單,單例實(shí)現(xiàn),并且在onCommit和onInit的時(shí)候都是空操作:
public final class NoDeletionPolicy extends IndexDeletionPolicy { public static final IndexDeletionPolicy INSTANCE = new NoDeletionPolicy(); private NoDeletionPolicy() { } public void onCommit(List<? extends IndexCommit> commits) {} public void onInit(List<? extends IndexCommit> commits) {} }
KeepOnlyLastCommitDeletionPolicy
KeepOnlyLastCommitDeletionPolicy是Lucene默認(rèn)的索引刪除策略,只保留最新的一次commit,從索引目錄看不管執(zhí)行多少次commit只保留了N最大的segments_N文件,下圖是KeepOnlyLastCommitDeletionPolicy策略進(jìn)行了兩次commit的結(jié)果,KeepOnlyLastCommitDeletionPolicy刪除策略只保留了segments_2。把上面示例代碼中的刪除策略替換成KeepOnlyLastCommitDeletionPolicy,即可得到,注意需要先清空索引目錄:
KeepOnlyLastCommitDeletionPolicy代碼實(shí)現(xiàn)也比較簡(jiǎn)單,除了最后一個(gè)commit之外,其他的commit都刪除:
public final class KeepOnlyLastCommitDeletionPolicy extends IndexDeletionPolicy { public KeepOnlyLastCommitDeletionPolicy() {} public void onInit(List<? extends IndexCommit> commits) { onCommit(commits); } // commits是從舊到新排序的 public void onCommit(List<? extends IndexCommit> commits) { // 只保留最新的一個(gè) int size = commits.size(); for (int i = 0; i < size - 1; i++) { commits.get(i).delete(); } } }
兩個(gè)快照相關(guān)的刪除策略
快照相關(guān)的刪除策略有兩個(gè),SnapshotDeletionPolicy和PersistentSnapshotDeletionPolicy,分別對(duì)應(yīng)了不可持久化和可持久化的模式。不管是SnapshotDeletionPolicy還是PersistentSnapshotDeletionPolicy,他們都封裝了其他的IndexDeletionPolicy來(lái)執(zhí)行刪除策略,他們只是提供了為當(dāng)前最新的commit生成快照的能力。只要快照存在,則跟快照相關(guān)的所有索引文件都會(huì)被無(wú)條件保留。
SnapshotDeletionPolicy
例子
public class SnapshotDeletionPolicyTest { private static final Random RANDOM = new Random(); public static void main(String[] args) throws IOException, InterruptedException { Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath()); WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setUseCompoundFile(true); SnapshotDeletionPolicy snapshotDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); indexWriterConfig.setIndexDeletionPolicy(snapshotDeletionPolicy); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第一次commit,生成segments_1 indexWriter.commit(); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第二次commit,生成segments_2 indexWriter.commit(); // segments_2當(dāng)做快照,無(wú)條件保留 snapshotDeletionPolicy.snapshot(); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第三次commit,生成segments_3 indexWriter.commit(); indexWriter.close(); } private static Document getDoc(int... point) { Document doc = new Document(); IntPoint intPoint = new IntPoint("point", point); doc.add(intPoint); return doc; } }
在上面的例子中,我們使用SnapshotDeletionPolicy,SnapshotDeletionPolicy底層封裝的是KeepOnlyLastCommitDeletionPolicy,我們進(jìn)行了三次commit,理論上KeepOnlyLastCommitDeletionPolicy只會(huì)保留最后一次,但是因?yàn)槲覀儗?duì)第一次的commit進(jìn)行了快照,所以第一次commit也被保留了:
接下來(lái)我們看看SnapshotDeletionPolicy是怎么實(shí)現(xiàn)。SnapshotDeletionPolicy保證生成快照的commit不會(huì)被刪除的原理就是引用計(jì)數(shù),SnapshotDeletionPolicy會(huì)記錄每個(gè)commit生成快照的次數(shù),在刪除的時(shí)候,只會(huì)刪除引用計(jì)數(shù)為0的commit。
成員變量
// key是IndexCommit的generation,value是對(duì)應(yīng)的IndexCommit有多少個(gè)快照 // 需要注意的是,有被快照引用的才會(huì)記錄在refCounts中,也就是只要被記錄在refCounts中,引用次數(shù)至少是1 protected final Map<Long, Integer> refCounts = new HashMap<>(); // key是IndexCommit的generation,value是對(duì)應(yīng)的IndexCommit protected final Map<Long, IndexCommit> indexCommits = new HashMap<>(); // SnapshotDeletionPolicy只是增加了支持快照的功能,刪除的邏輯是由primary參數(shù)對(duì)應(yīng)的刪除策略提供的 private final IndexDeletionPolicy primary; // 最近一次提交的commit,只會(huì)對(duì)這個(gè)IndexCommit生成快照 protected IndexCommit lastCommit; // 是否初始化的標(biāo)記,實(shí)例化后,必須先調(diào)用onInit方法 private boolean initCalled;
生成快照
生成快照只會(huì)對(duì)當(dāng)前最新的一個(gè)commit進(jìn)行快照:
public synchronized IndexCommit snapshot() throws IOException { if (!initCalled) { throw new IllegalStateException( "this instance is not being used by IndexWriter; be sure to use the instance returned from writer.getConfig().getIndexDeletionPolicy()"); } if (lastCommit == null) { throw new IllegalStateException("No index commit to snapshot"); } // 新增lastCommit的引用計(jì)數(shù) incRef(lastCommit); return lastCommit; } protected synchronized void incRef(IndexCommit ic) { long gen = ic.getGeneration(); Integer refCount = refCounts.get(gen); int refCountInt; if (refCount == null) { // 第一次被引用 indexCommits.put(gen, lastCommit); refCountInt = 0; } else { refCountInt = refCount.intValue(); } // 引用計(jì)數(shù)加+1 refCounts.put(gen, refCountInt + 1); }
釋放指定的快照
public synchronized void release(IndexCommit commit) throws IOException { long gen = commit.getGeneration(); releaseGen(gen); } protected void releaseGen(long gen) throws IOException { if (!initCalled) { throw new IllegalStateException( "this instance is not being used by IndexWriter; be sure to use the instance returned from writer.getConfig().getIndexDeletionPolicy()"); } Integer refCount = refCounts.get(gen); if (refCount == null) { throw new IllegalArgumentException("commit gen=" + gen + " is not currently snapshotted"); } int refCountInt = refCount.intValue(); assert refCountInt > 0; refCountInt--; if (refCountInt == 0) { // 引用計(jì)數(shù)為0,直接從refCounts中移除 refCounts.remove(gen); indexCommits.remove(gen); } else { refCounts.put(gen, refCountInt); } }
刪除commit
public synchronized void onCommit(List<? extends IndexCommit> commits) throws IOException { // 把commits中的所有IndexCommit都封裝成SnapshotCommitPoint,再使用primary執(zhí)行onCommit方法 primary.onCommit(wrapCommits(commits)); // 更新最新的commit lastCommit = commits.get(commits.size() - 1); } @Override public synchronized void onInit(List<? extends IndexCommit> commits) throws IOException { // 設(shè)置初始化的標(biāo)記 initCalled = true; primary.onInit(wrapCommits(commits)); for (IndexCommit commit : commits) { if (refCounts.containsKey(commit.getGeneration())) { indexCommits.put(commit.getGeneration(), commit); } } if (!commits.isEmpty()) { lastCommit = commits.get(commits.size() - 1); } } private List<IndexCommit> wrapCommits(List<? extends IndexCommit> commits) { List<IndexCommit> wrappedCommits = new ArrayList<>(commits.size()); for (IndexCommit ic : commits) { // 把IndexCommit都封裝成 SnapshotCommitPoint wrappedCommits.add(new SnapshotCommitPoint(ic)); } return wrappedCommits; }
前面我們列出了SnapshotCommitPoint是IndexCommit的一個(gè)實(shí)現(xiàn)類,但是沒(méi)有詳細(xì)介紹,SnapshotCommitPoint除了能夠提供IndexCommit接口所提供的信息之外,最核心的是在刪除的時(shí)候,會(huì)先判斷IndexCommit是否被快照引用,只有沒(méi)有任何快照引用的IndexCommit才能刪除:
public void delete() { synchronized (SnapshotDeletionPolicy.this) { if (!refCounts.containsKey(cp.getGeneration())) { cp.delete(); } } }
存在的問(wèn)題
需要注意的是SnapshotDeletionPolicy的快照信息是沒(méi)有持久化,我們重新打開(kāi)SnapshotDeletionPolicyTest例子中生成的索引:
public class SnapshotDeletionPolicyTest2 { public static void main(String[] args) throws IOException, InterruptedException { Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath()); WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setUseCompoundFile(true); SnapshotDeletionPolicy snapshotDeletionPolicy = new SnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy()); indexWriterConfig.setIndexDeletionPolicy(snapshotDeletionPolicy); // 重新打開(kāi)索引 IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.close(); } }
可以發(fā)現(xiàn)segments_1被刪除了,因?yàn)闆](méi)有持久化快照信息,所以根據(jù)KeepOnlyLastCommitDeletionPolicy的刪除策略,只保留了最新的一個(gè)commit:
PersistentSnapshotDeletionPolicy
例子
PersistentSnapshotDeletionPolicy主要是為了解決SnapshotDeletionPolicy無(wú)法持久化的問(wèn)題。PersistentSnapshotDeletionPolicy持久化的時(shí)候會(huì)生成snapshots_N的索引文件,我們看個(gè)例子:
public class PersistentSnapshotDeletionPolicyTest { private static final Random RANDOM = new Random(); public static void main(String[] args) throws IOException, InterruptedException { Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath()); WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setUseCompoundFile(true); PersistentSnapshotDeletionPolicy persistentSnapshotDeletionPolicy = new PersistentSnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy(), directory); indexWriterConfig.setIndexDeletionPolicy(persistentSnapshotDeletionPolicy); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第一次commit,生成segments_1 indexWriter.commit(); // segments_1當(dāng)做快照,無(wú)條件保留 persistentSnapshotDeletionPolicy.snapshot(); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第二次commit,生成segments_2 indexWriter.commit(); indexWriter.addDocument(getDoc(RANDOM.nextInt(10000),RANDOM.nextInt(10000))); // 第三次commit,生成segments_3 indexWriter.commit(); indexWriter.close(); } private static Document getDoc(int... point) { Document doc = new Document(); IntPoint intPoint = new IntPoint("point", point); doc.add(intPoint); return doc; } }
上面的例子和我們?cè)诮榻BSnapshotDeletionPolicy的時(shí)候邏輯一樣,只是把SnapshotDeletionPolicy換成了PersistentSnapshotDeletionPolicy,我們看結(jié)果:
從上面結(jié)果圖中可以看到,segments_1和segments_3同樣被保留了,但是多了一個(gè)持久化的快照信息的文件snapshots_0,有了這個(gè)文件,索引重新打開(kāi)的時(shí)候就可以恢復(fù)快照信息,segments_1還是會(huì)被保留,用下面的例子我們重新打開(kāi)索引,可以發(fā)現(xiàn)segments_1還是被保留了:
public class PersistentSnapshotDeletionPolicyTest2 { public static void main(String[] args) throws IOException, InterruptedException { Directory directory = FSDirectory.open(new File("D:\\code\\lucene-9.1.0-learning\\data").toPath()); WhitespaceAnalyzer analyzer = new WhitespaceAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); indexWriterConfig.setUseCompoundFile(true); PersistentSnapshotDeletionPolicy persistentSnapshotDeletionPolicy = new PersistentSnapshotDeletionPolicy(new KeepOnlyLastCommitDeletionPolicy(), directory); indexWriterConfig.setIndexDeletionPolicy(persistentSnapshotDeletionPolicy); IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig); indexWriter.close(); } }
接下來(lái)我們看看PersistentSnapshotDeletionPolicy的實(shí)現(xiàn),主要就是持久化和恢復(fù)快照信息的邏輯。
成員變量
// 持久化快照信息的文件名snapshots_N中的N,從0開(kāi)始 private long nextWriteGen; // 持久化的文件所在的目錄 private final Directory dir;
構(gòu)造函數(shù)
public PersistentSnapshotDeletionPolicy(IndexDeletionPolicy primary, Directory dir) throws IOException { this(primary, dir, OpenMode.CREATE_OR_APPEND); } public PersistentSnapshotDeletionPolicy(IndexDeletionPolicy primary, Directory dir, OpenMode mode) throws IOException { super(primary); this.dir = dir; if (mode == OpenMode.CREATE) { // 新建索引的模式,則需要清除所有的快照信息,索引模式以后再介紹 clearPriorSnapshots(); } // 加載快照信息 loadPriorSnapshots(); if (mode == OpenMode.APPEND && nextWriteGen == 0) { throw new IllegalStateException("no snapshots stored in this directory"); } }
生成快照
public synchronized IndexCommit snapshot() throws IOException { // 使用SnapshotDeletionPolicy來(lái)生成快照 IndexCommit ic = super.snapshot(); // 標(biāo)記持久化是否成功,不成功的話需要?jiǎng)h除快照 boolean success = false; try { // 持久化最新的快照信息 persist(); success = true; } finally { if (!success) { // 持久化失敗,刪除快照 try { super.release(ic); } catch ( @SuppressWarnings("unused") Exception e) { // Suppress so we keep throwing original exception } } } return ic; }
釋放快照
public synchronized void release(IndexCommit commit) throws IOException { // 使用SnapshotDeletionPolicy來(lái)釋放快照 super.release(commit); // 持久化快照信息是否成功 boolean success = false; try { // 持久化最新的快照信息 persist(); success = true; } finally { if (!success) { // 持久化失敗,重新加回快照信息 try { incRef(commit); } catch ( @SuppressWarnings("unused") Exception e) { // Suppress so we keep throwing original exception } } } }
持久化快照信息
private synchronized void persist() throws IOException { // 快照文件名 String fileName = SNAPSHOTS_PREFIX + nextWriteGen; boolean success = false; try (IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT)) { CodecUtil.writeHeader(out, CODEC_NAME, VERSION_CURRENT); out.writeVInt(refCounts.size()); for (Entry<Long, Integer> ent : refCounts.entrySet()) { // 持久化所有的引用信息 out.writeVLong(ent.getKey()); out.writeVInt(ent.getValue()); } success = true; } finally { if (!success) { IOUtils.deleteFilesIgnoringExceptions(dir, fileName); } } dir.sync(Collections.singletonList(fileName)); if (nextWriteGen > 0) { String lastSaveFile = SNAPSHOTS_PREFIX + (nextWriteGen - 1); // 刪除前一個(gè)快照文件,因?yàn)槊看纬志没际前旬?dāng)前的快照信息全量持久化,所以只需要保留最新的一個(gè)就可以 // 這里有可能刪除失敗,所以在啟動(dòng)加載的時(shí)候會(huì)再次嘗試把舊版本的文件都刪掉 IOUtils.deleteFilesIgnoringExceptions(dir, lastSaveFile); } nextWriteGen++; }
加載快照信息
private synchronized void loadPriorSnapshots() throws IOException { long genLoaded = -1; IOException ioe = null; List<String> snapshotFiles = new ArrayList<>(); for (String file : dir.listAll()) { if (file.startsWith(SNAPSHOTS_PREFIX)) { // 找到快照文件 long gen = Long.parseLong(file.substring(SNAPSHOTS_PREFIX.length())); if (genLoaded == -1 || gen > genLoaded) { // 找到gen最大的快照文件 snapshotFiles.add(file); Map<Long, Integer> m = new HashMap<>(); IndexInput in = dir.openInput(file, IOContext.DEFAULT); try { CodecUtil.checkHeader(in, CODEC_NAME, VERSION_START, VERSION_START); int count = in.readVInt(); for (int i = 0; i < count; i++) { long commitGen = in.readVLong(); int refCount = in.readVInt(); m.put(commitGen, refCount); } } catch (IOException ioe2) { // 保存第一個(gè)捕獲到的異常 if (ioe == null) { ioe = ioe2; } } finally { in.close(); } genLoaded = gen; // 清除舊數(shù)據(jù) refCounts.clear(); // 保留最新的 refCounts.putAll(m); } } } if (genLoaded == -1) { // 沒(méi)有加載快照文件 if (ioe != null) { // 加載過(guò)程中捕獲到異常了,直接拋出 throw ioe; } } else { // 把舊版本的快照文件都刪掉 if (snapshotFiles.size() > 1) { String curFileName = SNAPSHOTS_PREFIX + genLoaded; for (String file : snapshotFiles) { if (!curFileName.equals(file)) { IOUtils.deleteFilesIgnoringExceptions(dir, file); } } } nextWriteGen = 1 + genLoaded; } }
總結(jié)
本文介紹的索引刪除策略是在IndexCommit粒度的控制,具體到每個(gè)索引文件是怎么控制的,我們下一篇文章介紹,更多關(guān)于Lucene 索引刪除策略的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
GraalVM和Spring Native嘗鮮一步步讓Springboot啟動(dòng)飛起來(lái)66ms完成啟動(dòng)
GraalVM是高性能的JDK,支持Java/Python/JavaScript等語(yǔ)言,它可以讓Java變成二進(jìn)制文件來(lái)執(zhí)行,讓程序在任何地方運(yùn)行更快,這篇文章主要介紹了GraalVM和Spring Native嘗鮮一步步讓Springboot啟動(dòng)飛起來(lái)66ms完成啟動(dòng),需要的朋友可以參考下2023-02-02Java在運(yùn)行時(shí)識(shí)別類型信息的方法詳解
這篇文章主要給大家介紹了關(guān)于Java在運(yùn)行時(shí)識(shí)別類型信息的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考借鑒,下面來(lái)一起看看吧2019-01-01Java實(shí)現(xiàn)驗(yàn)證碼具體代碼(圖片、漢字)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)驗(yàn)證碼具體代碼,包括圖片驗(yàn)證碼、漢字驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-01-01SpringBoot 實(shí)現(xiàn)定時(shí)任務(wù)的方法詳解
這篇文章主要介紹了SpringBoot 實(shí)現(xiàn)定時(shí)任務(wù)的方法詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08SpringCloud Bus消息總線的實(shí)現(xiàn)
消息總線是一種通信工具,可以在機(jī)器之間互相傳輸消息、文件等,這篇文章主要介紹了SpringCloud Bus消息總線的實(shí)現(xiàn),Spring cloud bus 通過(guò)輕量消息代理連接各個(gè)分布的節(jié)點(diǎn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05