搜尋
首頁Javajava教程【MyBatis源碼解析】MyBatis一二級緩存

【MyBatis源碼解析】MyBatis一二級緩存

Jun 26, 2017 am 10:00 AM
mybatis原始碼解析

MyBatis快取

我們知道,頻繁的資料庫操作是非常耗費效能的(主要是因為對於DB而言,資料是持久化在磁碟中的,因此查詢操作需要通過IO,IO操作速度相比內存操作速度慢了好幾個量級),尤其是對於一些相同的查詢語句,完全可以把查詢結果存儲起來,下次查詢同樣的內容的時候直接從記憶體中取得資料即可,這樣在某些場景下可以大大提升查詢效率。

MyBatis的快取分為兩種:

  1. 一級緩存,一級快取是SqlSession等級的緩存,對於相同的查詢,會從快取中傳回結果而不是查詢資料庫

  2. 二級緩存,二級緩存是Mapper層級的緩存,定義在Mapper檔案的標籤中並需要開啟此緩存,多個Mapper檔案可以共用一個緩存,依賴標籤配置

下面來詳細看一下MyBatis的一二級快取。

 

MyBatis一級快取工作流程

接著看MyBatis一級快取工作流程。前面說了,MyBatis的一級緩存是SqlSession級別的緩存,當openSession()的方法運行完畢或主動調用了SqlSession的close方法,SqlSession就被回收了,一級緩存與此同時也一起被回收掉了。前面的文章有說過,在MyBatis中,無論selectOne還是selectList方法,最後都被轉換為了selectList方法來執行,那麼看一下SqlSession的selectList方法的實作:

#
 1 public <e> List<e> selectList(String statement, Object parameter, RowBounds rowBounds) { 2     try { 3       MappedStatement ms = configuration.getMappedStatement(statement); 4       return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5     } catch (Exception e) { 6       throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e); 7     } finally { 8       ErrorContext.instance().reset(); 9     }10 }</e></e>

繼續追蹤第4行的程式碼,到BaseExeccutor的query方法:

1 public <e> List<e> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2     BoundSql boundSql = ms.getBoundSql(parameter);3     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);4     return query(ms, parameter, rowBounds, resultHandler, key, boundSql);5 }</e></e>

第3行建置快取條件CacheKey,這裡牽涉到怎麼樣條件算是和上一次查詢是同一個條件的問題,因為同一個條件就可以回傳上一次的結果回去,這部分程式碼留在下一部分分析。

接著看第4行的query方法的實現,程式碼位於CachingExecutor中:

 1 public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2       throws SQLException { 3     Cache cache = ms.getCache(); 4     if (cache != null) { 5       flushCacheIfRequired(ms); 6       if (ms.isUseCache() && resultHandler == null) { 7         ensureNoOutParams(ms, parameterObject, boundSql); 8         @SuppressWarnings("unchecked") 9         List<e> list = (List<e>) tcm.getObject(cache, key);10         if (list == null) {11           list = delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12           tcm.putObject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }</e></e></e></e></e></e>

第3行~第16行的程式碼先不管,繼續跟第17行的query方法,程式碼位於BaseExecutor:

 1 public <e> List<e> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { 2     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); 3     if (closed) { 4       throw new ExecutorException("Executor was closed."); 5     } 6     if (queryStack == 0 && ms.isFlushCacheRequired()) { 7       clearLocalCache(); 8     } 9     List<e> list;10     try {11       queryStack++;12       list = resultHandler == null ? (List<e>) localCache.getObject(key) : null;13       if (list != null) {14         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);15       } else {16         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);17       }18     } finally {19       queryStack--;20     }21     ...22 }</e></e></e></e>

看12行,query的时候会尝试从localCache中去获取查询结果,如果获取到的查询结果为null,那么执行16行的代码从DB中捞数据,捞完之后会把CacheKey作为key,把查询结果作为value放到localCache中。

