[{"content":"Mockito学习笔记\n概要 Mockito是一个常见用于mock依赖对象的测试工具，本文重点讲解Mockito的基本使用方法以及实现原理。\n使用方法 @Test public void testMockito() { //mock 一个对象 ArrayList\u0026lt;String\u0026gt; urlList = Mockito.mock(ArrayList.class); /** *打印mock对象名，可以看到是被代理的对象 *java.util.ArrayList$$EnhancerByMockitoWithCGLIB$$c24a9da4 */ System.out.println(urlList.getClass().getName()); // stub打桩 when(urlList.size()).thenReturn(100); when(urlList.get(0)).thenReturn(\u0026quot;abc\u0026quot;); when(urlList.get(1)).thenReturn(\u0026quot;def\u0026quot;); String value0 = urlList.get(0); // abc String value1 = urlList.get(1); // def String value2 = urlList.get(2); // null int size = urlList.size(); // 100 // verify调用次数 verify(urlList, times(2)).size(); //error } 常用的功能主要是stub打桩和verify验证。\nstub用于对mock对象设置的方法和参数，返回给定的数据。\nverify用于检查mock对象方法的执行情况。\n实现原理 本文基于mockito 1.10.19版本\n跟踪Mockito.mock方法，得到MockitoCore的mock方法\npublic \u0026lt;T\u0026gt; T mock(Class\u0026lt;T\u0026gt; typeToMock, MockSettings settings) { if (!MockSettingsImpl.class.isInstance(settings)) { throw new IllegalArgumentException( \u0026quot;Unexpected implementation of '\u0026quot; + settings.getClass().getCanonicalName() + \u0026quot;'\\n\u0026quot; + \u0026quot;At the moment, you cannot provide your own implementations that class.\u0026quot;); } MockSettingsImpl impl = MockSettingsImpl.class.cast(settings); MockCreationSettings\u0026lt;T\u0026gt; creationSettings = impl.confirm(typeToMock); // 创建mock对象 T mock = mockUtil.createMock(creationSettings); mockingProgress.mockingStarted(mock, typeToMock); return mock; } 跟踪createMock方法，到MockUtil中的createMock：\npublic \u0026lt;T\u0026gt; T createMock(MockCreationSettings\u0026lt;T\u0026gt; settings) { // 创建handler MockHandler mockHandler = new MockHandlerFactory().create(settings); // 调用cglib创建mock对象 T mock = mockMaker.createMock(settings, mockHandler); Object spiedInstance = settings.getSpiedInstance(); if (spiedInstance != null) { new LenientCopyTool().copyToMock(spiedInstance, mock); } return mock; } 通过cglib创建mock对象：\npublic \u0026lt;T\u0026gt; T createMock(MockCreationSettings\u0026lt;T\u0026gt; settings, MockHandler handler) { InternalMockHandler mockitoHandler = cast(handler); new AcrossJVMSerializationFeature().enableSerializationAcrossJVM(settings); return new ClassImposterizer(new InstantiatorProvider().getInstantiator(settings)).imposterise( new MethodInterceptorFilter(mockitoHandler, settings), settings.getTypeToMock(), settings.getExtraInterfaces()); } Mockito使用cglib这个框架来生成代理类代码，然后用ClassLoader进行加载，将MethodInterceptorFilter作为代理类的拦截器。\n@Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { /** .... **/ Invocation invocation = new InvocationImpl(proxy, mockitoMethod, args, SequenceNumber.next(), realMethod); // 最终调用InternalMockHandler.handle方法 return handler.handle(invocation); } stub stub可以指定方法返回值，我们跟踪when方法来看看它的实现。\nMockitoCore.when方法\npublic \u0026lt;T\u0026gt; OngoingStubbing\u0026lt;T\u0026gt; when(T methodCall) { // 标记stub开始 mockingProgress.stubbingStarted(); return (OngoingStubbing) stub(); } public IOngoingStubbing stub() { IOngoingStubbing stubbing = mockingProgress.pullOngoingStubbing(); if (stubbing == null) { mockingProgress.reset(); reporter.missingMethodInvocation(); } return stubbing; } when方法就是返回上次mock方法调用封装好的OngoingStubbing。\nthenReturn // OngoingStubbingImpl public OngoingStubbing\u0026lt;T\u0026gt; thenAnswer(Answer\u0026lt;?\u0026gt; answer) { if(!invocationContainerImpl.hasInvocationForPotentialStubbing()) { new Reporter().incorrectUseOfApi(); } invocationContainerImpl.addAnswer(answer); return new ConsecutiveStubbing\u0026lt;T\u0026gt;(invocationContainerImpl); } // InvocationContainerImpl public void addAnswer(Answer answer, boolean isConsecutive) { // 获取stub的调用 Invocation invocation = invocationForStubbing.getInvocation(); mockingProgress.stubbingCompleted(invocation); AnswersValidator answersValidator = new AnswersValidator(); answersValidator.validate(answer, invocation); synchronized (stubbed) { if (isConsecutive) { // stubbed本质是一个LinkedList,这里把stub的 // 调用（invocation）和返回（answer）绑定，如果匹配到调用，就返回绑定值 stubbed.getFirst().addAnswer(answer); } else { stubbed.addFirst(new StubbedInvocationMatcher(invocationForStubbing, answer)); } } } 如何匹配调用方法呢？通过handle方法，将调用与stubbed链表进行匹配查询，如果有相同的调用，就返回该绑定值\npublic StubbedInvocationMatcher findAnswerFor(Invocation invocation) { synchronized (stubbed) { // 查询匹配的调用 for (StubbedInvocationMatcher s : stubbed) { if (s.matches(invocation)) { s.markStubUsed(invocation); invocation.markStubbed(new StubInfoImpl(s)); return s; } } } return null; } verify MockitoCore.verify\npublic \u0026lt;T\u0026gt; T verify(T mock, VerificationMode mode) { if (mock == null) { reporter.nullPassedToVerify(); } else if (!mockUtil.isMock(mock)) { reporter.notAMockPassedToVerify(mock.getClass()); } mockingProgress.verificationStarted(new MockAwareVerificationMode(mock, mode)); return mock; } mockito提供了多种VerificationMode,这里使用的是Times，实现了VerificationMode类，用于统计方法执行次数。\n// Times public void verify(VerificationData data) { if (wantedCount \u0026gt; 0) { MissingInvocationChecker missingInvocation = new MissingInvocationChecker(); missingInvocation.check(data.getAllInvocations(), data.getWanted()); } NumberOfInvocationsChecker numberOfInvocations = new NumberOfInvocationsChecker(); numberOfInvocations.check(data.getAllInvocations(), data.getWanted(), wantedCount); } // NumberOfInvocationChecker public void check(List\u0026lt;Invocation\u0026gt; invocations, InvocationMatcher wanted, int wantedCount) { List\u0026lt;Invocation\u0026gt; actualInvocations = finder.findInvocations(invocations, wanted); int actualCount = actualInvocations.size(); if (wantedCount \u0026gt; actualCount) { Location lastInvocation = finder.getLastLocation(actualInvocations); reporter.tooLittleActualInvocations(new Discrepancy(wantedCount, actualCount), wanted, lastInvocation); } else if (wantedCount == 0 \u0026amp;\u0026amp; actualCount \u0026gt; 0) { Location firstUndesired = actualInvocations.get(wantedCount).getLocation(); reporter.neverWantedButInvoked(wanted, firstUndesired); } else if (wantedCount \u0026lt; actualCount) { Location firstUndesired = actualInvocations.get(wantedCount).getLocation(); reporter.tooManyActualInvocations(wantedCount, actualCount, wanted, firstUndesired); } invocationMarker.markVerified(actualInvocations, wanted); } 如果判断失败，则抛出异常\n","permalink":"https://zhongyi-byte.github.io/posts/tech/mockito%E7%AC%94%E8%AE%B0/","summary":"Mockito学习笔记 概要 Mockito是一个常见用于mock依赖对象的测试工具，本文重点讲解Mockito的基本使用方法以及实现原理。 使用","title":"Mockito笔记"},{"content":"搜索引擎的核心在于根据**关键词（term）找到相关文档（document）**的能力。为了实现这一目的，lucene针对每个term维护了一个倒排链（posting list），即包含该term的docId列表。\n什么是倒排索引 正排索引 即docId - document的键值对\ndocId name age 1 Jack 20 2 Bob 22 3 Jack 20 4 Alice 20 5 Jack 19 6 Bob 19 类似于mysql索引，根据docId可以查询到相应的文档信息。\n倒排索引 倒排索引是关键词（term）到文档id列表的映射关系。例如针对上面这张表格，可以建立两个倒排索引，分别是name和age Index。\nname index\nterm dictionary posting list Jack [1,3,5] Bob [2,6] Alice [4] age index\nterm dictionary posting list 19 [5,6] 20 [1,3,4] 22 [2] 这样，针对每一个关键词term，都可以查询出对应的docId列表。\n对term dictionary进行排序后，得到一个有序的term列表，可以通过二分法进行查询，这样的时间复杂度是O(logN)，空间复杂度是O(N*len(key))，和term数量成正比。当term数量极为庞大，内存里也放不下时，需要对term进行索引来提高效率。\nTerm Index Trie树 假设字符的种数有m个，有若干个长度为n的字符串构成了一个Trie树，则每个节点的出度为m（即每个节点的可能子节点数量为m），Trie树的高度为n。很明显我们浪费了大量的空间来存储字符，此时Trie树的最坏空间复杂度为O(m^n)。也正由于每个节点的出度为m，所以我们能够沿着树的一个个分支高效的向下逐个字符的查询，而不是遍历所有的字符串来查询，此时Trie树的最坏时间复杂度为O(n)。这正是空间换时间的体现，也是利用公共前缀降低查询时间开销的体现。\n相较于hash表，trie树支持动态查询，可以进行前缀，后缀，范围查询，在搜索场景下明显优于hash表。\nFSA FSA是一个FSM(有限状态机)的一种，特性如下:\n* 确定：意味着指定任何一个状态，只可能最多有一个转移可以访问到。\n* 无环： 不可能重复遍历同一个状态\n* 接收机：有限状态机只“接受”特定的输入序列，并终止于final状态。\n事实上字典树也是一种FSA，但TRIE树只共享前缀，而FSA不仅共享前缀还共享后缀。假设我们用星期一、二、四去掉共同的day后缀组成一个Set: mon,tues,thurs。相应的TRIE是这样的，只共享了前缀。\nTRIE有重复的3个final state:3、8、11。 而8、11都是s转移，是可以合并的，FSA共享后缀后：\n进一步降低了存储空间。\nFST FST类似于FSA，区别在于给定一个key除了能回答是否存在，还能输出一个关联的值。\n依然以mon,tues,thurs这个集合为例，以星期天为一周的第1天，我们的mon对应2，tues对应3，thurs对应5，那么最后生成的FST就如下图所示，/后表示要输出的值：\n路径m-\u0026gt;o-\u0026gt;n将会输出2\n路径t-\u0026gt;u-\u0026gt;e-\u0026gt;s将会输出3\n路径t-\u0026gt;h-\u0026gt;u-\u0026gt;r-\u0026gt;s将会输出5\n当然，满足上诉输出条件并不只有这一种FST，比如可以m -\u0026gt; o -\u0026gt; n的路径上的m/2改成o/1\\`n/1`.\n关键点在于：每一个key,都在FST中对应一个唯一的路径。因此，对于任何一个特定的key，总会有一些value的组合使得路径是唯一的，我们需要做的就是如何来在转移中分配这些组合。如何构建FST留待下周。\n通过FST构建term index，可以大大降低需要的存储空间，使term index可以被缓存到内存中，查询时只需要在匹配到term时进行一次磁盘访问，降低了磁盘操作时间。\n联合索引查询 例如对上表进行 name == \u0026lsquo;xx\u0026rsquo; and age == \u0026lsquo;yy\u0026rsquo; 的查询，在mysql数据库中需要对age和name分别建立索引，查询结果后进行合并，或者建立联合索引。\n而对于lucene来说，实际上是根据term找到相应的倒排链，再对两条倒排链进行合并。倒排链的数据结构有两种解决方案，分别是skiplist（跳表）和bitset\nSkip List SkipList有以下几个特征：\n元素排序的，对应到我们的倒排链，lucene是按照docid进行排序，从小到大。\n跳跃有一个固定的间隔，这个是需要建立SkipList的时候指定好，例如下图以间隔是3\nSkipList的层次，这个是指整个SkipList有几层 有了这个SkipList以后比如我们要查找docid=12，原来可能需要一个个扫原始链表，1，2，3，5，7，8，10，12。有了SkipList以后先访问第一层看到是然后大于12，进入第0层走到3，8，发现15大于12，然后进入原链表的8继续向下经过10和12。\nSkipList本质上是在有序的链表上实现实现二分查找，它能有效的提升链表的查找效率，其时间复杂度为O(logn)（其中n为链表长度）。简单说SkipList优化了Postings的随机查找的性能问题。\n假如我们有下面三个倒排链需要进行合并。\n在lucene中会采用下列顺序进行合并：\n在termA开始遍历，得到第一个元素docId=1\nSet currentDocId=1\n在termB中 search(currentDocId) = 1 (返回大于等于currentDocId的一个doc),\n因为currentDocId ==1，继续\n如果currentDocId 和返回的不相等，执行2，然后继续\n到termC后依然符合，返回结果\ncurrentDocId = termC的nextItem\n然后继续步骤3 依次循环。直到某个倒排链到末尾。\n整个合并步骤我可以发现，如果某个链很短，会大幅减少比对次数，并且由于SkipList结构的存在，在某个倒排中定位某个docid的速度会比较快不需要一个个遍历。可以很快的返回最终的结果。从倒排的定位，查询，合并整个流程组成了lucene的查询过程，和传统数据库的索引相比，lucene合并过程中的优化减少了读取数据的IO，倒排合并的灵活性也解决了传统索引较难支持多条件查询的问题。\nbitset bitset 是一种很直观的数据结构，对应 posting list 如：[1,3,4,7,10]\n对应的 bitset 就是： [1,0,1,1,0,0,1,0,0,1] ，每个文档按照文档 id 排序对应其中的一个 bit。Bitset 自身就有压缩的特点，其用一个 byte 就可以代表 8 个文档。所以 100 万个文档只需要 12.5 万个 byte。但是考虑到文档可能有数十亿之多，在内存里保存 bitset 仍然是很奢侈的事情。而且对于个每一个 filter 都要消耗一个 bitset，比如 age=18 缓存起来的话是一个 bitset，18\u0026lt;=age\u0026lt;25 是另外一个 filter 缓存起来也要一个 bitset。 Lucene 会对bitset再进行压缩，称之为 Roaring Bitmap。压缩的思路其实很简单。与其保存 100 个 0，占用 100 个 bit。还不如保存 0 一次，然后声明这个 0 重复了 100 遍。\n这两种合并使用索引的方式都有其用途。Elasticsearch 对其性能有详细的对比。简单来说对于简单的相等条件的过滤缓存成纯内存的 bitset 还不如需要访问磁盘的 skip list 的方式要快。\n","permalink":"https://zhongyi-byte.github.io/posts/tech/lucene%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"搜索引擎的核心在于根据**关键词（term）找到相关文档（document）**的能力。为了实现这一目的，lucene针对每个term维护了","title":"Lucene学习笔记"},{"content":"花了两个晚上的时间搭建好了这个博客。记录下过程：\n参考sulv的建站教程，用他的代码生成了我的第一版博客。然后根据个人信息作了一些配置 通过docker搭建twikoo服务，提供了评论功能，不过目前docker运行在本地机器上，时不时就会关掉，将来换到云服务器上。\n不过我对twikoo不太满意，我预期中的评论应该能和github账号联动，而不是一个自定义的账号。 将博客部署到github page上，暂且先用着().github.io的域名，以后换成自己的。 利用github action，实现博客自动发布部署。 将评论替换为giscus，实现与github联动。\n根据个人信息配置好后，将script语句复制到/layouts/partials/comments.html中。 \u0026lt;!-- giscus --\u0026gt; \u0026lt;div\u0026gt; \u0026lt;div id=\u0026#34;tcomment\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;\u0026#34; //填写自己的仓库地址 data-repo-id=\u0026#34;\u0026#34; //repoId data-category=\u0026#34;Announcements\u0026#34; //评论类型，参照github的discussion data-category-id=\u0026#34;DIC_kwDOIAW3Os4CReNg\u0026#34; data-mapping=\u0026#34;url\u0026#34; data-strict=\u0026#34;0\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-emit-metadata=\u0026#34;0\u0026#34; data-input-position=\u0026#34;top\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;zh-CN\u0026#34; //中文 data-loading=\u0026#34;lazy\u0026#34; //懒加载 crossorigin=\u0026#34;anonymous\u0026#34; async\u0026gt; \u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; 以上语句可以在 gitcus中自动生成。 调用上述giscus代码的位置：layouts/_default/single.html\n\u0026lt;article class=\u0026#34;post-single\u0026#34;\u0026gt; // 这里是默认的其他代码 // giscus，一般只需要复制以下3行代码，加上其他代码是为了帮助读者确认代码添加的位置 {{- if (.Param \u0026#34;comments\u0026#34;) }} {{- partial \u0026#34;comments.html\u0026#34; . }} {{- end }} \u0026lt;/article\u0026gt; 参考 Hugo博客搭建 Hugo + GitHub Action，搭建你的博客自动发布系统 gitcus ","permalink":"https://zhongyi-byte.github.io/posts/blog/%E5%BB%BA%E7%AB%99%E8%AE%B0%E5%BD%95/","summary":"花了两个晚上的时间搭建好了这个博客。记录下过程： 参考sulv的建站教程，用他的代码生成了我的第一版博客。然后根据个人信息作了一些配置 通过do","title":"建站记录"},{"content":"","permalink":"https://zhongyi-byte.github.io/posts/life/%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%86%99%E5%8D%9A%E5%AE%A2/","summary":"","title":"为什么要写博客"},{"content":"","permalink":"https://zhongyi-byte.github.io/posts/blog/blog/","summary":"","title":"Blog"},{"content":"","permalink":"https://zhongyi-byte.github.io/posts/life/life/","summary":"","title":"Life"},{"content":"","permalink":"https://zhongyi-byte.github.io/posts/read/read/","summary":"","title":"Read"},{"content":"","permalink":"https://zhongyi-byte.github.io/posts/tech/tech/","summary":"","title":"Tech"},{"content":" Sulv\u0026#39;s Blog 一个记录技术、阅读、生活的博客 👉友链格式 名称： Sulv\u0026rsquo;s Blog 网址： https://www.sulvblog.cn 图标： https://www.sulvblog.cn/img/Q.gif 描述： 一个记录技术、阅读、生活的博客 👉友链申请要求 秉承互换友链原则、文章定期更新、不能有太多广告、个人描述字数控制在15字内\n👉Hugo博客交流群 787018782\n","permalink":"https://zhongyi-byte.github.io/links/","summary":"Sulv\u0026#39;s Blog 一个记录技术、阅读、生活的博客 👉友链格式 名称： Sulv\u0026rsquo;s Blog 网址： https://www.sulvblog.cn 图标： https://www.sulvblog.cn/img/Q.gif 描述： 一个记录技术、阅读、生活的博客 👉友链申请要求 秉承互换友链原则、文","title":"🤝友链"},{"content":"关于我\n英文名: Zyi 职业: 程序员 运动: 羽毛球，射箭，骑车 ","permalink":"https://zhongyi-byte.github.io/about/","summary":"关于我 英文名: Zyi 职业: 程序员 运动: 羽毛球，射箭，骑车","title":"🙋🏻‍♂️关于"}]