设计模式之美笔记7 女爷i 2022-11-25 13:19 186阅读 0赞 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 ### 文章目录 ### * * 实战1:id生成器的重构 * * 1. 需求背景 * 2. 代码实现 * 3. 如何发现代码质量问题 * 4. 制定重构计划 * * 第一轮重构:提高代码的可读性 * 第二轮重构:提高代码的可测试性 * 第三轮重构:编写完善的单元测试 * 第四轮重构:添加注释 * 5. 异常的处理 * * 1. 方法出错应该返回什么 * 2. 如何处理方法抛出的异常 * 3. id生成器代码的出错处理 * 4. 重构generate()方法 * 5. 重构getLastFieldOfHostName()方法 * 6. 重构getLastSubstrSplittedByDot(String hostName)方法 * 7. 重构generateRandomAlphameric()方法 * 6.总结 * 实战2:完善性能计数器 * * 1. 回顾版本1 * 2. 问题 * 3. 重构版本1 * 4. 代码review * 版本3 * * 2. 功能的完善 * 3. 非功能的完善 * 2. 性能 * 3. 扩展性 * 4. 容错性 * 5. 通用性 ## 实战1:id生成器的重构 ## ### 1. 需求背景 ### id中文译为标识identifier,如身份证、商品条码、二维码、车牌号、驾照号等。软件开发中,id常用来表示一些业务信息的唯一标识,如订单的单号或数据库的唯一主键。 假设正在参与后端业务系统的开发,为方便在请求出错时排查问题,编写代码时会在关键路径打印日志。某个请求出错后,希望能搜索出这个请求对应的所有日志,以此查找问题原因。实际上,日志文件中,不同的请求的日志会交织在一起。如果没有东西来标识哪些日志属于同一个请求,就无法关联同一个请求的所有日志。 听起来像微服务的调用链追踪,不过,微服务的是服务间的追踪,我们实现的是服务内的追踪。借鉴微服务调用链追踪的实现思路,给每个请求分配一个唯一id,并且保存到请求的上下文context中,如处理请求的工作线程的局部变量中。在java中可将id存储到servlet线程的ThreadLocal中,或者利用slf4j日志框架的MDC(Mapped Diagnostic Contexts)来实现(底层也是基于线程的ThreadLocal)。每次打印日志,从请求上下文取出请求id,跟日志一块输出,这样每个请求的所有日志都包含同样的请求id信息了。 ### 2. 代码实现 ### 一个简单的id生成器: public class IdGenerator { private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class); public static String generate(){ String id = ""; try{ String hostName = InetAddress.getLocalHost().getHostName(); String[] tokens = hostName.split("\\."); if (tokens.length > 0){ hostName = tokens[tokens.length - 1]; } char[] randomChars = new char[8]; int count = 0; Random random = new Random(); while (count < 8){ int randomAscii = random.nextInt(122); if (randomAscii >= 48 && randomAscii <= 57){ randomChars[count] = (char)('0' + (randomAscii - 48)); count++; }else if (randomAscii >=65 && randomAscii <= 90){ randomChars[count] = (cahr)('A' +(randomAscii - 65)); count++; }else if (randomAscii >= 97 && randomAscii <= 122){ randomChars[count] = (char)('a' +(randomAscii - 97)); count++; } } id = String.format("%s-%d-%s",hostName,System.currentTimeMillis(),new String(randomChars)); }catch (UnknownHostException e){ logger.warn("Failed to get the host name.",e); } return id; } } 整个id由三部分组成: * 本机名的最后一个字段 * 当前时间戳,精确到毫秒 * 8位随机字符串,包含大小字母和数字 对于日志追踪来说,重复概率极低,可接受。 但是这样一份代码,只能说能用,有很多值得优化的地方,如何将60分及格代码优化到80、90分呢? ### 3. 如何发现代码质量问题 ### 具体细节,可从以下几个方面审视代码: * 目录设置是否合理、模块划分是否清晰、代码结构是否满足高内聚、低耦合 * 是否遵循经典的设计原则和设计思想(SOLID、DRY、KISS、YAGNI、LOD等) * 设计模式是否应用得当?是否有过度设计 * 代码是否易扩展,如果添加新功能,是否易实现 * 代码是否可复用?是否可复用已有的项目代码或类库?是否有重复造轮子? * 代码是否易测试?单元测试是否全面覆盖各种正常和异常的情况 * 代码是否易读?是否符合编程规范? 除了上述的通用的关注点,针对业务本身特有的功能和非功能需求,还有些checklist,如下: * 代码是否实现了预期的业务需求? * 逻辑是否正确?是否处理各种异常情况 * 日志打印是否得当?是否方便debug排查问题 * 接口是否易用?是否支持幂等、事务等 * 代码是否存在并发问题?是否线程安全 * 性能是否有优化空间?如SQL、算法是否可优化? * 是否有安全漏洞?如输入、输出校验是否全面? 对照上面的checklist,看id生成器的代码有哪些问题。 首先,IdGenerator代码简单,只有一个类,不涉及到目录设置、模块划分、代码结构等问题,也不违反设计原则,没有用设计模式,不存在不合理使用和过度设计的问题。 其次,IdGenerator设计为实现类而非接口,调用者直接依赖实现而非接口,违反基于接口而非实现编程的设计思想。实际问题不大。但是,如果公司项目中需要同时存在两种ID生成算法,也就是同时存在两个实现类,如需要将这个框架给更多系统用。这时就要定义为接口了。 再次,将IdGenerator的generate()方法定义为静态方法,会影响使用该方法的代码的可测试性。同时,generate()方法的代码实现依赖运行环境(本机名)、时间函数、随机函数,generate()方法的本身可测试性也不好,要做较大的重构。此外,也没编写单元测试代码,要补充。 最后,虽然只包含一个方法,方法的代码行数也不多,但代码的可读性不好,特别是随机字符串生成的部分,一方面,代码没有注释,生成算法比较难读懂;另一个方面,代码有很多魔法数,影响代码的可读性。 再对照业务本身的功能和非功能需求,审视代码: 虽然生成的id并非绝对唯一,但对于追踪打印日志来说,可接受,满足预期的业务需求。不过,获取hostName部分代码逻辑有点问题,没有处理“hostName为空”的情况。此外,尽管对获取不到本机名做了异常处理,但对异常处理是在IdGenerator内部吞掉,打印一条报警日志,并没有继续向上抛出,是否得当? 该代码日志打印得当,日志描述准确,方便debug,只暴露一个generate()接口供使用者调用,不存在不宜用问题。方法的代码中并没有涉及到共享变量,代码线程安全,多线程下不存在并发问题。 性能方面,ID的生成不依赖外部存储,内存中生成,日志打印频率也并不高,性能可以应对目前的场景。不过每次生成id都要获取本机名,较为耗时。还有randomAscii的范围是`0~122`,但可用数字只包含三段子区间(`0~9`,`a~z`,`A~Z`),极端情况下会生成很多三段区间之外的无效的数字,需要循环多次才能生成随机字符串,可优化。 具体的代码方面,在generate()方法的while循环里,三个if语句内的代码很相似,而且实现稍微复杂,可进一步简化,将三个if合并。 ### 4. 制定重构计划 ### 循环渐进、小步快跑。重构每次改动一点点。分为四次重构完成: * 第一轮重构:提高代码的可读性 * 第二轮重构:提高代码的可测试性 * 第三轮重构:编写完善的单元测试 * 第四轮重构:所有重构完成后添加注释 #### 第一轮重构:提高代码的可读性 #### 先解决最明显、最急需改进的代码可读性问题: * hostName变量不该被重复使用,尤其当两次使用的含义不同的时候 * 将获取hostName的代码抽离出来,定义为getLastFieldOfHostName()方法 * 删除代码的魔法数,如57、90、97、122 * 将随机数生成的代码抽离出来,定义为generateRandomAlphameric()方法 * generate()方法的三个if逻辑重复,且实现过于复杂,需要简化 * 对IdGenerator类重命名,且抽象出对应的接口 对于生成ID生成器的代码,有下面三种类的命名方式: <table> <thead> <tr> <th></th> <th>接口</th> <th>实现类</th> </tr> </thead> <tbody> <tr> <td>命名方式1</td> <td>IdGenerator</td> <td>LogTraceIdGenerator</td> </tr> <tr> <td>命名方式2</td> <td>LogTraceIdGenerator</td> <td>HostNameMillisIdGenerator</td> </tr> <tr> <td>命名方式3</td> <td>LogTraceIdGenerator</td> <td>RandomIdGenerator</td> </tr> </tbody> </table> 这三种命名方式: 第一种,最先想到,但如果考虑以后两个类的使用和扩展,就不合理了。 首先,如果扩展新的日志ID生成算法,也就是创建另一个新的实现类,原来的叫LogTraceIdGenerator,命名过于通用,新的实现类不好取名,无法取跟LogTraceIdGenerator平行的名字。 其次,假设没有日志ID扩展需求,但要扩展其他业务的ID生成算法,如UserIdGenerator、OrderIdGenerator,第一种名字也不合理。基于接口而非实现编程,主要目的是为了方便后续灵活的替换实现类。而LogTraceIdGenerator、UserIdGenerator、OrderIdGenerator三个类是完全不同的业务,不存在互相替换的场景。 第二种呢?也不合理。LogTraceIdGenerator合理,但HostNameMillisIdGenerator暴露了太多实现细节,只要代码稍微改动,就可能需要改名字,才能匹配实现。 第三种最推荐,目前的ID生成器代码实现上,生成的ID是一个随机ID,命名较为合理,如果之后要实现一个递增有序的ID生成算法,可命名为SequenceIdGenerator。 更好的命名是:抽象出两个接口,一个是IdGenerator,一个是LogTraceIdGenerator,LogTraceIdGenerator继承IdGenerator。实现类实现接口IdGenerator,命名为RandomIdGenerator、SequenceIdGenerator这样,实现类可复用到很多业务模块,如用户、订单。 重构后的代码: public class RandomIdGenrator implements LogTraceIdGenrator{ private static final Logger logger = LoggerFactory.getLogger(RandomIdGenrator.class); @Override public String generate() { String substrOfHostName = getLastfiledOfHostName(); long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s",substrOfHostName,currentTimeMillis,randomString); return id; } private String getLastfiledOfHostName(){ String substrOfHostName = null; try{ String hostName = InetAddress.getLocalHost().getHostName(); String[] tokens = hostName.split("\\."); substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; }catch (UnknownHostException e){ logger.warn("Failed to get the host name.",e); } return substrOfHostName; } private String generateRandomAlphameric(int length){ char[] randomChars = new char[length]; int count = 0; Random random = new Random(); while (count < length){ int maxAscii = 'z'; int randomAscii = random.nextInt(maxAscii); boolean isDigit = randomAscii >= '0' && randomAscii <= '9'; boolean isUppercase = randomAscii >= 'A' && randomAscii <='Z'; boolean isLowercase = randomAscii >= 'a' && randomAscii <='z'; if (isDigit || isUppercase || isLowercase){ randomChars[count] =(char)(randomAscii); ++count; } } return new String(randomChars); } } //代码使用举例 LogTraceIdGenrator logTraceIdGenrator = new RandomIdGenrator(); #### 第二轮重构:提高代码的可测试性 #### 可测试性包含两个方面: * generate()方法定义为静态方法,影响使用该方法的代码的可测试性 * generate()方法的代码实现依赖运行环境(本机名)、时间函数等未决行为,本身可测试性不好 对第一点,在第一轮重构已解决。改为了普通方法。 对于第二点,需要再重构,主要包含几点改动: * 从getLastfiledOfHostName()方法中,将逻辑较为复杂的代码剥离出来,定义为getLastSubstrSplittedByDot()方法,剥离后,方法简单,不用测试。重点测getLastSubstrSplittedByDot()即可。 * 将generateRandomAlphameric()和getLastSubstrSplittedByDot()两个方法的访问权限设置为protected,目的是可直接在单元测试通过对象来调用这两个方法进行测试 * 给generateRandomAlphameric()和getLastSubstrSplittedByDot()两个方法添加Google Guava的annotation`@VisibleForTesting`,这个注解只是起到标识的作用,说明方法本该是private访问权限,提升到protected,只是为了测试,只能用于单元测试 public class RandomIdGenrator implements IdGenerator{ private static final Logger logger = LoggerFactory.getLogger(RandomIdGenrator.class); @Override public String generate() { String substrOfHostName = getLastfiledOfHostName(); long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s",substrOfHostName,currentTimeMillis,randomString); return id; } private String getLastfiledOfHostName(){ String substrOfHostName = null; try{ String hostName = InetAddress.getLocalHost().getHostName(); substrOfHostName = getLastSubstrSplittedByDot(hostName); }catch (UnknownHostException e){ logger.warn("Failed to get the host name.",e); } return substrOfHostName; } @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName){ String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; } @VisibleForTesting protected String generateRandomAlphameric(int length){ //... } } 打印日志的Logger对象被定义为static final,并在类内部创建,是否影响代码的可测试性?是否应该将Logger对象通过依赖注入的方式注入到类中? 不必,依赖注入之所以提高代码的可测试性,因为这样能通过mock对象替换依赖的真实对象。为什么要mock?因为这个对象参与逻辑执行,但又不可控。不过Logger对象我们只往里面写数据,并不读取数据,不参与业务逻辑的执行,没必要mock Logger对象。 #### 第三轮重构:编写完善的单元测试 #### 代码存在的明显问题已经解决,为代码补全单元测试。RandomIdGenerator类有4个方法: public String generate(); private String getLastfiledOfHostName(); @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName); @VisibleForTesting protected String generateRandomAlphameric(int length) 先看后两个方法,逻辑较复杂,是测试的重点。已经将它们跟不可控的组件(本机名、随机函数、时间函数)进行了隔离,只需要设计测试用例即可。 public class RandomIdGeneratorTest { @Test public void testGetLastSubstrSplittedByDot(){ RandomIdGenrator idGenrator = new RandomIdGenrator(); String actualSubstr = idGenrator.getLastSubstrSplittedByDot("field1.field2.field3"); Assert.assertEquals("field3",actualSubstr); actualSubstr = idGenrator.getLastSubstrSplittedByDot("field1"); Assert.assertEquals("field1",actualSubstr); actualSubstr = idGenrator.getLastSubstrSplittedByDot("field1#field2#field3"); Assert.assertEquals("field1#field2#field3",actualSubstr); } //此单元测试会失败,因为在代码中没有处理hostName为null或空字符串的情况 //之后优化 @Test public void testGetLastSubstrSplittedByDot_nullOrEmpty(){ RandomIdGenrator idGenrator = new RandomIdGenrator(); String actualSubstr = idGenrator.getLastSubstrSplittedByDot(null); Assert.assertNull(actualSubstr); actualSubstr = idGenrator.getLastSubstrSplittedByDot(""); Assert.assertEquals("",actualSubstr); } @Test public void testGenerateRandomAlphameric(){ RandomIdGenrator idGenrator = new RandomIdGenrator(); String actualRandomString = idGenrator.generateRandomAlphameric(6); Assert.assertNotNull(actualRandomString); Assert.assertEquals(6,actualRandomString.length()); for (char c: actualRandomString.toCharArray()){ Assert.assertTrue('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')); } } //此单元测试会失败,因为在代码中没有处理length<=0的情况 //之后优化 @Test public void testGenerateRandomAlphameric_lengthEqualsOrLessThanZero(){ RandomIdGenrator idGenrator = new RandomIdGenrator(); String actualRandomString = idGenrator.generateRandomAlphameric(0); Assert.assertEquals("",actualRandomString); actualRandomString = idGenrator.generateRandomAlphameric(-1); Assert.assertNull(actualRandomString); } } 再看generate()方法,这个方法是唯一暴露给外部使用的public方法,它依赖主机名、随机函数、时间函数,如何测试? 要分情况看,单元测试,测试对象是方法定义的功能,而非具体的实现逻辑。那generate()的功能是什么呢? 针对同一份generate()方法的代码实现,有三种不同的功能定义,对应三种不同的单元测试 1. 如果把generate()方法的功能定义为:生成一个随机唯一id,只要测试多次调用生成的id是否唯一即可 2. 如果把generate()方法的功能定义为:生成一个只包含数字、大小写字母和中划线的唯一id,不仅测试id的唯一性,还要测试生成id是否只包含数字、大小写字母和中划线 3. 如果把generate()方法的功能定义为:生成唯一id,格式为:\{主机名substr\}-\{时间戳\}-\{8位随机数\}。主机名获取失败时,返回:null-\{时间戳\}-\{8位随机数\}。不仅要测试id的唯一性,还要测试生成的id是否完全符合格式要求 最后看getLastfiledOfHostName()方法,这个方法不容易测试,因为调用静态方法,且这个静态方法依赖运行环境。但这个方法的实现很简单,肉眼可以排除明显的bug。毕竟不是为了写单元测试而写单元测试。 #### 第四轮重构:添加注释 #### 注释需要写:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对方法输入、输出、异常进行说明 /** * Id Generator that is used to generate random IDs. * * <p> * The IDs generated by this class are not absolutely unique, * but the probability of duplication is very low. */ public class RandomIdGenerator implements LogTraceIdGenerator { private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class); /** * Generate the random ID. The IDs may be duplicated only in extreme situation. * * @return a random ID */ @Override public String generate() { //... } /** * Get the local hostname and * extract the last field of the name string splitted by delimiter '.'. * * @return the last field of hostname. Returns null if hostname is not obtained. */ private String getLastfieldOfHostName() { //... } /** * Get the last field of {@hostName} splitted by delemiter '.'. * * @param hostName should not be null * @return the last field of {@hostName}. Returns empty string if {@hostName} is empty string. */ @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName) { //... } /** * Generate random string which * only contains digits, uppercase letters and lowercase letters. * * @param length should not be less than 0 * @return the random string. Returns empty string if {@length} is 0 */ @VisibleForTesting protected String generateRandomAlphameric(int length) { //... } } ### 5. 异常的处理 ### 程序的bug往往出现在一些边界条件和异常情况下,异常处理的好坏直接影响代码的健壮性。全面、合理的处理各种异常能有效减少代码bug,保证代码的质量。 #### 1. 方法出错应该返回什么 #### 返回数据类型,有4种情况 1. 返回错误码 一般在C语言中使用错误码 2. 返回null值 对于get、find、select等查找方法来说,数据不存在,并非是异常情况,是正常行为。对于查找方法,有的还会返回下标位置,如java的indexOf()方法,用来实现在某个字符串中查找另一个子串第一次出现的位置,方法的返回值类型为基本类型int,无法使用null值表示不存在,有两种思路:返回NotFoundException和返回一个特殊值如-1。-1更合理点,也就是说,没有找到是正常行为 3. 返回空对象 有个空对象设计模式,后面会谈到。有两种简单的空对象,就是空字符串和空集合。如果方法返回的数据是字符串类型或集合类型,可以用空字符串""或空集合`Collections.emptyList()`替代。 4. 抛异常 最常用,异常携带很多错误信息,也将正常逻辑和异常逻辑处理区分开,代码可读性更好。对于代码bug如数组越界或不可恢复异常如数据库连接失败,倾向于使用非受检异常。对于可恢复异常、业务异常,如提现金额大于余额的异常,倾向于使用受检异常,明确告知调用者要捕获处理。 #### 2. 如何处理方法抛出的异常 #### 3种: * 直接吞掉 如果底层抛出的异常可恢复,且上层不关心此异常,可吞掉 * 原封不动的re-throw 如果底层抛出的异常,和上层业务相关,直接抛出 * 包装成新的异常re-throw 如果底层抛出的异常跟上层业务无关,重新包装为上层可理解的新异常,抛出 #### 3. id生成器代码的出错处理 #### * 对generate()方法,如果本机名获取失败,方法该返回什么?这样的返回值是否合理? * 对getLastFiledOfHostName()方法,是否该将UnknownHostException异常内部吞掉(try-catch并打印日志)?还是将异常继续往上抛?往上抛出的话,将该异常原封不动的抛出,还是封装为新的异常抛出? * 对getLastSubstrSplittedByDot(String hostName)方法,如果hostName为null或空字符串,方法该返回什么? * 对generateRandomAlphameric(int length)方法,如果length小于0或者等于0,方法该返回什么? #### 4. 重构generate()方法 #### 首先看:对generate()方法,如果本机名获取失败,方法该返回什么?这样的返回值是否合理? id是由三部分组成,时间戳和随机数的生成函数不会出错,只有主机名可能获取失败。目前的代码实现上,主机名获取失败,substrOfHostName为null,generate()方法返回类似"null-16723733647-83332ua1"这样的数据,如果主机名获取失败,substrOfHostName为空字符串,返回类似"-16723733647-83332ua1"这样的数据。 这样是否合理呢?要看具体业务需求,更倾向于明确将异常告知调用者,最好抛出受检异常,而非特殊值。 重构后: public String generate() throws IdGenerationFailureException{ String substrOfHostName = getLastfiledOfHostName(); if (substrOfHostName == null || substrOfHostName.isEmpty()){ throw new IdGenerationFailureException("host name is empty"); } long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s",substrOfHostName,currentTimeMillis,randomString); return id; } #### 5. 重构getLastFieldOfHostName()方法 #### 对getLastFiledOfHostName()方法,是否该将UnknownHostException异常内部吞掉(try-catch并打印日志)?还是将异常继续往上抛?往上抛出的话,将该异常原封不动的抛出,还是封装为新的异常抛出? 目前的处理是返回null值,获取主机名失败会影响后续逻辑的处理,不是期望的,是异常行为,最好抛出异常,而非返回null值。 直接抛还是封装后抛出,要看方法跟异常是否有业务相关性。该方法获取主机名的最后一个字段,UnknownHostException异常标识主机名获取失败,算是业务相关,可以直接将UnknownHostException抛出,不需要重新包裹 重构后: private String getLastfiledOfHostName() throws UnknownHostException{ String substrOfHostName = null; String hostName = InetAddress.getLocalHost().getHostName(); substrOfHostName = getLastSubstrSplittedByDot(hostName); return substrOfHostName; } getLastfiledOfHostName()方法修改后,generate()方法也要做对应的修改,捕获UnknownHostException异常,捕获后怎么处理呢? 按之前分析,id生成失败后,要明确告知调用者,不能在generate()方法中,将UnknownHostException异常吞掉,是否要封装为新异常抛出呢? 要封装为IdGenerateFailureException往上抛出。调用者不care底层的细节。跟上层的业务也无关。 对generate()方法再次重构: public String generate() throws IdGenerationFailureException{ String substrOfHostName = null; try { substrOfHostName = getLastfiledOfHostName(); } catch (UnknownHostException e) { throw new IdGenerationFailureException("host name is empty."); } long currentTimeMillis = System.currentTimeMillis(); String randomString = generateRandomAlphameric(8); String id = String.format("%s-%d-%s",substrOfHostName,currentTimeMillis,randomString); return id; } #### 6. 重构getLastSubstrSplittedByDot(String hostName)方法 #### 对于getLastSubstrSplittedByDot(String hostName)方法,如果hostName为null或者空字符串,应该返回什么? 理论上说,参数传递的正确性应该有程序员保证,无需做null值或者空字符串的判断和特殊处理。调用者不该把null值或空字符串传给getLastSubstrSplittedByDot(),如果传递了就是code bug,需要修复,但万一传了这种,是否做判断呢? 如果方法是private,只在类内部调用,自己保证不会传null值或空字符串即可,不用判断。但如果是public,为保证代码的健壮性,还是做判断。因此,最好加上校验。 对getLastSubstrSplittedByDot(String hostName)方法重构: @VisibleForTesting protected String getLastSubstrSplittedByDot(String hostName){ if (hostName == null || hostName.isEmpty()){ throw IllegalArgumentException("...");//运行时异常 } String[] tokens = hostName.split("\\."); String substrOfHostName = tokens[tokens.length - 1]; return substrOfHostName; } 使用这个方法时,也要保证不传递null值或空字符串,getLastFieldOfHostName()方法也要修改代码: private String getLastfiledOfHostName() throws UnknownHostException{ String substrOfHostName = null; String hostName = InetAddress.getLocalHost().getHostName(); if (hostName==null || hostName.isEmpty()){ //此处做判断 throw new UnknownHostException("..."); } substrOfHostName = getLastSubstrSplittedByDot(hostName); return substrOfHostName; } #### 7. 重构generateRandomAlphameric()方法 #### 对generateRandomAlphameric(int length)方法,如果length小于0或者等于0,方法该返回什么? 先看length<0,这种不符合常规逻辑,是异常行为,抛出IllegalArgumentException异常。 再看length=0的情况,是否为异常?看自己定义。可以定义为异常,抛出IllegalArgumentException异常,可以定义为一种正常行为,让方法在入参length=0时,直接返回空字符串。关键是要在方法注释中,明确告知length=0时,返回什么样的数据。 ### 6.总结 ### 1. 再简单的代码,看上去再完美的代码,只要下功夫推敲,总可以优化,就看是否愿意把事情做到极致 2. 如果内功不够深厚,理论知识不够扎实,很难参透开源项目的代码到底优秀在哪里。 ## 实战2:完善性能计数器 ## ### 1. 回顾版本1 ### 整个框架的代码分为下面几个类: * MetricsCollector:负责埋点采集原始数据,包括记录每次接口请求的响应时间和请求时间戳,并调用MetricsStorage提供的接口存储原始数据 * MetricsStorage和RedisMetricsStorage负责原始数据的存储和读取 * Aggregator:工具类,负责各种统计数据的计算,如响应时间的最大值、最小值、平均值、百分位值、接口访问次数、tps * ConsoleReporter和EmailReporter相当于上帝类,定时根据给定的时间区间,从 数据库获取数据,借助Aggregator类完成统计工作,并将统计结果输出到相应的终端,如命令行、邮件 ### 2. 问题 ### 先看Aggregator类存在的问题 Aggregator类只有一个静态方法,负责各种统计数据的计算,当要添加新的功能时,需要修改aggregate()方法的代码。统计功能增加后,代码量持续增加,可读性、可维护性变差。需要重构 再看ConsoleReporter和EmailReporter存在的问题 存在代码重复问题,两个类从数据库中取数据、做统计的逻辑相同,可抽取出来复用。否则违反DRY原则。 整个类负责的事情较多,不相关的逻辑杂糅在一起,职责不够单一,特别是显示部分的代码可能较为复杂(如email的显示方式),最好能将这部分显示逻辑剥离出来,为独立的类。 此外,涉及到线程操作,调用Aggregator的静态方法,代码的可测试性有待提高。 ### 3. 重构版本1 ### Aggregator类和ConsoleReporter、EmailReporter类主要负责统计显示的工作。如果把统计显示要完成的功能逻辑细分,包含4点: 1. 根据给定的时间区间,从数据库中拉取数据 2. 根据原始数据,计算得到统计数据 3. 将统计数据显示到终端 4. 定时触发上述三个过程的执行 之前的划分方法是将所有的逻辑都放到ConsoleReporter、EmailReporter这两个上帝类中,而Aggregator只是个包含静态方法的工具类。划分存在前面提到的问题。 面向对象设计的最后一步是组装类并提供执行入口,所以,组装前三部分逻辑的上帝类是必须有的。可以将上帝类做的很轻量级。将核心逻辑剥离出来,形成独立的类,上帝类只负责组装类和串联执行流程。这样,代码结构更清晰,底层更易被复用。具体重构包含4个方面: 1. 根据给定时间区间,从数据库拉取数据。这部分逻辑已经被封装到MetricsStorage类,不需要处理 2. 根据原始数据,计算得到统计数据,可将这部分逻辑移动到Aggregator类中 3. 将统计数据显示到终端,将这部分逻辑剥离出来,设计为两个类ConsoleViewer、EmailViewer类 4. 组装类并定时触发执行统计显示。核心逻辑剥离后,类只负责组装各个类来完成整个工作流程 具体代码: * Aggregator的代码: public class Aggregator { public Map<String, RequestStat> aggregate(Map<String,List<RequestInfo>> requestInfos, long durationInMills){ Map<String,RequestStat> requestStats = new HashMap<>(); for (Map.Entry<String,List<RequestInfo>> entry:requestInfos.entrySet()){ String apiName = entry.getKey(); List<RequestInfo> requestInfosPerApi = entry.getValue(); RequestStat requestStat = doAggregate(requestInfosPerApi,durationInMills); requestStats.put(apiName,requestStat); } return requestStats; } private RequestStat doAggregate(List<RequestInfo> requestInfos,long durationInMillis){ List<Double> respTimes = new ArrayList<>(); for (RequestInfo requestInfo:requestInfos){ double respTime = requestInfo.getResponseTime(); respTimes.add(respTime); } RequestStat requestStat = new RequestStat(); requestStat.setMaxResponseTime(max(respTimes)); requestStat.setMinResponseTime(min(respTimes)); requestStat.setAvgResponseTime(avg(respTimes)); requestStat.setP999ResponseTime(percentile999(respTimes)); requestStat.setP99ResponseTime(percentile99(respTimes)); requestStat.setCount(respTimes.size()); requestStat.setTps((long)tps(respTimes.size(),durationInMillis/1000)); return requestStat; } //以下的代码的实现暂时忽略 private double max(List<Double> dataset){ return 0.0; } private double min(List<Double> dataset){ return 0.0; } private double avg(List<Double> dataset){ return 0.0; } private double percentile999(List<Double> dataset){ return 0.0; } private double percentile99(List<Double> dataset){ return 0.0; } private double percentile(List<Double> dataset,double ratio){ return 0.0; } private double tps(int count,double duration){ return 0.0; } } * 显示部分的代码: public interface StatViewer { void output(Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMillis); } public class ConsoleViewer implements StatViewer { @Override public void output(Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMillis) { System.out.println("Time Span: ["+startTimeInMillis+", "+endTimeInMillis); Gson gson = new Gson(); System.out.println(gson.toJson(requestStats)); } } public class EmailViewer implements StatViewer { private EmailSender emailSender; private List<String> toAddresses = new ArrayList<>(); public EmailViewer(){ this.emailSender = new EmailSender(); } public EmailViewer(EmailSender emailSender){ this.emailSender = emailSender; } public void addToAddress(String address){ toAddresses.add(address); } @Override public void output(Map<String, RequestStat> requestStats, long startTimeInMillis, long endTimeInMillis) { //format the requestStats to HTML style // send it to email toAddresses } } * 组装类 public class ConsoleReporter { private MetricsStorage metricsStorage; private ScheduledExecutorService executor; private Aggregator aggregator; private StatViewer viewer; public ConsoleReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.executor = Executors.newSingleThreadScheduledExecutor(); this.aggregator = aggregator; this.viewer = viewer; } public void startRepeatedReport(long periodInSeconds, final long durationInSeconds) { executor.scheduleAtFixedRate(new Runnable() { @Override public void run() { long durationInMills = durationInSeconds * 1000; long endTimeInMills = System.currentTimeMillis(); long startTimeInMills = endTimeInMills - durationInMills; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMills, endTimeInMills); Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos,durationInMills); viewer.output(requestStats, startTimeInMills, endTimeInMills); } }, 0L, periodInSeconds, TimeUnit.SECONDS); } } public class EmailReporter { private static final Long DAY_HOURS_IN_SECONDS = 86400L; private MetricsStorage metricsStorage; private Aggregator aggregator; private StatViewer viewer; public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; } public void startDailyReport() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); Date firstTime = calendar.getTime(); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { long durationInMillis = DAY_HOURS_IN_SECONDS * 1000; long endTimeInMillis = System.currentTimeMillis(); long startTimeInMillis = endTimeInMillis - durationInMillis; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis); Map<String, RequestStat> stats = aggregator.aggregate(requestInfos, durationInMillis); viewer.output(stats, startTimeInMillis, endTimeInMillis); } }, firstTime, DAY_HOURS_IN_SECONDS * 1000); } } 重构后,框架的使用: 在应用启动时,创建好ConsoleReporter对象,调用它的startRepeatedReporter()方法,启动定时统计并输出数据到终端,同样,创建EmailReporter对象,调用startDailyReport()方法,启动每日统计并输出数据到指定邮件地址。通过MetricsCollector类收集接口的访问情况,收集代码跟业务代码耦合在一起,或者统一放到类似spring aop的切面中完成。 public class PerfCounterTest { public static void main(String[] args) { MetricsStorage storage = new RedisMetricsStorage(); Aggregator aggregator = new Aggregator(); //定时触发统计并将结果显示到终端 ConsoleViewer consoleViewer = new ConsoleViewer(); ConsoleReporter consoleReporter = new ConsoleReporter(storage, aggregator, consoleViewer); consoleReporter.startRepeatedReport(60, 60); //定时触发统计并将结果输出到邮件 EmailViewer emailViewer = new EmailViewer(); emailViewer.addToAddress("xx@126.com"); EmailReporter emailReporter = new EmailReporter(storage, aggregator, emailViewer); emailReporter.startDailyReport(); //收集接口访问数据 MetricsCollector collector = new MetricsCollector(storage); collector.recordRequest(new RequestInfo("register", 123, 10234)); collector.recordRequest(new RequestInfo("register", 223, 11234)); collector.recordRequest(new RequestInfo("login", 23, 12234)); collector.recordRequest(new RequestInfo("login", 1223, 14234)); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } ### 4. 代码review ### 重构后,MetricsStorage负责存储,Aggregator负责统计,StatViewer(ConsoleViewer EmailViewer)负责显示,三个类各司其职。ConsoleReporter和EmailReporter负责组装三个类,将获取原始数据、聚合统计、显示统计结果到终端的工作串联起来,定时触发执行。 此外,MetricsStorage Aggregator StatViewer三个类的设计符合迪米特法则。只跟自己有直接关系的数据交互。MetricsStorage输出的是RequestInfo相关数据。Aggregator输入的是RequestInfo数据,输出RequestStat数据。StatViewer输入的是RequestStat数据。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70] 上图为代码的整体结构和依赖关系。再看具体每个类的设计。 Aggregator类从一个只包含一个静态方法的工具类,变为一个普通的聚合统计类。通过依赖注入方式,将其组装进ConsoleReporter和EmailReporter类,更容易编写单元测试。 Aggregator类的设计目前还算较为合理,如果统计功能越来越多,可以将统计方法剥离出来,设计为独立的类,解决该类无限膨胀问题。 ConsoleReporter和EmailReporter重构后,代码重复问题变小,但仍没有完美解决。涉及到多线程和时间相关的计算,代码的测试性不够好。 ### 版本3 ### ConsoleReporter和EmailReporter仍存在代码重复问题,可测试性差的问题。此外,也要继续完善框架的功能和非功能需求。如,让原始数据的采集和存储异步执行,解决聚合统计在数据量大的情况下导致内存吃紧的问题,以及提高框架的易用性。 可将ConsoleReporter和EmailReporter中的相同代码逻辑,提取到父类ScheduledReporter中,解决代码重复问题。 public abstract class ScheduledReporter { protected MetricsStorage metricsStorage; protected Aggregator aggregator; protected StatViewer viewer; public ScheduledReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; } protected void doStatAndReporter(long startTimeInMillis,long endTimeInMillis){ long durationInMillis = endTimeInMillis - startTimeInMillis; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis); Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos,durationInMillis); viewer.output(requestStats, startTimeInMillis, endTimeInMillis); } } 再看代码的可测试性问题,以EmailReporter为例。抽取重复代码后,该类的代码为: public class EmailReporter extends ScheduledReporter { private static final Long DAY_HOURS_IN_SECONDS = 86400L; private MetricsStorage metricsStorage; private Aggregator aggregator; private StatViewer viewer; public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { super(metricsStorage, aggregator, viewer); } public void startDailyReport() { Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); Date firstTime = calendar.getTime(); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { long durationInMillis = DAY_HOURS_IN_SECONDS * 1000; long endTimeInMillis = System.currentTimeMillis(); long startTimeInMillis = endTimeInMillis - durationInMillis; doStatAndReporter(startTimeInMillis,endTimeInMillis); } }, firstTime, DAY_HOURS_IN_SECONDS * 1000); } } 经过重构,EmailReporter的startDailyReport()方法的核心逻辑已经被抽离出来,较复杂的、易出bug的只剩下firstTime的部分代码,可将该部分代码继续抽离,封装为一个方法,再针对该方法写单元测试 public void startDailyReport() { Date firstTime = trimTimeFieldsToZeroOfNextDay(); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { //... } }, firstTime, DAY_HOURS_IN_SECONDS * 1000); } @VisibleForTesting protected Date trimTimeFieldsToZeroOfNextDay(){ Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DATE, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } 代码抽离后更清晰,但可测试性依旧不好,强依赖当前的系统时间。这个问题很普遍,一般解决方案是,将强依赖部分通过参数传递进来 public void startDailyReport() { //new Date()获取当前时间 Date firstTime = trimTimeFieldsToZeroOfNextDay(new Date()); Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { //... } }, firstTime, DAY_HOURS_IN_SECONDS * 1000); } @VisibleForTesting protected Date trimTimeFieldsToZeroOfNextDay(Date date){ Calendar calendar = Calendar.getInstance();//这里可以获取当前时间 calendar.setTime(date);//重新设置时间 calendar.add(Calendar.DATE, 1); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar.getTime(); } 重构后,比较容易编写单元测试了。 不过,EmailReporter类的startDailyReport()还是涉及多线程,如何单元测试呢?多次重构后,该方法里已经没有多少代码逻辑了,没必要对其写单元测试。不需要为了提高单元测试覆盖率而写单元测试。代码足够简单。 #### 2. 功能的完善 #### 已初步实现功能了 #### 3. 非功能的完善 #### 需要考虑非功能性需求:易用性、性能、扩展性、容错性、通用性 * 易用性 就是框架是否好用。从PerfCounterTest看,框架使用较为复杂,需要组装各种类,如创建MetricsStorage对象、Aggregator对象、ConsoleViewer对象,然后注入到ConsoleReporter中,才能用ConsoleReporter。此外,还可能误用,如把EmailViewer传递给ConsoleReporter。总体说,暴露太多细节给用户,过于灵活的同时降低了易用性。 为了让框架更简单,又不失灵活性(可自由组装不同的MetricsStorage实现类、StatViewer实现类到ConsoleReporter货EmailReporter),也不降低代码的可测试性(通过依赖注入来组装类,方便在单元测试中mock),可额外的提供一些封装了默认依赖的构造函数,让使用者自主选择使用哪种构造方法来构造对象。重构后: public class MetricsCollector { private MetricsStorage metricsStorage; //兼顾代码的易用性,新增一个封装了默认依赖的构造函数 public MetricsCollector(){ this(new RedisMetricsStorage()); } public MetricsCollector(MetricsStorage metricsStorage){ this.metricsStorage = metricsStorage; } //省略其他代码... } public class ConsoleReporter extends ScheduledReporter { private ScheduledExecutorService executor; //兼顾代码的易用性,新增一个封装了默认依赖的构造方法 public ConsoleReporter(){ this(new RedisMetricsStorage(),new Aggregator(),new ConsoleViewer()); } //兼顾灵活性和代码的可测试性,构造方法继续保留 public ConsoleReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { super(metricsStorage, aggregator, viewer); this.executor = Executors.newSingleThreadScheduledExecutor(); } //省略其他代码... } public class EmailReporter extends ScheduledReporter { private static final Long DAY_HOURS_IN_SECONDS = 86400L; private MetricsStorage metricsStorage; private Aggregator aggregator; private StatViewer viewer; //兼顾代码的易用性,新增一个封装了默认依赖的构造方法 public EmailReporter(List<String> emailToAddresses){ this(new RedisMetricsStorage(),new Aggregator(),new EmailViewer(emailToAddresses)); } //兼顾灵活性和代码的可测试性,这个构造方法继续保留 public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { super(metricsStorage, aggregator, viewer); } //省略其他代码... } 再看框架如何使用: public class PerfCounterTest { public static void main(String[] args) { ConsoleReporter consoleReporter = new ConsoleReporter(); consoleReporter.startRepeatedReport(60,60); List<String> emailToAddresses = new ArrayList<>(); emailToAddresses.add("xx@126.com"); com.ai.doc.chonggou1.metricsv2.EmailReporter emailReporter = new EmailReporter(emailToAddresses); emailReporter.startDailyReport(); MetricsCollector collector = new MetricsCollector(); collector.recordRequest(new RequestInfo("register", 123, 10234)); collector.recordRequest(new RequestInfo("register", 223, 11234)); collector.recordRequest(new RequestInfo("login", 23, 12234)); collector.recordRequest(new RequestInfo("login", 1223, 14234)); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } } 当然,RedisMetricsStorage和EmailViewer还需要另外一些配置信息才能构建成功,如Redis的地址,Email邮箱的pop3服务器地址、发送地址。这些配置信息如何获取? 可将配置信息放到配置文件中,在框架启动时,读取配置文件的配置信息到Configuration单例类。RedisMetricsStorage类和EmailViewer类都可从Configuration中获取需要的配置信息构建自己。 #### 2. 性能 #### 对于需要集成到业务系统的框架来说,不希望框架本身代码的执行效率,对业务系统有太多性能的影响。对性能计数器这个框架来说,一方面希望是低延迟,也就是说,统计代码不影响或很少影响接口本身的响应时间;另一方面,希望框架本身对内存的消耗不能太大。 在具体的代码层面,需要解决两个问题,一个是采集和存储要异步执行,因为存储基于外部存储如redis,会比较慢,异步存储可降低对接口响应时间的影响。另一个是当需要聚合统计的数据量较大时,一次性加载太多的数据到内存,可能导致内存吃紧,甚至内存溢出。 针对第一个问题,通过在MetricsCollector中引入Google guava eventBus来解决。实际上,可把EventBus看做“生产者-消费者”模型或“发布-订阅”模型,采集的数据先放入内存共享队列,另一个线程读取共享队列的数据,写入到外部存储如redis。具体的代码实现: public class MetricsCollector { private static final int DEFAULT_STORAGE_THREAD_POOL_SIZE = 20; private MetricsStorage metricsStorage; private EventBus eventBus; //兼顾代码的易用性,新增一个封装了默认依赖的构造函数 public MetricsCollector(){ this(new RedisMetricsStorage()); } public MetricsCollector(MetricsStorage metricsStorage){ this(metricsStorage,DEFAULT_STORAGE_THREAD_POOL_SIZE); } public MetricsCollector(MetricsStorage metricsStorage,int thredNumToSaveData){ this.metricsStorage = metricsStorage; this.eventBus = new AsyncEventBus(Executors.newFixedThreadPool(thredNumToSaveData)); this.eventBus.register(new EventListener() { }); } //用一个方法代替最小原型中的两个方法 public void recordRequest(RequestInfo requestInfo){ if(requestInfo==null || StringUtils.isBlank(requestInfo.getApiName())){ return; } eventBus.post(requestInfo); } public class EventListener { @Subscribe public void saveRequestInfo(RequestInfo requestInfo){ metricsStorage.saveRequestInfo(requestInfo); }} } 针对第二个问题,解决的思路较简单,但代码实现稍微复杂。统计的时间间隔较大时,需要统计的数据量较大。可将其划分为一些小的时间区间(如10分钟作为一个统计单元)。针对每个小的时间区间分别统计,然后将统计得到的结果再进行聚合,得到整个时间区间的统计结果。不过,这个思路只适合响应时间的max、min、avg,以及接口请求count、tps的统计,对于响应时间的percentile的统计并不适用。 对percentile的统计稍微复杂,具体的解决思路:分批从redis中读取数据,然后存储到文件,再根据响应时间从小到大利用外部排序算法进行排序。完成后, 再从文件读取第count\*percentile个数据,就是对应的percentile响应时间。 暂时只给出除了percentile外的统计信息的计算代码。 public abstract class ScheduledReporter { private static final long MAX_STAT_DURATION_IN_MILLIS = 10*60*1000;//10 MIN protected MetricsStorage metricsStorage; protected Aggregator aggregator; protected StatViewer viewer; public ScheduledReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) { this.metricsStorage = metricsStorage; this.aggregator = aggregator; this.viewer = viewer; } protected void doStatAndReporter(long startTimeInMillis,long endTimeInMillis){ Map<String,RequestStat> stats = doStat(startTimeInMillis,endTimeInMillis); long durationInMillis = endTimeInMillis - startTimeInMillis; Map<String, List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis); Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos,durationInMillis); viewer.output(requestStats, startTimeInMillis, endTimeInMillis); } private Map<String, RequestStat> doStat(long startTimeInMillis, long endTimeInMillis) { Map<String,List<RequestStat>> segmentStats = new HashMap<>(); long segmentStartTimeInMillis = startTimeInMillis; while (segmentStartTimeInMillis < endTimeInMillis){ long segmentEndTimeInMillis = segmentStartTimeInMillis + MAX_STAT_DURATION_IN_MILLIS; if (segmentEndTimeInMillis > endTimeInMillis){ segmentEndTimeInMillis = endTimeInMillis; } Map<String,List<RequestInfo>> requestInfos = metricsStorage.getRequestInfos(segmentStartTimeInMillis,segmentEndTimeInMillis); if (requestInfos == null || requestInfos.isEmpty()){ continue; } Map<String,RequestStat> segmentStat = aggregator.aggregate(requestInfos,segmentEndTimeInMillis-segmentStartTimeInMillis); addStat(segmentStats,segmentStat); segmentStartTimeInMillis += MAX_STAT_DURATION_IN_MILLIS; } long durationInMillis = endTimeInMillis - startTimeInMillis; Map<String,RequestStat> aggregatedStats = aggregateStat(segmentStats,durationInMillis); return aggregatedStats; } private Map<String, RequestStat> aggregateStat(Map<String, List<RequestStat>> segmentStats, long durationInMillis) { Map<String,RequestStat> aggregatedStats = new HashMap<>(); for (Map.Entry<String,List<RequestStat>> entry:segmentStats.entrySet()){ String apiName = entry.getKey(); List<RequestStat> apiStats = entry.getValue(); double maxRespTime = Double.MAX_VALUE; double minRespTime = Double.MIN_VALUE; long count = 0; double sumRespTime = 0; for (RequestStat stat:apiStats){ if (stat.getMaxResponseTime() > maxRespTime) maxRespTime = stat.getMaxResponseTime(); if (stat.getMinResponseTime() < minRespTime) minRespTime = stat.getMinResponseTime(); count += stat.getCount(); sumRespTime +=(stat.getCount() * stat.getAvgResponseTime()); } RequestStat aggregatedStat = new RequestStat(); aggregatedStat.setMaxResponseTime(maxRespTime); aggregatedStat.setMinResponseTime(minRespTime); aggregatedStat.setAvgResponseTime(sumRespTime/count); aggregatedStat.setTps(count/durationInMillis*1000); aggregatedStats.put(apiName,aggregatedStat); } return aggregatedStats; } private void addStat(Map<String, List<RequestStat>> segmentStats, Map<String, RequestStat> segmentStat) { for (Map.Entry<String,RequestStat> entry:segmentStat.entrySet()){ String apiName = entry.getKey(); RequestStat stat = entry.getValue(); List<RequestStat> statList = segmentStats.putIfAbsent(apiName,new ArrayList<RequestStat>()); statList.add(stat); } } } #### 3. 扩展性 #### 框架的扩展性有别于代码的扩展性。是从使用者的角度讲的。特指使用者在不修改框架源码,甚至不拿到框架源码的情况下,为框架扩展新的功能。 框架在兼顾易用性的同时,也可以灵活的替换各种类对象。如MetricsStorage、StatViewer。例如,我们让框架基于HBase存储原始数据,而非日的是,只需要设计一个实现MetricsStorage接口的HBaseMetricsStorage类,传递给MetricsCollector和ConsoleReporter、EmailReporter即可。 #### 4. 容错性 #### 对框架来说,不能因为框架本身的异常导致接口请求出错,对于框架可能存在的各种异常,要考虑全面。 在现在的框架设计和实现中,采集和存储是异步执行的,即使redis挂掉,不影响接口的正常响应。此外,redis异常,可能影响数据统计显示(也就是ConsoleReporter、EmailReporter负责的工作),但不影响接口的正常响应。 #### 5. 通用性 #### 为提高框架的复用性,能灵活应用于各种场景,框架设计时,尽可能通用。多思考,除了接口统计的需求,这个框架还可应用哪些场景。如是否可处理其他事件的统计信息,如SQL请求时间的统计、业务统计(如支付成功率)等。目前版本3暂时没考虑。 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70]: /images/20221124/4a1ed3ebbaee43368f326f101bf87a06.png
相关 设计模式之美笔记16 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 解释器模式 解释器模式的原理和实现 深藏阁楼爱情的钟/ 2022年12月01日 11:53/ 0 赞/ 136 阅读
相关 设计模式之美笔记15 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 访问者模式 访问者模式的诞生 我就是我/ 2022年12月01日 05:16/ 0 赞/ 143 阅读
相关 设计模式之美笔记14 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 状态模式 背景 什么 水深无声/ 2022年11月30日 15:51/ 0 赞/ 146 阅读
相关 设计模式之美笔记13 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 策略模式 策略模式的原理和实现 忘是亡心i/ 2022年11月30日 12:27/ 0 赞/ 145 阅读
相关 设计模式之美笔记12 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 观察者模式 原理及应用场景剖析 深碍√TFBOYSˉ_/ 2022年11月30日 04:18/ 0 赞/ 174 阅读
相关 设计模式之美笔记11 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 门面模式 门面模式的原理和实现 ゞ 浴缸里的玫瑰/ 2022年11月28日 13:41/ 0 赞/ 155 阅读
相关 设计模式之美笔记10 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 序言 代理模式 桥接模式 柔情只为你懂/ 2022年11月28日 10:36/ 0 赞/ 149 阅读
相关 设计模式之美笔记9 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 工厂模式 1. 简单工厂 待我称王封你为后i/ 2022年11月28日 00:41/ 0 赞/ 148 阅读
相关 设计模式之美笔记8 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 单例模式 1. 为什么要使用单例 柔光的暖阳◎/ 2022年11月26日 07:52/ 0 赞/ 154 阅读
相关 设计模式之美笔记7 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战1:id生成器的重构 1. 需求背景 女爷i/ 2022年11月25日 13:19/ 0 赞/ 187 阅读
还没有评论,来说两句吧...