MyBatis一级缓存存储流程看完了,接着我们从这段代码中可以得到三个结论:

  1. MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor

  2. MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为"localCache"的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果

  3. MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将,这意味着每次查询的时候都会清理一遍PerpetualCache,PerpetualCache中没数据,自然只能走DB

从MyBatis一级缓存来看,它以单纯的HashMap做缓存,没有容量控制,而一次SqlSession中通常来说并不会有大量的查询操作,因此只适用于一次SqlSession,如果用到二级缓存的Mapper级别的场景,有可能缓存数据不断碰到而导致内存溢出。

还有一点,差点忘了写了,最终都会转换为update方法,看一下BaseExecutor的update方法:

1 public int update(MappedStatement ms, Object parameter) throws SQLException {2     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());3     if (closed) {4       throw new ExecutorException("Executor was closed.");5     }6     clearLocalCache();7     return doUpdate(ms, parameter);8 }

第6行clearLocalCache()方法,这意味着所有的增、删、改都会清空本地缓存,这和是否配置了flushCache=true是无关的。

这很好理解,因为增、删、改这三种操作都可能会导致查询出来的结果并不是原来的结果,如果增、删、改不清理缓存,那么可能导致读取出来的数据是脏数据。

 

一级缓存的CacheKey

接着我们看下一个问题:怎么样的查询条件算和上一次查询是一样的查询,从而返回同样的结果回去?这个问题,得从CacheKey说起。

我们先看一下CacheKey的数据结构:

 1 public class CacheKey implements Cloneable, Serializable { 2  3   private static final long serialVersionUID = 1146682552656046210L; 4  5   public static final CacheKey NULL_CACHE_KEY = new NullCacheKey(); 6  7   private static final int DEFAULT_MULTIPLYER = 37; 8   private static final int DEFAULT_HASHCODE = 17; 9 10   private int multiplier;11   private int hashcode;12   private long checksum;13   private int count;14   private List<object> updateList;15   ...16 }</object>

