IntelliJ IDEA远程Debug Linux的Java程序，找问题不要只会看日志了
1 前言我们习惯于在本地开发的时候debug，能快速定位与解决问题，那部署在服务器上是不是就没有办法了呢？只能通过查看日志来定位？不是的，在远端的服务器上，我们一样可以debug。2 IDEA的debug我们先来看一下在IntelliJ IDEA直接debug是怎样的。先准备一个简单的Java程序：package com.pkslow.basic; import java.util.Map; public class RemoteDebug { public static void main(String[] args) { System.out.println("------------------start------------------"); System.out.println("get all the system environment"); Map<String, String> envs = System.getenv(); System.out.println(" print out the contains `HOME`"); System.out.println("------env HOME------"); envs.entrySet().stream() .filter(env -> env.getKey().contains("HOME")) .forEach(env -> { System.out.println(env.getKey() + ":" + env.getValue()); }); System.out.println("------------------end------------------"); } }功能很简单，获取所有系统环境变量，并打印出含有HOME字段的。Debug很简单，直接点击以下按钮就可以：相信大家都知道这一点，但应该很多人都不会注意，IDEA究竟做了什么，为什么就可以调试了呢？我们看一下控制台的日志就明白了：/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n -javaagent:/Users/pkslow/Library/Caches/IntelliJIdea2019.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "/Library/Java/JavaVirtualMachines/jdk1.8.0_212.jdk/Contents/Home/jre/lib/charsets.jar:" com.pkslow.basic.RemoteDebug Connected to the target VM, address: '127.0.0.1:59313', transport: 'socket'简化一下，不重要的参数去掉：java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:59313,suspend=y,server=n com.pkslow.basic.RemoteDebug这就是可以Debug的原因，利用了Java Agent原理。这个功能很强大，类似一个AOP，代理了Java程序，可以利用它进行调试、热部署等。3 调试本地程序我们先试试如何可以调试本地程序，不是直接在IDEA上调试。先要编译出class文件RemoteDebug.class，然后按package结构放好。我通过mvn clean compile来编译。启动程序，在target/classes/目录执行：$ java -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:50050,suspend=y,server=y com.pkslow.basic.RemoteDebug Listening for transport dt_socket at address: 50050然后程序就会等待调试客户端的连接，不会往下执行。配置IDEA以进行调试：配置完成保存后，点击debug就可以了：程序已经进入debug模式：我们已经执行到了其中一行，现在看看服务端：与IDEA是同步的，并且确实已经控制了服务端Java的执行。4 远程调试Linux Java程序先把程序部署在Linux上：$ scp -P 22 ./com/pkslow/basic/RemoteDebug.class root@xxx.xx.xx.xxx:/root/remoteDebug/com/pkslow/basic/ RemoteDebug.class 100% 2572 282.5KB/s 00:00 通过以下命令在服务端启动程序，这里调试端口改为9999，因为部分端口在远程服务器并没有开启：java -agentlib:jdwp=transport=dt_socket,address=9999,suspend=y,server=y com.pkslow.basic.RemoteDebug 本地电脑IDEA配置如下：开始debug，正常控制了远程服务端的Java：服务端的实时执行情况：$ java -agentlib:jdwp=transport=dt_socket,address=9999,suspend=y,server=y com.pkslow.basic.RemoteDebug Listening for transport dt_socket at address: 9999 ------------------start------------------ get all the system environment print out the contains `HOME` ------env HOME------ JAVA_HOME:/root/jdk1.8.0_131 HOME:/root ------------------end------------------
1. Streams简介今天要讲的Stream指的是java.util.stream包中的诸多类。Stream可以方便的将之前的结合类以转换为Stream并以流式方式进行处理，大大的简化了我们的编程，Stream包中，最核心的就是interface Stream1.1 创建StreamStream的创建有很多方式，java引入Stream之后所有的集合类都添加了一个stream()方法，通过这个方法可以直接得到其对应的Stream。也可以通过Stream.of方法来创建：//Stream Creation String[] arr = new String[]{"a", "b", "c"}; Stream<String> stream = Arrays.stream(arr); stream = Stream.of("a", "b", "c");复制代码1.2 Streams多线程如果我们想使用多线程来处理集合类的数据，Stream提供了非常方便的多线程方法parallelStream()：//Multi-threading List<String> list =new ArrayList(); list.add("aaa"); list.add("bbb"); list.add("abc"); list.add("ccc"); list.add("ddd"); list.parallelStream().forEach(element -> doPrint(element));复制代码1.3 Stream的基本操作Stream的操作可以分为两类，一类是中间操作，中间操作返回Stream，因此可以级联调用。 另一类是终止操作，这类操作会返回Stream定义的类型。//Operations long count = list.stream().distinct().count();复制代码上面的例子中，distinct()返回一个Stream，所以可以级联操作，最后的count()是一个终止操作，返回最后的值。Matching Stream提供了anyMatch(), allMatch(), noneMatch()这三种match方式，我们看下怎么使用： //Matching boolean isValid = list.stream().anyMatch(element -> element.contains("h")); boolean isValidOne = list.stream().allMatch(element -> element.contains("h")); boolean isValidTwo = list.stream().noneMatch(element -> element.contains("h")); 复制代码Filteringfilter() 方法允许我们对Stream中的数据进行过滤，从而得到我们需要的：Stream filterStream = list.stream().filter(element -> element.contains("d"));复制代码上面的例子中我们从list中选出了包含“d”字母的String。Mappingmap就是对Stream中的值进行再加工，然后将加工过后的值作为新的Stream返回。//Mapping Stream<String> mappingStream = list.stream().map(element -> convertElement(element)); private static String convertElement(String element) { return "element"+"abc"; }复制代码上的例子中我们把list中的每个值都加上了“abc”然后返回一个新的Stream。FlatMapflatMap和Map很类似，但是他们两个又有不同，看名字我们可以看到flatMap意思是打平之后再做Map。怎么理解呢？假如我们有一个CustBook类：@Data public class CustBook { List<String> bookName; }复制代码CustBook定义了一个bookName字段。先看一下Map返回的结果：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<Stream<String>> userStreamMap = users.stream().map(user -> user.getBookName().stream());复制代码在上面的代码中，map将每一个user都转换成了stream，所以最后的结果是返回Stream的Stream。如果我们只想返回String，则可以使用FlatMap：List<CustBook> users = new ArrayList<>(); users.add(new CustBook()); Stream<String> userStream = users.stream().flatMap(user -> user.getBookName().stream());复制代码简单点讲FlatMap就是将层级关系铺平重来。Reduction使用reduce() 方法可以方便的对集合的数据进行运算，reduce()接收两个参数，第一个是开始值，后面是一个函数表示累计。//Reduction List<Integer> integers = Arrays.asList(1, 1, 1); Integer reduced = integers.stream().reduce(100, (a, b) -> a + b);复制代码上面的例子我们定义了3个1的list，然后调用reduce(100, (a, b) -> a + b)方法，最后的结果是103.Collecting collect()方法可以方便的将Stream再次转换为集合类，方便处理和展示： List<String> resultList = list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());
处理 Exception 的几种实践，很优雅，被很多团队采纳！
在Java中处理异常并不是一个简单的事情。不仅仅初学者很难理解，即使一些有经验的开发者也需要花费很多时间来思考如何处理异常，包括需要处理哪些异常，怎样处理等等。这也是绝大多数开发团队都会制定一些规则来规范对异常的处理的原因。而团队之间的这些规范往往是截然不同的。本文给出几个被很多团队使用的异常处理最佳实践。在Finally块中清理资源或者使用try-with-resource语句当使用类似InputStream这种需要使用后关闭的资源时，一个常见的错误就是在try块的最后关闭资源。`public void doNotCloseResourceInTry() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file``// do NOT do this` `inputStream.close();` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`上述代码在没有任何exception的时候运行是没有问题的。但是当try块中的语句抛出异常或者自己实现的代码抛出异常，那么就不会执行最后的关闭语句，从而资源也无法释放。合理的做法则是将所有清理的代码都放到finally块中或者使用try-with-resource语句。`public void closeResourceInFinally() {` `FileInputStream inputStream = null;``try {` `File file = new File("./tmp.txt");` `inputStream = new FileInputStream(file);``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} finally {``if (inputStream != null) {``try {` `inputStream.close();` `} catch (IOException e) {``log.error(e);` `}` `}` `}``}``public void automaticallyCloseResource() {` `File file = new File("./tmp.txt");``try (FileInputStream inputStream = new FileInputStream(file);) {``// use the inputStream to read a file` `} catch (FileNotFoundException e) {``log.error(e);` `} catch (IOException e) {``log.error(e);` `}``}`指定具体的异常尽可能的使用最具体的异常来声明方法，这样才能使得代码更容易理解。`public void doNotDoThis() throws Exception {` `...``}``public void doThis() throws NumberFormatException {` `...``}`如上，NumberFormatException字面上即可以看出是数字格式化错误。对异常进行文档说明当在方法上声明抛出异常时，也需要进行文档说明。和前面的一点一样，都是为了给调用者提供尽可能多的信息，从而可以更好地避免/处理异常。异常处理的 10 个最佳实践，这篇也推荐看下。在Javadoc中加入throws声明，并且描述抛出异常的场景。`/**` `* This method does something extremely useful ...` `*` `* @param input` `* @throws MyBusinessException if ... happens` `*/``public void doSomething(String input) throws MyBusinessException {` `...``}`抛出异常的时候包含描述信息在抛出异常时，需要尽可能精确地描述问题和相关信息，这样无论是打印到日志中还是监控工具中，都能够更容易被人阅读，从而可以更好地定位具体错误信息、错误的严重程度等。但这里并不是说要对错误信息长篇大论，因为本来Exception的类名就能够反映错误的原因，因此只需要用一到两句话描述即可。`try {``new Long("xyz");``} catch (NumberFormatException e) {``log.error(e);``}`NumberFormatException即告诉了这个异常是格式化错误，异常的额外信息只需要提供这个错误字符串即可。当异常的名称不够明显的时候，则需要提供尽可能具体的错误信息。首先捕获最具体的异常现在很多IDE都能智能提示这个最佳实践，当你试图首先捕获最笼统的异常时，会提示不能达到的代码。当有多个catch块中，按照捕获顺序只有第一个匹配到的catch块才能执行。因此，如果先捕获IllegalArgumentException，那么则无法运行到对NumberFormatException的捕获。`public void catchMostSpecificExceptionFirst() {``try {` `doSomething("A message");` `} catch (NumberFormatException e) {``log.error(e);` `} catch (IllegalArgumentException e) {``log.error(e)` `}``}`# 不要捕获ThrowableThrowable是所有异常和错误的父类。你可以在catch语句中捕获，但是永远不要这么做。如果catch了throwable，那么不仅仅会捕获所有exception，还会捕获error。而error是表明无法恢复的jvm错误。因此除非绝对肯定能够处理或者被要求处理error，不要捕获throwable。`public void doNotCatchThrowable() {``try {``// do something` `} catch (Throwable t) {``// don't do this!` `}``}`不要忽略异常很多时候，开发者很有自信不会抛出异常，因此写了一个catch块，但是没有做任何处理或者记录日志。`public void doNotIgnoreExceptions() {``try {``// do something` `} catch (NumberFormatException e) {``// this will never happen` `}``}`但现实是经常会出现无法预料的异常或者无法确定这里的代码未来是不是会改动(删除了阻止异常抛出的代码)，而此时由于异常被捕获，使得无法拿到足够的错误信息来定位问题。合理的做法是至少要记录异常的信息。`public void logAnException() {``try {``// do something` `} catch (NumberFormatException e) {``log.error("This should never happen: " + e);` `}``}`不要记录并抛出异常可以发现很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。如下：`try {``new Long("xyz");``} catch (NumberFormatException e) {``log.error(e);``throw e;``}`这个处理逻辑看着是合理的。但这经常会给同一个异常输出多条日志。如下：`17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"``Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"``at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)``at java.lang.Long.parseLong(Long.java:589)``at java.lang.Long.(Long.java:965)``at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)``at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)`如上所示，后面的日志也没有附加更有用的信息。如果想要提供更加有用的信息， `````` 那么可以将异常包装为自定义异常。 `public void wrapException(String input) throws MyBusinessException {``try {``// do something` `} catch (NumberFormatException e) {``throw new MyBusinessException("A message that describes the error.", e);` `}``}`因此，仅仅当想要处理异常时才去捕获，否则只需要在方法签名中声明让调用者去处理包装异常时不要抛弃原始的异常捕获标准异常并包装为自定义异常是一个很常见的做法。这样可以添加更为具体的异常信息并能够做针对的异常处理。需要注意的是，包装异常时，一定要把原始的异常设置为cause(Exception有构造方法可以传入cause)。否则，丢失了原始的异常信息会让错误的分析变得困难。`public void wrapException(String input) throws MyBusinessException {``try {``// do something` `} catch (NumberFormatException e) {``throw new MyBusinessException("A message that describes the error.", e);` `}``}`总结综上可知，当抛出或者捕获异常时，有很多不一样的东西需要考虑。其中的许多点都是为了提升代码的可阅读性或者api的可用性。异常不仅仅是一个错误控制机制，也是一个沟通媒介，因此与你的协作者讨论这些最佳实践并制定一些规范能够让每个人都理解相关的通用概念并且能够按照同样的方式使用它们。
一、前言好看的代码千篇一律，恶心的程序升职加薪。该说不说几乎是程序员就都知道或者了解设计模式，但大部分小伙伴写代码总是习惯于一把梭。无论多少业务逻辑就一个类几千行，这样的开发也可以归纳为三步；定义属性、创建方法、调用展示，Done！只不过开发一时爽，重构火葬场。好的代码不只为了完成现有功能，也会考虑后续扩展。在结构设计上松耦合易读易扩展，在领域实现上高内聚不对外暴漏实现细节不被外部干扰。而这就有点像家里三居(MVC)室、四居(DDD)室的装修，你不会允许几十万的房子把走线水管裸漏在外面，也不会允许把马桶放到厨房，炉灶安装到卫生间。谁发明了设计模式？ 设计模式的概念最早是由 克里斯托佛·亚历山大 在其著作 《建筑模式语言》 中首次提出的。 本书介绍了城市设计的 “语言”，提供了253个描述城镇、邻里、住宅、花园、房间及西部构造的模式， 而此类 “语言” 的基本单元就是模式。后来，埃里希·伽玛、 约翰·弗利赛德斯、 拉尔夫·约翰逊 和 理查德·赫尔姆 这四位作者接受了模式的概念。 1994 年， 他们出版了 《设计模式： 可复用面向对象软件的基础》 一书， 将设计模式的概念应用到程序开发领域中。其实有一部分人并没有仔细阅读过设计模式的相关书籍和资料，但依旧可以编写出优秀的代码。这主要是由于在经过众多项目的锤炼和对程序设计的不断追求，从而在多年编程历程上提炼出来的心得体会。而这份经验最终会与设计模式提到的内容几乎一致，同样会要求高内聚、低耦合、可扩展、可复用。你可能也遇到类似的经历，在学习一些框架的源码时，发现它里的某些设计和你在做开发时一样。我怎么学不会设计模式？ 钱也花了，书也买了。代码还是一坨一坨的！设计模式是由多年的经验提炼出来开发指导思想。就像我告诉你自行车怎么骑、汽车怎么开，但只要你没跑过几千公里，你能记住的只是理论，想上道依旧很慌！所以，本设计模式专题系列开始，会带着你使用设计模式的思想去优化代码。从而学习设计模式的心得并融入给自己。当然这里还需要多加练习，一定是人车合一，才能站在设计模式的基础上构建出更加合理的代码。二、工厂方法模式介绍工厂方法模式，图片来自 refactoringguru.cn工厂模式又称工厂方法模式，是一种创建型设计模式，其在父类中提供一个创建对象的方法， 允许子类决定实例化对象的类型。这种设计模式也是 Java 开发中最常见的一种模式，它的主要意图是定义一个创建对象的接口，让其子类自己决定实例化哪一个工厂类，工厂模式使其创建过程延迟到子类进行。简单说就是为了提供代码结构的扩展性，屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可，同时，这也是去掉众多ifeslse的方式。当然这可能也有一些缺点，比如需要实现的类非常多，如何去维护，怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中，逐步降低。三、模拟发奖多种商品为了可以让整个学习的案例更加贴近实际开发，这里模拟互联网中在营销场景下的业务。由于营销场景的复杂、多变、临时的特性，它所需要的设计需要更加深入，否则会经常面临各种紧急CRUD操作，从而让代码结构混乱不堪，难以维护。在营销场景中经常会有某个用户做了一些操作；打卡、分享、留言、邀请注册等等，进行返利积分，最后通过积分在兑换商品，从而促活和拉新。那么在这里我们模拟积分兑换中的发放多种类型商品，假如现在我们有如下三种类型的商品接口；序号类型接口1优惠券CouponResult sendCoupon(String uId, String couponNumber, String uuid)2实物商品Boolean deliverGoods(DeliverReq req)3第三方爱奇艺兑换卡void grantToken(String bindMobileNumber, String cardId)从以上接口来看有如下信息：三个接口返回类型不同，有对象类型、布尔类型、还有一个空类型。入参不同，发放优惠券需要仿重、兑换卡需要卡ID、实物商品需要发货位置(对象中含有)。另外可能会随着后续的业务的发展，会新增其他种商品类型。因为你所有的开发需求都是随着业务对市场的拓展而带来的。四、用一坨坨代码实现ifelse实现需求public class PrizeController { private Logger logger = LoggerFactory.getLogger(PrizeController.class); public AwardRes awardToUser(AwardReq req) { String reqJson = JSON.toJSONString(req); AwardRes awardRes = null; try { logger.info("奖品发放开始{}。req:{}", req.getuId(), reqJson); // 按照不同类型方法商品[1优惠券、2实物商品、3第三方兑换卡(爱奇艺)] if (req.getAwardType() == 1) { CouponService couponService = new CouponService(); CouponResult couponResult = couponService.sendCoupon(req.getuId(), req.getAwardNumber(), req.getBizId()); if ("0000".equals(couponResult.getCode())) { awardRes = new AwardRes("0000", "发放成功"); } else { awardRes = new AwardRes("0001", couponResult.getInfo()); } } else if (req.getAwardType() == 2) { GoodsService goodsService = new GoodsService(); DeliverReq deliverReq = new DeliverReq(); deliverReq.setUserName(queryUserName(req.getuId())); deliverReq.setUserPhone(queryUserPhoneNumber(req.getuId())); deliverReq.setSku(req.getAwardNumber()); deliverReq.setOrderId(req.getBizId()); deliverReq.setConsigneeUserName(req.getExtMap().get("consigneeUserName")); deliverReq.setConsigneeUserPhone(req.getExtMap().get("consigneeUserPhone")); deliverReq.setConsigneeUserAddress(req.getExtMap().get("consigneeUserAddress")); Boolean isSuccess = goodsService.deliverGoods(deliverReq); if (isSuccess) { awardRes = new AwardRes("0000", "发放成功"); } else { awardRes = new AwardRes("0001", "发放失败"); } } else if (req.getAwardType() == 3) { String bindMobileNumber = queryUserPhoneNumber(req.getuId()); IQiYiCardService iQiYiCardService = new IQiYiCardService(); iQiYiCardService.grantToken(bindMobileNumber, req.getAwardNumber()); awardRes = new AwardRes("0000", "发放成功"); } logger.info("奖品发放完成{}。", req.getuId()); } catch (Exception e) { logger.error("奖品发放失败{}。req:{}", req.getuId(), reqJson, e); awardRes = new AwardRes("0001", e.getMessage()); } return awardRes; } private String queryUserName(String uId) { return "花花"; } private String queryUserPhoneNumber(String uId) { return "15200101232"; } }如上就是使用 ifelse 非常直接的实现出来业务需求的一坨代码，如果仅从业务角度看，研发如期甚至提前实现了功能。那这样的代码目前来看并不会有什么问题，但如果在经过几次的迭代和拓展，接手这段代码的研发将十分痛苦。重构成本高需要理清之前每一个接口的使用，测试回归验证时间长，需要全部验证一次。这也就是很多人并不愿意接手别人的代码，如果接手了又被压榨开发时间。那么可想而知这样的 ifelse 还会继续增加。五、工厂模式优化代码定义发奖接口public interface ICommodity { void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception; }所有的奖品无论是实物、虚拟还是第三方，都需要通过我们的程序实现此接口进行处理，以保证最终入参出参的统一性。接口的入参包括；用户ID、奖品ID、业务ID以及扩展字段用于处理发放实物商品时的收获地址。实现奖品发放接口优惠券public class CouponCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class); private CouponService couponService = new CouponService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId); logger.info("请求参数[优惠券] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[优惠券]：{}", JSON.toJSON(couponResult)); if (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo()); } }实物商品 public class GoodsCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class); private GoodsService goodsService = new GoodsService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { DeliverReq deliverReq = new DeliverReq(); deliverReq.setUserName(queryUserName(uId)); deliverReq.setUserPhone(queryUserPhoneNumber(uId)); deliverReq.setSku(commodityId); deliverReq.setOrderId(bizId); deliverReq.setConsigneeUserName(extMap.get("consigneeUserName")); deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone")); deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress")); Boolean isSuccess = goodsService.deliverGoods(deliverReq); logger.info("请求参数[优惠券] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[优惠券]：{}", isSuccess); if (!isSuccess) throw new RuntimeException("实物商品发放失败"); } private String queryUserName(String uId) { return "花花"; } private String queryUserPhoneNumber(String uId) { return "15200101232"; } }第三方兑换卡public class CardCommodityService implements ICommodity { private Logger logger = LoggerFactory.getLogger(CardCommodityService.class); // 模拟注入 private IQiYiCardService iQiYiCardService = new IQiYiCardService(); public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception { String mobile = queryUserMobile(uId); iQiYiCardService.grantToken(mobile, bizId); logger.info("请求参数[爱奇艺兑换卡] => uId：{} commodityId：{} bizId：{} extMap：{}", uId, commodityId, bizId, JSON.toJSON(extMap)); logger.info("测试结果[爱奇艺兑换卡]：success"); } private String queryUserMobile(String uId) { return "15200101232"; } }从上面可以看到每一种奖品的实现都包括在自己的类中，新增、修改或者删除都不会影响其他奖品功能的测试，降低回归测试的可能。后续在新增的奖品只需要按照此结构进行填充即可，非常易于维护和扩展。在统一了入参以及出参后，调用方不在需要关心奖品发放的内部逻辑，按照统一的方式即可处理。创建商店工厂public class StoreFactory { public ICommodity getCommodityService(Integer commodityType) { if (null == commodityType) return null; if (1 == commodityType) return new CouponCommodityService(); if (2 == commodityType) return new GoodsCommodityService(); if (3 == commodityType) return new CardCommodityService(); throw new RuntimeException("不存在的商品服务类型"); } }这里我们定义了一个商店的工厂类，在里面按照类型实现各种商品的服务。可以非常干净整洁的处理你的代码，后续新增的商品在这里扩展即可。如果你不喜欢if判断，也可以使用switch或者map配置结构，会让代码更加干净。另外很多代码检查软件和编码要求，不喜欢if语句后面不写扩展，这里是为了更加干净的向你体现逻辑。在实际的业务编码中可以添加括号。总结从上到下的优化来看，工厂方法模式并不复杂，甚至这样的开发结构在你有所理解后，会发现更加简单了。那么这样的开发的好处知道后，也可以总结出来它的优点；避免创建者与具体的产品逻辑耦合、满足单一职责，每一个业务逻辑实现都在所属自己的类中完成、满足开闭原则，无需更改使用调用方就可以在程序中引入新的产品类型。但这样也会带来一些问题，比如有非常多的奖品类型，那么实现的子类会极速扩张。因此也需要使用其他的模式进行优化，这些在后续的设计模式中会逐步涉及到。从案例入手看设计模式往往要比看理论学的更加容易，因为案例是缩短理论到上手的最佳方式，如果你已经有所收获，一定要去尝试实操。
如今Java14已经发布许久了，Java15也在路上跑着了，然鹅不少小伙伴Java8的API应该还没用溜吧！今天跟各位小伙伴们聊聊Java Stream API的具体使用方法以及应用场景。举哥简单的例子，定义一个数组：String[] users = new String[]{"张三","李四","王二"};我们来使用之前的方式遍历：for(int i=0;i<users.length;i++){ System.out.println(users[i]); }亦或者使用：for(String u:users){ System.out.println(u); }使用最新的API实现方式：Stream<String> stream = Stream.of(users); stream.forEach(user-> System.out.println(user));然鹅好像并没有多么方便，仅仅少撸了一行代码，下面跟大家分享一个稍微复杂一点的例子。首先定义一个学生类：@Data @Builder public class Student { private String name; private Integer age; private Integer sex; private Double score; }定义基础数据：List<Student> list = new ArrayList<>(); list.add(new Student("张三",28,1,78.9)); list.add(new Student("李四",20,1,98.0)); list.add(new Student("王二",29,0,100.0)); list.add(new Student("张三",28,1,78.5)); Stream<Student> stream = list.stream();遍历：stream.forEach(stu-> System.out.println(stu.getName()));过滤性别：stream.filter(stu -> stu.getSex()==1) .forEach(stu -> System.out.println(stu.getName()));名字去重：stream.distinct().forEach(stu -> System.out.println(stu.getName()));年龄排序：stream.sorted(Comparator.comparingInt(Student::getAge)) .forEach(stu -> System.out.println(stu.getName()));排序反转：stream.sorted(Comparator.comparingInt(Student::getAge).reversed()) .forEach(stu -> System.out.println(stu.getName()));姓名集合：List<String> userList = stream.map(stu -> stu.getName()) .collect(Collectors.toList());分数求和：Optional<Double> totalScore = stream.map(Student::getScore) .reduce((x, y) -> x+y); System.out.println(totalScore.get());求分数最大值：Optional<Double> maxScore = stream.map(Student::getScore).reduce(Double::max);求分数最小值：Optional<Double> minScore = stream.map(Student::getScore).reduce(Double::min);取前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .limit(3) .forEach(System.out::println);扔了前三名同学：stream.sorted(Comparator.comparingDouble(Student::getScore).reversed()) .skip(3) .forEach(System.out::println);卧槽太多了，撸主举不过来了，但是基本上常用的就这么几个。函数式编程写出的代码更加简洁且意图明确，使用stream接口让你从此告别for循环，小伙伴们赶紧入坑享受吧！！！
Dataway介绍Dataway 是基于 DataQL 服务聚合能力，为应用提供的一个接口配置工具。使得使用者无需开发任何代码就配置一个满足需求的接口。整个接口配置、测试、冒烟、发布。一站式都通过 Dataway 提供的 UI 界面完成。UI 会以 Jar 包方式提供并集成到应用中并和应用共享同一个 http 端口，应用无需单独为 Dataway 开辟新的管理端口。 这种内嵌集成方式模式的优点是，可以使得大部分老项目都可以在无侵入的情况下直接应用 Dataway。进而改进老项目的迭代效率，大大减少企业项目研发成本。 Dataway 工具化的提供 DataQL 配置能力。这种研发模式的变革使得，相当多的需求开发场景只需要配置即可完成交付。从而避免了从数据存取到前端接口之间的一系列开发任务，例如：Mapper、BO、VO、DO、DAO、Service、Controller 统统不在需要。Dataway 是 Hasor 生态中的一员，因此在 Spring 中使用 Dataway 首先要做的就是打通两个生态。根据官方文档中推荐的方式我们将 Hasor 和 Spring Boot 整合起来。这里是原文：https://www.hasor.net/web/extends/spring/for_boot.html第一步：引入相关依赖<dependency> <groupId>net.hasor</groupId> <artifactId>hasor-spring</artifactId> <version>4.1.3</version> </dependency> <dependency> <groupId>net.hasor</groupId> <artifactId>hasor-dataway</artifactId> <version>4.1.3-fix20200414</version> </dependency>hasor-spring 负责 Spring 和 Hasor 框架之间的整合。hasor-dataway 是工作在 Hasor 之上，利用 hasor-spring 我们就可以使用 dataway了。第二步：配置 Dataway，并初始化数据表dataway 会提供一个界面让我们配置接口，这一点类似 Swagger 只要jar包集成就可以实现接口配置。找到我们 springboot 项目的配置文件 application.properties# 是否启用 Dataway 功能（必选：默认false） HASOR_DATAQL_DATAWAY=true # 是否开启 Dataway 后台管理界面（必选：默认false） HASOR_DATAQL_DATAWAY_ADMIN=true # dataway API工作路径（可选，默认：/api/） HASOR_DATAQL_DATAWAY_API_URL=/api/ # dataway-ui 的工作路径（可选，默认：/interface-ui/） HASOR_DATAQL_DATAWAY_UI_URL=/interface-ui/ # SQL执行器方言设置（可选，建议设置） HASOR_DATAQL_FX_PAGE_DIALECT=mysqlDataway 一共涉及到 5个可以配置的配置项，但不是所有配置都是必须的。其中 HASOR_DATAQL_DATAWAY、HASOR_DATAQL_DATAWAY_ADMIN 两个配置是必须要打开的，默认情况下 Datawaty 是不启用的。Dataway 需要两个数据表才能工作，下面是这两个数据表的简表语句。下面这个 SQL 可以在 dataway的依赖 jar 包中 “META-INF/hasor-framework/mysql” 目录下面找到，建表语句是用 mysql 语法写的。CREATE TABLE `interface_info` ( `api_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID', `api_method` varchar(12) NOT NULL COMMENT 'HttpMethod：GET、PUT、POST', `api_path` varchar(512) NOT NULL COMMENT '拦截路径', `api_status` int(2) NOT NULL COMMENT '状态：0草稿，1发布，2有变更，3禁用', `api_comment` varchar(255) NULL COMMENT '注释', `api_type` varchar(24) NOT NULL COMMENT '脚本类型：SQL、DataQL', `api_script` mediumtext NOT NULL COMMENT '查询脚本：xxxxxxx', `api_schema` mediumtext NULL COMMENT '接口的请求/响应数据结构', `api_sample` mediumtext NULL COMMENT '请求/响应/请求头样本数据', `api_create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `api_gmt_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`api_id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='Dataway 中的API'; CREATE TABLE `interface_release` ( `pub_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Publish ID', `pub_api_id` int(11) NOT NULL COMMENT '所属API ID', `pub_method` varchar(12) NOT NULL COMMENT 'HttpMethod：GET、PUT、POST', `pub_path` varchar(512) NOT NULL COMMENT '拦截路径', `pub_status` int(2) NOT NULL COMMENT '状态：0有效，1无效（可能被下线）', `pub_type` varchar(24) NOT NULL COMMENT '脚本类型：SQL、DataQL', `pub_script` mediumtext NOT NULL COMMENT '查询脚本：xxxxxxx', `pub_script_ori` mediumtext NOT NULL COMMENT '原始查询脚本，仅当类型为SQL时不同', `pub_schema` mediumtext NULL COMMENT '接口的请求/响应数据结构', `pub_sample` mediumtext NULL COMMENT '请求/响应/请求头样本数据', `pub_release_time`datetime DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间（下线不更新）', PRIMARY KEY (`pub_id`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COMMENT='Dataway API 发布历史。'; create index idx_interface_release on interface_release (pub_api_id);第三步：配置数据源作为 Spring Boot 项目有着自己完善的数据库方面工具支持。我们这次采用 druid + mysql + spring-boot-starter-jdbc 的方式。首先引入依赖<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.21</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency>然后增加数据源的配置# db spring.datasource.url=jdbc:mysql://xxxxxxx:3306/example spring.datasource.username=xxxxx spring.datasource.password=xxxxx spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.type:com.alibaba.druid.pool.DruidDataSource # druid spring.datasource.druid.initial-size=3 spring.datasource.druid.min-idle=3 spring.datasource.druid.max-active=10 spring.datasource.druid.max-wait=60000 spring.datasource.druid.stat-view-servlet.login-username=admin spring.datasource.druid.stat-view-servlet.login-password=admin spring.datasource.druid.filter.stat.log-slow-sql=true spring.datasource.druid.filter.stat.slow-sql-millis=1如果项目已经集成了自己的数据源，那么可以忽略第三步。第四步：把数据源设置到 Hasor 容器中Spring Boot 和 Hasor 本是两个独立的容器框架，我们做整合之后为了使用 Dataway 的能力需要把 Spring 中的数据源设置到 Hasor 中。首先新建一个 Hasor 的 模块，并且将其交给 Spring 管理。然后把数据源通过 Spring 注入进来。@DimModule @Component public class ExampleModule implements SpringModule { @Autowired private DataSource dataSource = null; @Override public void loadModule(ApiBinder apiBinder) throws Throwable { // .DataSource form Spring boot into Hasor apiBinder.installModule(new JdbcModule(Level.Full, this.dataSource)); } }Hasor 启动的时候会调用 loadModule 方法，在这里再把 DataSource 设置到 Hasor 中。第五步：在SprintBoot 中启用 Hasor@EnableHasor() @EnableHasorWeb() @SpringBootApplication(scanBasePackages = { "net.example.hasor" }) public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(ExampleApplication.class, args); } }这一步非常简单，只需要在 Spring 启动类上增加两个注解即可。第六步：启动应用应用在启动过程中会看到 Hasor Boot 的欢迎信息 _ _ ____ _ | | | | | _ | | | |__| | __ _ ___ ___ _ __ | |_) | ___ ___ | |_ | __ |/ _` / __|/ _ | '__| | _ < / _ / _ | __| | | | | (_| \__ (_) | | | |_) | (_) | (_) | |_ |_| |_|\__,_|___/\___/|_| |____/ \___/ \___/ \__|在后面的日志中还可以看到类似下面这些日志。2020-04-14 13:52:59.696 [main] INFO n.h.core.context.TemplateAppContext - loadModule class net.hasor.dataway.config.DatawayModule 2020-04-14 13:52:59.697 [main] INFO n.hasor.dataway.config.DatawayModule - dataway api workAt /api/ 2020-04-14 13:52:59.697 [main] INFO n.h.c.e.AbstractEnvironment - var -> HASOR_DATAQL_DATAWAY_API_URL = /api/. 2020-04-14 13:52:59.704 [main] INFO n.hasor.dataway.config.DatawayModule - dataway admin workAt /interface-ui/ 2020-04-14 13:52:59.716 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[901d38f22faa419a8593bb349905ed0e] -> bindType ‘class net.hasor.dataway.web.ApiDetailController’ mappingTo: ‘[/interface-ui/api/api-detail]’. 2020-04-14 13:52:59.716 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[c6eb9f3b3d4c4c8d8a4f807435538172] -> bindType ‘class net.hasor.dataway.web.ApiHistoryListController’ mappingTo: ‘[/interface-ui/api/api-history]’. 2020-04-14 13:52:59.717 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[eb841dc72ad54023957233ef602c4327] -> bindType ‘class net.hasor.dataway.web.ApiInfoController’ mappingTo: ‘[/interface-ui/api/api-info]’. 2020-04-14 13:52:59.717 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[96aebb46265245459ae21d558e530921] -> bindType ‘class net.hasor.dataway.web.ApiListController’ mappingTo: ‘[/interface-ui/api/api-list]’. 2020-04-14 13:52:59.718 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[7467c07f160244df8f228321f6262d3d] -> bindType ‘class net.hasor.dataway.web.ApiHistoryGetController’ mappingTo: ‘[/interface-ui/api/get-history]’. 2020-04-14 13:52:59.719 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[97d8da5363c741ba99d87c073a344412] -> bindType ‘class net.hasor.dataway.web.DisableController’ mappingTo: ‘[/interface-ui/api/disable]’. 2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[8ddc3316ef2642dfa4395ca8ac0fff04] -> bindType ‘class net.hasor.dataway.web.SmokeController’ mappingTo: ‘[/interface-ui/api/smoke]’. 2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[cc06c5fb343b471aacedc58fb2fe7bf8] -> bindType ‘class net.hasor.dataway.web.SaveApiController’ mappingTo: ‘[/interface-ui/api/save-api]’. 2020-04-14 13:52:59.720 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[7915b2b1f89a4e73891edab0264c9bd4] -> bindType ‘class net.hasor.dataway.web.PublishController’ mappingTo: ‘[/interface-ui/api/publish]’. 2020-04-14 13:52:59.721 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[0cfa34586455414591bdc389bff23ccb] -> bindType ‘class net.hasor.dataway.web.PerformController’ mappingTo: ‘[/interface-ui/api/perform]’. 2020-04-14 13:52:59.721 [main] INFO net.hasor.core.binder.ApiBinderWrap - mapingTo[37fe4af3e2994acb8deb72d21f02217c] -> bindType ‘class net.hasor.dataway.web.DeleteController’ mappingTo: ‘[/interface-ui/api/delete]’. 当看到 “dataway api workAt /api/” 、 dataway admin workAt /interface-ui/ 信息时，就可以确定 Dataway 的配置已经生效了。第七步：访问接口管理页面进行接口配置在浏览器中输入 “http://127.0.0.1:28080/interface-ui/” 就可以看到期待已久的界面了。第八步：新建一个接口Dataway 提供了2中语言模式，我们可以使用强大的 DataQL 查询语言，也可以直接使用 SQL 语言（在 Dataway 内部 SQL 语言也会被转换为 DataQL 的形式执行。）首先我们在 SQL 模式下尝试执行一条 select 查询，立刻就可以看到这条 SQL 的查询结果。同样的方式我们使用 DataQL 的方式需要这样写：var query = @@sql()<% select title,views from typecho_contents %> return query()其中 var query = @@sql()<% ... %> 是用来定义SQL外部代码块，并将这个定义存入 query 变量名中。<% %> 中间的就是 SQL 语句。最后在 DataQL 中调用这个代码块，并返回查询结果。当接口写好之后就可以保存发布了，为了测试方便，我选用 GET 方式。接口发布之后我们直接请求：http://localhost:28080/api/test，就看到期待已久的接口返回值了。最后总结经过上面的几个步骤我们介绍了如何基于 Spring Boot 项目使用 Dataway 来简单的配置接口。Dataway 的方式确实给人耳目一新，一个接口竟然可以如此简单的配置出来无需开发任何一行代码，也不需要做任何 Mapping 实体映射绑定。最后放几个有用的连接：Dataway 官方手册：https://www.hasor.net/web/dataway/about.htmlDataway 在 OSC 上的项目地址，欢迎收藏：https://www.oschina.net/p/datawayDataQL 手册地址：https://www.hasor.net/web/dataql/what_is_dataql.htmlHasor 项目的首页：https://www.hasor.net/web/index.html
智力题目有三个容积分别为3升、5升、8升的水桶，其中容积为8升的水桶中装满了水，容积为3升和容积为5升的水桶都是空的。三个水桶都没有刻度，现在需要将大水桶中的8升水等分成两份，每份都是4升水，附加条件是只能这三个水桶，不能借助其他辅助容器。“恩，是的，这是一个很经典的问题。”“然而，我们并不能想全，不信请继续往下看。”答案”废话不多说，直接看方法吧。“第一种（7步）将8L的水桶中的水，倒满5L的水桶，这时：8L水桶为3L、5L水桶为5L、3L水桶为0L将5L的水桶中的水，倒满3L的水桶，这时：8L水桶为3L、5L水桶为2L、3L水桶为3L将3L的水桶中的水，倒入8L的水桶，这时：8L水桶为6L、5L水桶为2L、3L水桶为0L将5L的水桶中的水，倒入3L的水桶，这时：8L水桶为6L、5L水桶为0L、3L水桶为2L将8L的水桶中的水，倒入5L的水桶，这时：8L水桶为1L、5L水桶为5L、3L水桶为2L将5L的水桶中的水，倒满3L的水桶，这时：8L水桶为1L、5L水桶为4L、3L水桶为3L将3L的水桶中的水，倒入8L的水桶，这时：8L水桶为4L、5L水桶为4L、3L水桶为0L第二种（8步）将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为5L、5L水桶为0L、3L水桶为3L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为5L、5L水桶为3L、3L水桶为0L将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为2L、5L水桶为3L、3L水桶为3L将3L的水桶中的水，倒满5L的水桶，这时：8L水桶为2L、5L水桶为5L、3L水桶为1L将5L的水桶中的水，倒入8L的水桶，这时：8L水桶为7L、5L水桶为0L、3L水桶为1L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为7L、5L水桶为1L、3L水桶为0L将8L的水桶中的水，倒满3L的水桶，这时：8L水桶为4L、5L水桶为1L、3L水桶为3L将3L的水桶中的水，倒入5L的水桶，这时：8L水桶为4L、5L水桶为4L、3L水桶为0L我相信答案肯定不止两个，到底有多少种答案？带着这个疑问，我们来设计一个算法吧。问题分析人的思维解决这个问题的关键是怎么通过倒水凑出确定的1升水或能容纳1升水的空间。例如，当8L水桶或5L水桶或3L水桶有1L水时，都能快速倒出4L水。计算机思维“穷举法”水桶初始状态：8L水桶装满水，3L和5L的水桶为空。水桶最终状态：3L水桶为空，5L和8L的水桶各4L水。假设将每个状态下三个水桶中的水的体积作为status。从 status = array(8,0,0) 得到 status = array(4,4,0)。当然还会有一些限制：各个水桶的都有最大值：当前倒水之后各个水桶的状态，与历史倒水之后各个水桶的状态，不能相同。当前水桶为空时，不能倒给其他水桶。当前水桶为最大容积时，其他水桶不能再向这个水桶倒水。BucketWater.java桶的实体类，用于包装容量和剩余的水public class BucketWater implements Cloneable { private int index; private int capacity; private int remainWater; public BucketWater() { } public BucketWater(int index, int remainWater , int capacity) { this.index = index; this.remainWater = remainWater; this.capacity = capacity; } public int getRemainWater() { return remainWater; } public void setRemainWater(int remainWater) { this.remainWater = remainWater; } public int getIndex() { return index; } public void setIndex(int index) { this.index = index; } public int getCapacity() { return capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } @Override public BucketWater clone() throws CloneNotSupportedException { return (BucketWater)super.clone(); } }BucketStatus.java桶状态的实体类，主要为了保存3个桶创建的对象public class BucketStatus implements Cloneable { private BucketWater[] bucketWaterAry = new BucketWater[3]; public BucketStatus() { } public BucketWater[] getBucketWaterAry() { return bucketWaterAry; } public void setBucketWaterAry(BucketWater[] bucketWaterAry) { this.bucketWaterAry = bucketWaterAry; } @Override public BucketStatus clone() throws CloneNotSupportedException { BucketStatus bucketStatus = (BucketStatus)super.clone(); BucketWater[] bucketWaterAry = new BucketWater[3]; for(int i = 0; i < this.bucketWaterAry.length ; i ++){ bucketWaterAry[i] = this.bucketWaterAry[i].clone(); } bucketStatus.bucketWaterAry = bucketWaterAry; return bucketStatus; } }Test.java 主类，包含了所有算法和流程public class Test{ private static int index = 0; private static Stack<BucketStatus> bucketStatusQueue = new Stack<>(); public static void main( String[] args ) throws CloneNotSupportedException { BucketWater bucketWater1 = new BucketWater(1, 8 , 8); BucketWater bucketWater2 = new BucketWater(2, 0 , 5); BucketWater bucketWater3 = new BucketWater(3, 0 , 3); BucketStatus bucketStatus = new BucketStatus(); bucketStatus.getBucketWaterAry()[0] = bucketWater1; bucketStatus.getBucketWaterAry()[1] = bucketWater2; bucketStatus.getBucketWaterAry()[2] = bucketWater3; bucketStatusQueue.add(bucketStatus); search(); } /** * 核心查找方法 * @throws CloneNotSupportedException */ public static void search() throws CloneNotSupportedException { //从栈中获取顶端的元素 BucketStatus bucketStatus = bucketStatusQueue.peek(); //判断是否结束，若结束了打印日志 //放在上面判断因为递归必须需要进入此方法，方法退出后才会pop，在这里打印可以获取所有路径 if(isEnd(bucketStatus)){ logProcess(); } BucketWater[] bucketWaterAry = bucketStatus.getBucketWaterAry(); for(int i = 0 ; i < bucketWaterAry.length ; i ++){ for(int j = 0 ; j < bucketWaterAry.length ; j ++){ //判断是否可以倒水 并获取可以倒水的量 int pushWater = canPush(bucketWaterAry[i] , bucketWaterAry[j]); if(pushWater == 0 ) { continue; } //这里返回的对象已经被clone了，没有指针引用的问题 //在后期处理直接压入栈 BucketStatus nextBucketStatus = push(bucketStatus , i , j , pushWater); //判断当前的状态是否已经处理过 循环栈即可获得 //在这里不需要添加到的水量，可以想象为一个树型结构，即该节点是否处理过，以节点为一个维度 if(isAlreadyProcess(nextBucketStatus)){ continue; } bucketStatusQueue.add(nextBucketStatus); search(); //pop只会pop扫描到的子节点 bucketStatusQueue.pop(); } } } /** * 倒水 */ public static BucketStatus push(BucketStatus bucketStatus , Integer fromIndex, Integer toIndex , int pushWater) throws CloneNotSupportedException { BucketStatus copyBucketStatus = bucketStatus.clone(); BucketWater bucketWaterFrom = copyBucketStatus.getBucketWaterAry()[fromIndex]; BucketWater bucketWaterTo = copyBucketStatus.getBucketWaterAry()[toIndex]; bucketWaterFrom.setRemainWater(bucketWaterFrom.getRemainWater() - pushWater); bucketWaterTo.setRemainWater(bucketWaterTo.getRemainWater() + pushWater); return copyBucketStatus; } /** * 判断是否可以倒入水 * @param bucketWaterFrom * @param bucketWaterTo * @return */ public static int canPush(BucketWater bucketWaterFrom , BucketWater bucketWaterTo){ //相同的水桶号 不能倒入水 if(bucketWaterFrom.getIndex() == bucketWaterTo.getIndex()){ return 0; } //满的桶 不能导入水 if(bucketWaterTo.getRemainWater() == bucketWaterTo.getCapacity()){ return 0; } //没有水 不能倒 if(bucketWaterFrom.getRemainWater() == 0){ return 0; } //可以倒入的水 int canPushWater = bucketWaterTo.getCapacity() - bucketWaterTo.getRemainWater(); return bucketWaterFrom.getRemainWater() <= canPushWater ? bucketWaterFrom.getRemainWater() : canPushWater; } /** * 判断当前这个流程是否之前处理过 * @return */ public static boolean isAlreadyProcess(BucketStatus bucketStatus){ Iterator<BucketStatus> iterator = bucketStatusQueue.iterator(); while(iterator.hasNext()){ BucketStatus bucket = iterator.next(); if(bucket.getBucketWaterAry()[0].getRemainWater() == bucketStatus.getBucketWaterAry()[0].getRemainWater() && bucket.getBucketWaterAry()[1].getRemainWater() == bucketStatus.getBucketWaterAry()[1].getRemainWater() && bucket.getBucketWaterAry()[2].getRemainWater() == bucketStatus.getBucketWaterAry()[2].getRemainWater()){ return true; } } return false; } /** * 判断是否查找到了解决路径 * @param bucketStatus * @return */ public static boolean isEnd(BucketStatus bucketStatus){ int index = 2; for(BucketWater bucketWater : bucketStatus.getBucketWaterAry()){ if(bucketWater.getRemainWater() == 4){ index--; } } return index == 0; } /** * 成功时输出 */ public static void logProcess(){ index++; Iterator<BucketStatus> iterator = bucketStatusQueue.iterator(); System.out.print(index + ":"); while(iterator.hasNext()) { BucketStatus bucket = iterator.next(); System.out.print("[" + bucket.getBucketWaterAry()[0].getRemainWater() + "," + bucket.getBucketWaterAry()[1].getRemainWater() + "," + bucket.getBucketWaterAry()[2].getRemainWater() + "] ==>"); } System.out.println(); } }运行结果:1:[8,0,0] ==>[3,5,0] ==>[0,5,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 2:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[0,5,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 3:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 4:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[0,5,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 5:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[0,5,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 6:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 7:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 8:[8,0,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 9:[8,0,0] ==>[5,0,3] ==>[0,5,3] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 10:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 11:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[0,5,3] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 12:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[0,5,3] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 13:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 14:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[0,5,3] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==> 15:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[7,0,1] ==>[7,1,0] ==>[4,1,3] ==>[4,4,0] ==> 16:[8,0,0] ==>[5,0,3] ==>[5,3,0] ==>[2,3,3] ==>[2,5,1] ==>[3,5,0] ==>[3,2,3] ==>[6,2,0] ==>[6,0,2] ==>[1,5,2] ==>[1,4,3] ==>[4,4,0] ==>一共有 16 种倒水方法，方法如下：
一，前言首先说明一下MySQL的版本：mysql> select version(); +-----------+ | version() | +-----------+ | 5.7.17 |row in set (0.00 sec)表结构：mysql> desc test; +--------+---------------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +--------+---------------------+------+-----+---------+----------------+ | id | bigint(20) unsigned | NO | PRI | NULL | auto_increment | | val | int(10) unsigned | NO | MUL | 0 | | | source | int(10) unsigned | NO | | 0 | |rows in set (0.00 sec)id为自增主键，val为非唯一索引。灌入大量数据，共500万：mysql> select count(*) from test; +----------+ | count(*) | +----------+ | 5242882 |row in set (4.25 sec)我们知道，当limit offset rows中的offset很大时，会出现效率问题：mysql> select * from test where val=4 limit 300000,5;idvalsource332762244332763244332764244332765244332766244rows in set (15.98 sec)为了达到相同的目的，我们一般会改写成如下语句：mysql> select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id;idvalsourceid33276224433276223327632443327632332764244332764233276524433276523327662443327662rows in set (0.38 sec)时间相差很明显。为什么会出现上面的结果？我们看一下select * from test where val=4 limit 300000,5;的查询过程：查询到索引叶子节点数据。根据叶子节点上的主键值去聚簇索引上查询需要的全部字段值。类似于下面这张图：像上面这样，需要查询300005次索引节点，查询300005次聚簇索引的数据，最后再将结果过滤掉前300000条，取出最后5条。MySQL耗费了大量随机I/O在查询聚簇索引的数据上，而有300000次随机I/O查询到的数据是不会出现在结果集当中的。肯定会有人问：既然一开始是利用索引的，为什么不先沿着索引叶子节点查询到最后需要的5个节点，然后再去聚簇索引中查询实际数据。这样只需要5次随机I/O，类似于下面图片的过程：其实我也想问这个问题。证实下面我们实际操作一下来证实上述的推论：为了证实select * from test where val=4 limit 300000,5是扫描300005个索引节点和300005个聚簇索引上的数据节点，我们需要知道MySQL有没有办法统计在一个sql中通过索引节点查询数据节点的次数。我先试了Handler_read_*系列，很遗憾没有一个变量能满足条件。我只能通过间接的方式来证实：InnoDB中有buffer pool。里面存有最近访问过的数据页，包括数据页和索引页。所以我们需要运行两个sql，来比较buffer pool中的数据页的数量。预测结果是运行select * from test a inner join (select id from test where val=4 limit 300000,5) b>之后，buffer pool中的数据页的数量远远少于select * from test where val=4 limit 300000,5;对应的数量，因为前一个sql只访问5次数据页，而后一个sql访问300005次数据页。select * from test where val=4 limit 300000,5 mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name; Empty set (0.04 sec)可以看出，目前buffer pool中没有关于test表的数据页。mysql> select * from test where val=4 limit 300000,5; +---------+-----+--------+ | id | val | source | +---------+-----+--------+ | 3327622 | 4 | 4 | | 3327632 | 4 | 4 | | 3327642 | 4 | 4 | | 3327652 | 4 | 4 | | 3327662 | 4 | 4 | +---------+-----+--------+ 5 rows in set (26.19 sec)mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name; +------------+----------+ | index_name | count(*) | +------------+----------+ | PRIMARY | 4098 | | val | 208 | +------------+----------+ 2 rows in set (0.04 sec)可以看出，此时buffer pool中关于test表有4098个数据页，208个索引页。select * from test a inner join (select id from test where val=4 limit 300000,5)` b>为了防止上次试验的影响，我们需要清空buffer pool，重启mysql。mysqladmin shutdown /usr/local/bin/mysqld_safe & mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name; Empty set (0.03 sec)运行sql：mysql> select * from test a inner join (select id from test where val=4 limit 300000,5) b on a.id=b.id; +---------+-----+--------+---------+ | id | val | source | id | +---------+-----+--------+---------+ | 3327622 | 4 | 4 | 3327622 | | 3327632 | 4 | 4 | 3327632 | | 3327642 | 4 | 4 | 3327642 | | 3327652 | 4 | 4 | 3327652 | | 3327662 | 4 | 4 | 3327662 | +---------+-----+--------+---------+ 5 rows in set (0.09 sec) mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('val','primary') and TABLE_NAME like '%test%' group by index_name; +------------+----------+ | index_name | count(*) | +------------+----------+ | PRIMARY | 5 | | val | 390 | +------------+----------+ 2 rows in set (0.03 sec)我们可以看明显的看出两者的差别：第一个sql加载了4098个数据页到buffer pool，而第二个sql只加载了5个数据页到buffer pool。符合我们的预测。也证实了为什么第一个sql会慢：读取大量的无用数据行（300000），最后却抛弃掉。而且这会造成一个问题：加载了很多热点不是很高的数据页到buffer pool，会造成buffer pool的污染，占用buffer pool的空间。遇到的问题为了在每次重启时确保清空buffer pool，我们需要关闭innodb_buffer_pool_dump_at_shutdown和innodb_buffer_pool_load_at_startup，这两个选项能够控制数据库关闭时dump出buffer pool中的数据和在数据库开启时载入在磁盘上备份buffer pool的数据。
为什么Mysql的常用引擎都默认使用B+树作为索引？
一、前言为了讲清楚这个问题，阿粉先带大家了解一下什么是索引。我记得刚刚学习数据库的时候，老师喜欢用书本的目录来类比数据库的索引，并告诉我们索引能够像目录一样，让我们更快地找到想要找到的数据。如果是第一次接触索引，这个比喻能够让我们有一个直观的印象。但是当深入去学习索引的时候，我们不能继续持有索引即目录的思想，我们要跳出来去思考索引的本质是什么。二、索引的本质在没有索引的情况下，我们查找数据只能按照从头到尾的顺序逐行查找，每查找一行数据，意味着我们要到到磁盘相应的位置去读取一条数据。如果把查询一条数据分为到磁盘中查询和比对查询条件两步的话，到磁盘中查询的时间会远远大于比对查询条件的时间，这意味着在一次查询中，磁盘io占用了大部分的时间。更进一步地说，一次查询的效率取绝于磁盘io的次数，如果我们能够在一次查询中尽可能地降低磁盘io的次数，那么我们就能加快查询的速度。在知道了减少磁盘io能加快查询速度后，我们就要聚焦于如何减少磁盘io。如果按照原表逐行查询的话，n条数据就要查询n次，也就是O(N)的时间复杂度，为了减少磁盘io的次数，我们必须用一种查询时间复杂度更低的数据结构来保存数据。这种查询时间复杂度低的数据结构，我们称之为索引。所以通俗来说，索引其实就是某种数据结构，能充当索引的数据结构是多种多样的。三、索引的选择既然索引是一种便于查询的数据结构，如果大家对数据结构有一定了解的话，大概率会首选树型结构。毕竟树型结构普遍有着O(logN)的查询时间复杂度，而且插入删除数据的性能也比较平均。（可能你会说数组，哈希表的查询速度也很高啊，这个后面也会分析）虽然我们都已经知道Mysql中最常用的引擎像InnoDB和MyISAM，最终都选择了B+树作为索引，但是这里我还是打算从最常见的二叉树开始讲起，推导一下为什么最终选择了B+树作为索引，并比较一下几种树型结构在充当索引时的优劣。二叉树最普通的二叉树的问题在于他不能保证O(logN)的查询时间复杂度，我们看下面的图：由于插入的元素逐渐增大，元素始终在右边进行插入，好好的一棵二叉树最终变成了一条“链表”。在这种极端的情况下，二叉树的查询时间复杂度不再是O(logN)，而是退化为O(N)，这样显然不符合索引的要求。平衡二叉树（红黑树）像红黑树这样的平衡二叉树，无论如何插入元素，他都可以通过一些旋转的方法调整树的高度，使得整棵树的查询效率维持在O(logN)，如下图所示：这么来说他已经符合了成为索引的必备条件，但是最终没有选择他作为索引说明还有不足的地方。仔细看看可以发现平衡二叉树的每个节点只有两个孩子节点，如果一张表的数据量特别大，整棵树的高度也会随之上升。一个千万级别的表如果用平衡二叉树作为索引的话，树高将会达到二十多层。这也就意味着做一次查询需要二十多次磁盘io，这是一个不小的开销。那么有没有能在大数据量的情况下，还能保持一个较小树高的树型结构呢？B树和B+树答案就是B树。上面我们说到了平衡二叉树的瓶颈在于一个节点只有两个孩子节点，而B树一个节点可以存放N个孩子节点，这就完美解决了树高的问题，我们可以把B树称为平衡多叉树，B树作为索引如下图所示：但是以B树的结构作为索引仍有可以优化的地方，我们先看看最终的B+树，再仔细分析B+树在B树的基础上作了哪些改进，为什么B+树最终能够胜任索引的工作：从图片中可以看到B+树同样是一棵多差平衡树，和B树一样很好地解决了树高的问题。改进点一：但仔细看可以发现，B树的节点中既存储索引，也存储表对应的数据；而B+树的非叶子节点是不存储数据的，只存储索引，数据全部存储在叶子节点上。为什么要做这样的改进？我们做一次算术就知道了。假设树高为2，主键ID为bigint类型，长度为8字节，节点指针为6字节，一行数据记录的大小为1k，一次io操作能获得一页16k的数据。在索引为B+树的情况下，根节点能存储：16k / （6 + 8） = 1170 条索引指针；到了第一层，一共能指向 1170 1170 = 1368900 条索引指针；到了最底一层叶子节点，一个节点能存储16k / 1k = 16 条记录，一共能存储 1170 1170 * 16 = 21902400 条记录在B树的情况下，由于非叶子节点使用了大量空间存储数据，存放的索引指针肯定就少，最终整棵树如果想要存储和B+树一样多的数据就必须要增加树高，这样一来就增加了磁盘io，所以说B+树作为索引的性能比B树高。改进点二：叶子节点之间使用指针连接，提高区间访问效率。如果我们要进行范围查询，可以轻松通过B+树叶子节点之间的指针进行遍历，减少了不必要的磁盘io。总结看到这里，相信大家对为什么Mysql的常用引擎都默认使用B+树作为索引已经有了初步的认知。我们只要牢记一点：索引是为了减少磁盘io提高查询性能而存在的。最后回应一下为什么不常用哈希表和数组作为索引哈希表虽然单一个值的查询效率很高，但是撑不住范围查询，哪个公司的业务还没个范围查询呢？而数组虽然查询的效率高，但是增加和删除的效率低，由于记录在增加和删除的时候索引也得跟着维护，这会导致大数据量的情况下，增加或删除一条记录效率较低