其中最重要的是第14行的updateList这个两个属性,为什么这么说,因为HashMap的Key是CacheKey,而HashMap的get方法是先判断hashCode,在hashCode冲突的情况下再进行equals判断,因此最终无论如何都会进行一次equals的判断,看下equals方法的实现:

 1 public boolean equals(Object object) { 2     if (this == object) { 3       return true; 4     } 5     if (!(object instanceof CacheKey)) { 6       return false; 7     } 8  9     final CacheKey cacheKey = (CacheKey) object;10 11     if (hashcode != cacheKey.hashcode) {12       return false;13     }14     if (checksum != cacheKey.checksum) {15       return false;16     }17     if (count != cacheKey.count) {18       return false;19     }20 21     for (int i = 0; i 

看到整个方法的流程都是围绕着updateList中的每个属性进行逐一比较,因此再进一步的,我们要看一下updateList中到底存储了什么。

关于updateList里面存储的数据我们可以看下哪里使用了updateList的add方法,然后一步一步反推回去即可。updateList中数据的添加是在doUpdate方法中:

 1 private void doUpdate(Object object) { 2     int baseHashCode = object == null ? 1 : object.hashCode(); 3  4     count++; 5     checksum += baseHashCode; 6     baseHashCode *= count; 7  8     hashcode = multiplier * hashcode + baseHashCode; 9 10     updateList.add(object);11 }

它的调用方为update方法:

 1 public void update(Object object) { 2     if (object != null && object.getClass().isArray()) { 3       int length = Array.getLength(object); 4       for (int i = 0; i 

这里主要是对输入参数是数组类型进行了一次判断,是数组就遍历逐一做doUpdate,否则就直接做doUpdate。再看update方法的调用方,其实update方法的调用方有挺多处,但是这里我们要看的是Executor中的,看一下BaseExecutor中的createCacheKey方法实现:

1 ...2 CacheKey cacheKey = new CacheKey();3 cacheKey.update(ms.getId());4 cacheKey.update(rowBounds.getOffset());5 cacheKey.update(rowBounds.getLimit());6 cacheKey.update(boundSql.getSql());7 ...

到了这里应当一目了然了,MyBastis从三组共四个条件判断两次查询是相同的:

  1. 标签的id属性

  2. RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE

即只要两次查询满足以上三个条件且没有定义flushCache="true",那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。

 

MyBatis二级缓存

上面说完了MyBatis,接着看一下MyBatis二级缓存,还是从二级缓存工作流程开始。还是从DefaultSqlSession的selectList方法进去:

 1 public <e> List<e> selectList(String statement, Object parameter, RowBounds rowBounds) { 2     try { 3       MappedStatement ms = configuration.getMappedStatement(statement); 4       return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); 5     } catch (Exception e) { 6       throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e); 7     } finally { 8       ErrorContext.instance().reset(); 9     }10 }</e></e>

执行query方法,方法位于CachingExecutor中:

1 public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {2     BoundSql boundSql = ms.getBoundSql(parameterObject);3     CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);4     return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);5 }</e></e>

继续跟第4行的query方法,同样位于CachingExecutor中:

 1 public <e> List<e> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) 2       throws SQLException { 3     Cache cache = ms.getCache(); 4     if (cache != null) { 5       flushCacheIfRequired(ms); 6       if (ms.isUseCache() && resultHandler == null) { 7         ensureNoOutParams(ms, parameterObject, boundSql); 8         @SuppressWarnings("unchecked") 9         List<e> list = (List<e>) tcm.getObject(cache, key);10         if (list == null) {11           list = delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);12           tcm.putObject(cache, key, list); // issue #578 and #11613         }14         return list;15       }16     }17     return delegate.<e> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);18 }</e></e></e></e></e></e>

从这里看到,执行第17行的BaseExecutor的query方法之前,会先拿Mybatis二级缓存,而BaseExecutor的query方法会优先读取MyBatis一级缓存,由此可以得出一个重要结论:假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存

而第3行~第16行的逻辑:

  • 第5行的方法很好理解,根据flushCache=true或者flushCache=false判断是否要清理二级缓存

  • 第7行的方法是保证MyBatis二级缓存不会存储存储过程的结果

  • 第9行的方法先尝试从tcm中获取查询结果,这个tcm解释一下,这又是一个装饰器模式(数数MyBatis用到了多少装饰器模式了),创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的创建的Cache实例

  • 第10行~第12行,如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去

至于如何判定上次查询和这次查询是一次查询?由于这里的CacheKey和MyBatis一级缓存使用的是同一个CacheKey,因此它的判定条件和前文写过的MyBatis一级缓存三个维度的判定条件是一致的。

最后再来谈一点,"Cache cache = ms.getCache()"这句代码十分重要,这意味着Cache是从MappedStatement中获取到的,而MappedStatement又和每一个

protected final Map<string> mappedStatements = new StrictMap<mappedstatement>("Mapped Statements collection");</mappedstatement></string>

因此MyBatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。

从这个角度考虑,为了避免MyBatis二级缓存中数据量过大导致内存溢出,MyBatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。

 

MyBatis二级缓存实例化过程

接着看一下MyBatis二级缓存实例化的过程,代码位于XmlMapperBuilder的cacheElement方法中:

 1 private void cacheElement(XNode context) throws Exception { 2     if (context != null) { 3       String type = context.getStringAttribute("type", "PERPETUAL"); 4       Class extends Cache> typeClass = typeAliasRegistry.resolveAlias(type); 5       String eviction = context.getStringAttribute("eviction", "LRU"); 6       Class extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction); 7       Long flushInterval = context.getLongAttribute("flushInterval"); 8       Integer size = context.getIntAttribute("size"); 9       boolean readWrite = !context.getBooleanAttribute("readOnly", false);10       boolean blocking = context.getBooleanAttribute("blocking", false);11       Properties props = context.getChildrenAsProperties();12       builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);13     }14 }

这里分别取中配置的各个属性,关注一下两个默认值:

  1. type表示缓存实现,默认是PERPETUAL,根据typeAliasRegistry中注册的,PERPETUAL实际对应PerpetualCache,这和MyBatis一级缓存是一致的

  2. eviction表示淘汰算法,默认是LRU算法

第3行~第11行拿到了所有属性,那么调用12行的useNewCache方法创建缓存:

 1 public Cache useNewCache(Class extends Cache> typeClass, 2       Class extends Cache> evictionClass, 3       Long flushInterval, 4       Integer size, 5       boolean readWrite, 6       boolean blocking, 7       Properties props) { 8     Cache cache = new CacheBuilder(currentNamespace) 9         .implementation(valueOrDefault(typeClass, PerpetualCache.class))10         .addDecorator(valueOrDefault(evictionClass, LruCache.class))11         .clearInterval(flushInterval)12         .size(size)13         .readWrite(readWrite)14         .blocking(blocking)15         .properties(props)16         .build();17     configuration.addCache(cache);18     currentCache = cache;19     return cache;20 }

这里又使用了建造者模式,跟一下第16行的build()方法,在此之前该传入的参数都已经传入了CacheBuilder:

 1 public Cache build() { 2     setDefaultImplementations(); 3     Cache cache = newBaseCacheInstance(implementation, id); 4     setCacheProperties(cache); 5     // issue #352, do not apply decorators to custom caches 6     if (PerpetualCache.class.equals(cache.getClass())) { 7       for (Class extends Cache> decorator : decorators) { 8         cache = newCacheDecoratorInstance(decorator, cache); 9         setCacheProperties(cache);10       }11       cache = setStandardDecorators(cache);12     } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {13       cache = new LoggingCache(cache);14     }15     return cache;16 }

第3行的代码,构建基础的缓存,implementation指的是type配置的值,这里是默认的PerpetualCache。

第6行的代码,如果是PerpetualCache,那么继续装饰(又是装饰器模式,可以数数这几篇MyBatis源码解析的文章里面出现了多少次装饰器模式了),这里的装饰是根据eviction进行装饰,到这一步,给PerpetualCache加上了LRU的功能。

第11行的代码,继续装饰,这次MyBatis将它命名为标准装饰,setStandardDecorators方法实现为:

 1 private Cache setStandardDecorators(Cache cache) { 2     try { 3       MetaObject metaCache = SystemMetaObject.forObject(cache); 4       if (size != null && metaCache.hasSetter("size")) { 5         metaCache.setValue("size", size); 6       } 7       if (clearInterval != null) { 8         cache = new ScheduledCache(cache); 9         ((ScheduledCache) cache).setClearInterval(clearInterval);10       }11       if (readWrite) {12         cache = new SerializedCache(cache);13       }14       cache = new LoggingCache(cache);15       cache = new SynchronizedCache(cache);16       if (blocking) {17         cache = new BlockingCache(cache);18       }19       return cache;20     } catch (Exception e) {21       throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);22     }23 }

这次是根据其它的配置参数来:

  • 如果配置了flushInterval,那么继续装饰为ScheduledCache,这意味着在调用Cache的getSize、putObject、getObject、removeObject四个方法的时候都会进行一次时间判断,如果到了指定的清理缓存时间间隔,那么就会将当前缓存清空

  • 如果readWrite=true,那么继续装饰为SerializedCache,这意味着缓存中所有存储的内存都必须实现Serializable接口

  • 跟配置无关,将之前装饰好的Cache继续装饰为LoggingCache与SynchronizedCache,前者在getObject的时候会打印缓存命中率,后者将Cache接口中所有的方法都加了Synchronized关键字进行了同步处理

  • 如果blocking=true,那么继续装饰为BlockingCache,这意味着针对同一个CacheKey,拿数据与放数据、删数据是互斥的,即拿数据的时候必须没有在放数据、删数据

Cache全部装饰完毕,返回,至此MyBatis二级缓存生成完毕。

最后说一下,MyBatis支持三种类型的二级缓存:

  • MyBatis默认的缓存,type为空,Cache为PerpetualCache

  • 自定义缓存

  • 第三方缓存

从build()方法来看,后两种场景的Cache,MyBatis只会将其装饰为LoggingCache,理由很简单,这些缓存的定期清除功能、淘汰过期数据功能开发者自己或者第三方缓存都已经实现好了,根本不需要依赖MyBatis本身的装饰。

 

MyBatis二级缓存带来的问题

补充一个内容,MyBatis二级缓存使用的在某些场景下会出问题,来看一下为什么这么说。

假设我有一条select语句(开启了二级缓存):

select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

對於tableA與tableB的操作定義在兩個Mapper中,分別叫做MapperA與MapperB,也就是它們屬於兩個命名空間,如果此時啟用快取:

  1. MapperA中執行上述sql語句查詢這6個欄位

  2. # #tableB更新了col1與col2兩個欄位

  3. #MapperA再次執行上述sql語句查詢這6個欄位(前提是沒有執行過任何insert、delete、update操作)

#此時問題就來了,即使第(2)步驟tableB更新了col1與col2兩個字段,第(3)步MapperA走二級快取查詢到的這6個字段依然是原來的這6個字段的值,因為我們從CacheKey的3組條件來看:

  1. 標籤的id屬性

  2. RowBounds的offset和limit屬性,RowBounds是MyBatis用於處理分頁的一個類,offset預設為0,limit預設為Integer.MAX_VALUE

  3. #

#對MapperA來說,其中的任何條件都沒有變化,自然會將原結果回傳。

這個問題對於MyBatis的二級快取來說是一個無解的問題,因此使用MyBatis二級快取有一個前提:必須保證所有的增刪改查都在同一個命名空間下才行

以上是【MyBatis源碼解析】MyBatis一二級緩存的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?如何將Maven或Gradle用於高級Java項目管理,構建自動化和依賴性解決方案?Mar 17, 2025 pm 05:46 PM

本文討論了使用Maven和Gradle進行Java項目管理,構建自動化和依賴性解決方案,以比較其方法和優化策略。

如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?如何使用適當的版本控制和依賴項管理創建和使用自定義Java庫(JAR文件)?Mar 17, 2025 pm 05:45 PM

本文使用Maven和Gradle之類的工具討論了具有適當的版本控制和依賴關係管理的自定義Java庫(JAR文件)的創建和使用。

如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?如何使用咖啡因或Guava Cache等庫在Java應用程序中實現多層緩存?Mar 17, 2025 pm 05:44 PM

本文討論了使用咖啡因和Guava緩存在Java中實施多層緩存以提高應用程序性能。它涵蓋設置,集成和績效優勢,以及配置和驅逐政策管理最佳PRA

如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?如何將JPA(Java持久性API)用於具有高級功能(例如緩存和懶惰加載)的對象相關映射?Mar 17, 2025 pm 05:43 PM

本文討論了使用JPA進行對象相關映射,並具有高級功能,例如緩存和懶惰加載。它涵蓋了設置,實體映射和優化性能的最佳實踐,同時突出潛在的陷阱。[159個字符]

Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Java的類負載機制如何起作用,包括不同的類載荷及其委託模型?Mar 17, 2025 pm 05:35 PM

Java的類上載涉及使用帶有引導,擴展程序和應用程序類負載器的分層系統加載,鏈接和初始化類。父代授權模型確保首先加載核心類別,從而影響自定義類LOA

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器

Safe Exam Browser

Safe Exam Browser

Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。