diff --git a/Java基础教程/Java学习路线.md b/Java基础教程/Java学习路线.md index ef48c58e..87e26c2d 100644 --- a/Java基础教程/Java学习路线.md +++ b/Java基础教程/Java学习路线.md @@ -11,9 +11,10 @@ * [ ] 设计模式 * [ ] Java基础教程(Java的基本语法和使用及原理,晚上自学,第一周学完) * [X] Java语言基础。语言语法。:20200830 - * [ ] Java标注库开发.(自动拆装箱、String、集合类、枚举类、IO类、反射、动态代理、序列化、注解、泛型、单元测试、正则表达式、工具库、异常、日期时间、编码方式) + * [x] Java标注库.(自动拆装箱、String枚举类、反射、动态代理、序列化、注解、泛型、单元测试、正则表达式、工具库、异常、日期时间、编码方式) + * [x] Java集合类 + * [x] JavaIO与网络编程(网络基础、Socket编程) * [ ] Java并发编程(线程、锁、同步、并发包) - * [ ] Java网络编程(网络基础、Socket编程) * [ ] Java基本原理。JVM底层的原理和技术。(内存结构、垃圾回收) * [ ] Java架构模式。面向对象和设计模式 * [ ] Java网站开发(JavaWeb相关的技术知识。) @@ -72,4 +73,11 @@ > 一份可以参考的文档。其中大数据、网络安全和扩展篇不学。 -> ![](image/2022-10-27-20-26-36.png) \ No newline at end of file +> ![](image/2022-10-27-20-26-36.png) + + +> 找到了两个很不得了的东西。如果有时间,可以把这两个文档全部重新看一遍。完全有书籍的质量的基础知识介绍。常看常新。自己接下来的笔记就从这上边找吧。 +> +> 入门小站。https://rumenz.com/ +> BestJavaer。https://github.com/crisxuan/bestJavaer +> \ No newline at end of file diff --git a/Java基础教程/Java容器/10 Java容器.md b/Java基础教程/Java容器/10 Java容器.md deleted file mode 100644 index c6f694f1..00000000 --- a/Java基础教程/Java容器/10 Java容器.md +++ /dev/null @@ -1,31 +0,0 @@ - -数据结构分为 - -* 线性数据结构 -* 树型数据结构 -* 图型数据结构 - - -C++中的容器分为(都是线性的) -* 顺序容器 - * array 数组 - * vector向量 - * list 链表 -* 关联容器 - * map 映射 - * set 集合 -* 容器适配器 - * stack 栈 - * queue 队列 - - -Java中的容器分为(都是线性的) -* 集合collection - * List - * Queue - * Map - * Set - -![](image/2022-11-08-10-51-54.png) - -![](image/2022-11-08-10-54-19.png) \ No newline at end of file diff --git a/Java基础教程/Java容器/11 JavaList.md b/Java基础教程/Java容器/11 JavaList.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Java基础教程/Java标准库/01 Object类.md b/Java基础教程/Java标准库/01 Object类.md index 257e6674..0dfdd4fc 100644 --- a/Java基础教程/Java标准库/01 Object类.md +++ b/Java基础教程/Java标准库/01 Object类.md @@ -2,6 +2,10 @@ ### 概览 +`java.lang.Object`类是Java语言中的根类,即所有类的父类。它中描述的所有方法子类都可以使用。在对象实例化的时候,最终找的父类就是Object。 + +如果一个类没有特别指定父类, 那么默认则继承自Object类。 + ```java public native int hashCode() @@ -354,3 +358,38 @@ CloneConstructorExample e2 = new CloneConstructorExample(e1); e1.set(2, 222); System.out.println(e2.get(2)); // 2 ``` + + +## 2 Objects工具类 + + +Objects类是对象工具类,它里面的的方法都是用来操作对象的。 + +### equals方法 + +在**JDK7**添加了一个Objects工具类,它提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或null-tolerant(容忍空指针的),用于计算对象的hashcode、返回对象的字符串表示形式、比较两个对象。 + +在比较两个对象的时候,Object的equals方法容易抛出空指针异常,而Objects类中的equals方法就优化了这个问题。方法如下: + +* `public static boolean equals(Object a, Object b)`:判断两个对象是否相等。 + +我们可以查看一下源码,学习一下: + +```java +public static boolean equals(Object a, Object b) { + return (a == b) || (a != null && a.equals(b)); +} +``` + +### isNull + +`static boolean isNull(Object obj)` 判断对象是否为null,如果为null返回true。 + +```java +Student s1 = null; +Student s2 = new Student("蔡徐坤", 22); + +// static boolean isNull(Object obj) 判断对象是否为null,如果为null返回true +System.out.println(Objects.isNull(s1)); // true +System.out.println(Objects.isNull(s2)); // false +``` \ No newline at end of file diff --git a/Java基础教程/Java标准库/02 包装器类.md b/Java基础教程/Java标准库/02 包装器类.md index 76ca3b53..b275b0a0 100644 --- a/Java基础教程/Java标准库/02 包装器类.md +++ b/Java基础教程/Java标准库/02 包装器类.md @@ -34,7 +34,19 @@ Integer i = new Interger(1); Double d = Double.valueOf(3.14); ``` -### 包装器类转换 +### 装箱拆箱 + +* 装箱Boxing: 将基本类型转化为包装器类型 包装器类.valueOf(基本数据类型变量或常量)。装箱共享内存。 +* 拆箱unBoxing:将包装器类型转化为基本数据类型XX.XXXvalue();拆箱也共享内存 + +装箱操作 + +```java +Integer i = Integer.valueOf(10);//10是基本数据类型,i是包装器类型 +int n = i.intValue();//i是包装器类型,n是包装器类型 +``` + +拆箱操作 ```java Boolean.booleanValue() @@ -47,17 +59,18 @@ Float.floatValue() Double.doubleValue() ``` -* 装箱Boxing: 将基本类型转化为包装器类型 包装器类.valueOf(基本数据类型变量或常量)。装箱共享内存。 -* 拆箱unBoxing:将包装器类型转化为基本数据类型XX.XXXvalue();拆箱也共享内存 - +可以自动进行拆箱装箱 ```java -Integer i = Integer.valueOf(10);//10是基本数据类型,i是包装器类型 -int n = i.intValue();//i是包装器类型,n是包装器类型 +Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4); +i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5; +//加法运算完成后,再次装箱,把基本数值转成对象。 + ``` -### 包装器类 + +> 注意事项 * 对象一旦赋值,其值不能在改变。 * ++/--自增自减运算符只能对基本数据类型操作 -* 集合中只能存放帮装起类型的对象 +* 集合中只能存放包装器类型的对象 @@ -175,4 +188,45 @@ System.out.println(m == n); // true 7. toLowerCase() 指定字母的小写形式 8. toString() -返回字符的字符串形式,字符串的长度仅为1 \ No newline at end of file +返回字符的字符串形式,字符串的长度仅为1 + +## 2 Integer类 + +- Integer类概述 + + 包装一个对象中的原始类型 int 的值 + +- Integer类构造方法及静态方法 + +| 方法名 | 说明 | +| --------------------------------------- | -------------------------------------- | +| public Integer(int value) | 根据 int 值创建 Integer 对象(过时) | +| public Integer(String s) | 根据 String 值创建 Integer 对象(过时) | +| public static Integer valueOf(int i) | 返回表示指定的 int 值的 Integer 实例 | +| public static Integer valueOf(String s) | 返回保存指定String值的 Integer 对象 | + +- 示例代码 + +```java +public class IntegerDemo { + public static void main(String[] args) { + //public Integer(int value):根据 int 值创建 Integer 对象(过时) + Integer i1 = new Integer(100); + System.out.println(i1); + + //public Integer(String s):根据 String 值创建 Integer 对象(过时) + Integer i2 = new Integer("100"); + //Integer i2 = new Integer("abc"); //NumberFormatException + System.out.println(i2); + System.out.println("--------"); + + //public static Integer valueOf(int i):返回表示指定的 int 值的 Integer 实例 + Integer i3 = Integer.valueOf(100); + System.out.println(i3); + + //public static Integer valueOf(String s):返回保存指定String值的Integer对象 + Integer i4 = Integer.valueOf("100"); + System.out.println(i4); + } +} +``` \ No newline at end of file diff --git a/Java基础教程/Java标准库/03 String类.md b/Java基础教程/Java标准库/03 String类.md index 1ab4c3f9..586cbf2e 100644 --- a/Java基础教程/Java标准库/03 String类.md +++ b/Java基础教程/Java标准库/03 String类.md @@ -198,7 +198,7 @@ String s5 = new String("Runoob"); // String 对象创建 * String(byte数组,起始下标,长度) * String(StringBuffer buffer) * String(StringBuilder builder) -### 关键方法 +### 字符串拼接 * 字符串链接concat和+号的功能类似。 @@ -207,6 +207,7 @@ String s5 = new String("Runoob"); // String 对象创建 "Hello," + " runoob" + "!" ``` +### 格式化方法 * 创建格式化字符串printf和format两个方法 ```java @@ -216,13 +217,13 @@ System.out.printf("浮点型变量的值为 " + "is %s", floatVar, intVar, stringVar); ``` -``` -String fs; -fs = String.format("浮点型变量的值为 " + - "%f, 整型变量的值为 " + - " %d, 字符串变量的值为 " + - " %s", floatVar, intVar, stringVar); -``` +### 带正则表达式的方法 +* boolean matches(String regex)告知此字符串是否匹配给定的正则表达式。 +* String replaceAll(String regex, String replacement)使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 +* String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 +* String[] split(String regex)根据给定正则表达式的匹配拆分此字符串。 +* String[] split(String regex, int limit)根据匹配给定的正则表达式来拆分此字符串。 + ### 其他方法 * char charAt(int index)返回指定索引处的 char 值。 @@ -250,14 +251,11 @@ fs = String.format("浮点型变量的值为 " + * int lastIndexOf(String str)返回指定子字符串在此字符串中最右边出现处的索引。 * int lastIndexOf(String str, int fromIndex) 返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。 * int length()返回此字符串的长度。 -* boolean matches(String regex)告知此字符串是否匹配给定的正则表达式。 + * boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等。 * boolean regionMatches(int toffset, String other, int ooffset, int len)测试两个字符串区域是否相等。 * String replace(char oldChar, char newChar)返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 -* String replaceAll(String regex, String replacement)使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 -* String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 -* String[] split(String regex)根据给定正则表达式的匹配拆分此字符串。 -* String[] split(String regex, int limit)根据匹配给定的正则表达式来拆分此字符串。 + * boolean startsWith(String prefix)测试此字符串是否以指定的前缀开始。 * boolean startsWith(String prefix, int toffset)测试此字符串从指定索引开始的子字符串是否以指定前缀开始。 * CharSequence subSequence(int beginIndex, int endIndex) 返回一个新的字符序列,它是此序列的一个子序列。 @@ -290,6 +288,10 @@ StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不 * 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。 +StringBuffer的内部实现方式和String不同. +* StringBuffer在进行字符串处理时,不生成新的对象,在内存使用上要优于String类。所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入、删除等操作,使用StringBuffer要更加适合一些。 +* String:在String类中没有用来改变已有字符串中的某个字符的方法,由于不能改变一个java字符串中的某个单独字符,所以在JDK文档中称String类的对象是不可改变的。然而,不可改变的字符串具有一个很大的优点:编译器可以把字符串设为共享的。 + ### 使用 ```java @@ -343,3 +345,185 @@ public class RunoobTest{ +## 4 字符串格式化 + +利用了Formatter类。printf、printfln等方法原理一直。 + +字符串的格式化相当于将字符串按照指定的格式进行toString(),一般有两种形式: + +```java +//使用指定的格式字符串和参数返回一个格式化字符串。 + public static String format(String format, Object... args) { + return new Formatter().format(format, args).toString(); + } + +//使用指定的语言环境、格式字符串和参数返回一个格式化字符串。 +public static String format(Locale l, String format, Object... args) { + return new Formatter(l).format(format, args).toString(); + } +``` +* 实例 + +``` +String fs; +fs = String.format("浮点型变量的值为 " + + "%f, 整型变量的值为 " + + " %d, 字符串变量的值为 " + + " %s", floatVar, intVar, stringVar); +``` + +### 数据转化符 + +| 数据类型 | 说明 | 转化形式 | +|------|------------|------------| +| %s | 字符串类型 | “string” | +| %c | 字符类型 | ‘A’ | +| %b | 布尔类型 | true/false | +| %o | 整数类型(八进制) | 111 | +| %d | 整数类型(十进制) | 17 | +| %x | 整数类型(十六进制) | 11 | +| %f | 浮点类型(基本) | 66.66 | +| %e | 指数类型 | 1.11e+5 | +| %a | 浮点类型(十六进制) | FF.22 | +| %h | 散列码 | 11 | +| %% | 百分比类型 | 17% | +| %n | 换行符 | +| %tx | 日期与时间类型 | + +```java +public class Format01 { + public static void main(String[] args) { + System.out.println(String.format("字符串:%s", "String")); + System.out.println(String.format("字符:%c", 'M')); + System.out.println(String.format("布尔类型:%b", 'M'>'A')); + System.out.println(String.format("八进制整数类型:%o", 17)); + System.out.println(String.format("十进制整数类型:%d", 17)); + System.out.println(String.format("十六进制整数类型:%x", 17)); + System.out.println(String.format("基本浮点类型:%f", 99.1)); + System.out.println(String.format("指数类型:%e", 100.111111111)); + System.out.println(String.format("十六进制浮点类型:%a", 17.111111)); + System.out.println(String.format("散列码:%h", 17)); + System.out.println(String.format("百分比类型:17%%")); + System.out.print(String.format("换行符:%n", 17)); + } + +} +``` + +### 格式化控制符 + +| 标志 | 说明 | 示例 | 输出 | +|---|---|---|---| +| + | 为正数添加符号 | (“正数:%+f”,11.11)) | 正数:+11.110000 | +| - | 左对齐 | (“左对齐:%-5d”,11) | 左对齐:11 | +| 0 | 整数前面补0 | (“数字前面补0:%04d”,11) | 数字前面补0:0011 | +| , | 对数字分组 | (“按,对数字分组:%,d”,111111111) | 按,对数字分组:111,111,111 | +| 空格 | 数字前面补空格 | (“空格:% 4d”,11) | 空格: 11 | +| ( | 包含负数 | (“使用括号包含负数:%(f”,-11.11) | 使用括号包含负数:(11.110000) | +| # | 浮点数包含小数,八进制包含0,十六进制包含0x | +| < | 格式化前一个转换符描述的参数 | (“格式化前描述的参数:%f转化后%❤️.3f”,111.1111111) | 格式化前描述的参数:111.111111转化后111.111 | +| $ | 被格式化的参数索引 | (“被格式化的参数索引:%1 + + +```java +public class formatString { + public static void main(String[] args) { + System.out.println(String.format("正数:%+f",11.11)); + System.out.println(String.format("右对齐:%+10d",11)); + System.out.println(String.format("左对齐:%-5d",11)); + System.out.println(String.format("数字前面补0:%044d",11)); + System.out.println(String.format("空格:% 4d",11)); + System.out.println(String.format("按,对数字分组:%,d",111111111)); + System.out.println(String.format("使用括号包含负数:%(f",-11.11)); + System.out.println(String.format("浮点数包含小数点:%#f",11.1)); + System.out.println(String.format("八进制包含0:%#o",11)); + System.out.println(String.format("十六进制包含0x:%#x",11)); + System.out.println(String.format("格式化前描述的参数:%f转化后%<3.3f",111.1111111)); + System.out.println(String.format("被格式化的参数索引:%1$d,%2$s",11,"111.1111111")); + + } + +} +``` + +### 日期格式化符 + +| 转换符 | 说明 | 示例 | +|---|---|---| +| c | 全部时间日期 | 星期四 十二月 17 13:11:35 CST 2020 | +| F | 年-月-日格式 | 2020-12-17 | +| D | 月/日/年格式 | 12/17/20 | +| r | HH:MM:SS PM格式(12时制) | 01:11:35 下午 | +| T | HH:MM:SS格式(24时制) | 13:11:35 | +| R | HH:MM格式(24时制) | 13:11 | + + +```java +public class formatDate { + public static void main(String[] args) { + Date date = new Date(); + System.out.println(String.format("全部时间日期:%tc",date)); + System.out.println(String.format("年-月-日格式:%tF",date)); + System.out.println(String.format("月/日/年格式:%tD",date)); + System.out.println(String.format("HH:MM:SS PM格式(12时制):%tr",date)); + System.out.println(String.format("HH:MM:SS格式(24时制):%tT",date)); + System.out.println(String.format("HH:MM格式(24时制):%tR",date)); + } +} +``` + + +### 时间格式化符 + +| 转换符 | 说明 | 示例 | +|---|---|---| +| H | 2位数字24时制的小时(不足2位前面补0) | 13 | +| l | 2位数字12时制的小时 | 1 | +| k | 2位数字24时制的小时 | 13 | +| M | 2位数字的分钟 | 33 | +| L | 3位数字的毫秒 | 745 | +| S | 2位数字的秒 | 33 | +| N | 9位数字的毫秒数 | 745000000 | +| p | Locale.US,"小写字母的上午或下午标记(英) | 下午 | +| Z | 时区缩写字符串 | CST | +| z | 相对于GMT的RFC822时区的偏移量 | +0800 | +| s | 1970-1-1 00:00:00 到现在所经过的秒数 | 1608183213 | +| Q | 1970-1-1 00:00:00 到现在所经过的毫秒数 | 1608183213745 | + +```java +public class formatTime { + public static void main(String[] args) { + Date date = new Date(); + System.out.println(String.format("2位数字24时制的小时(不足2位前面补0):%tH", date)); + System.out.println(String.format("2位数字12时制的小时:%tl", date)); + System.out.println(String.format("2位数字24时制的小时:%tk", date)); + System.out.println(String.format("2位数字的分钟:%tM", date)); + System.out.println(String.format("3位数字的毫秒:%tL", date)); + System.out.println(String.format("2位数字的秒:%tS", date)); + System.out.println(String.format("9位数字的毫秒数:%tN", date)); + System.out.println(String.format("时区缩写字符串:%tZ", date)); + System.out.println(String.format("相对于GMT的RFC822时区的偏移量:%tz", date)); + System.out.println(String.format("Locale.US,\"小写字母的上午或下午标记(英):%tp", date)); + System.out.println(String.format("1970-1-1 00:00:00 到现在所经过的秒数:%ts", date)); + System.out.println(String.format("1970-1-1 00:00:00 到现在所经过的毫秒数:%tQ", date)); + + } + +} +``` +### 类型转换 + +* 其他类型转字符串 + +```java +1.String s=""+i; +2.String s=Integer.toString(i); +3.String s=String.valueOf(i); +``` + +* 字符串转其他类型 + +```java +1.int i=Integer.parsenInt(s); +2.int i=Integer.valueOf(s).intValue(); +``` diff --git a/Java基础教程/Java标准库/04 Math类.md b/Java基础教程/Java标准库/04 Math类.md deleted file mode 100644 index b1d38b24..00000000 --- a/Java基础教程/Java标准库/04 Math类.md +++ /dev/null @@ -1,28 +0,0 @@ -## 1 概述 - -### Math类 -Java 的 Math 包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。 - -### Math中的常量 -* Math.PI 记录的圆周率 -* Math.E 记录e的常量 - -### Math中的函数 -* Math.abs 求绝对值 -* Math.sin 正弦函数 Math.asin 反正弦函数 -* Math.cos 余弦函数 Math.acos 反余弦函数 -* Math.tan 正切函数 Math.atan 反正切函数 Math.atan2 商的反正切函数 -* Math.toDegrees 弧度转化为角度 Math.toRadians 角度转化为弧度 -* Math.ceil 得到不小于某数的最大整数 -* Math.floor 得到不大于某数的最大整数 -* Math.IEEEremainder 求余 -* Math.max 求两数中最大 -* Math.min 求两数中最小 -* Math.sqrt 求开方 -* Math.pow 求某数的任意次方, 抛出ArithmeticException处理溢出异常 -* Math.exp 求e的任意次方 -* Math.log10 以10为底的对数 -* Math.log 自然对数 -* Math.rint 求距离某数最近的整数(可能比某数大,也可能比它小) -* Math.round 同上,返回int型或者long型(上一个函数返回double型) -* Math.random 返回0,1之间的一个随机数 diff --git a/Java基础教程/Java标准库/04 数学计算.md b/Java基础教程/Java标准库/04 数学计算.md new file mode 100644 index 00000000..ae21806a --- /dev/null +++ b/Java基础教程/Java标准库/04 数学计算.md @@ -0,0 +1,71 @@ +## 1 Math + +### Math类 +Java 的 Math 包含了用于执行基本数学运算的属性和方法,如初等指数、对数、平方根和三角函数。 + +### Math中的常量 +* Math.PI 记录的圆周率 +* Math.E 记录e的常量 + +### Math中的函数 +三角函数 +* Math.sin 正弦函数 Math.asin 反正弦函数 +* Math.cos 余弦函数 Math.acos 反余弦函数 +* Math.tan 正切函数 Math.atan 反正切函数 Math.atan2 商的反正切函数 +* Math.toDegrees 弧度转化为角度 Math.toRadians 角度转化为弧度 + +舍入函数 +* Math.abs 求绝对值 +* Math.ceil 得到不小于某数的最大整数 +* Math.floor 得到不大于某数的最大整数 +* Math.IEEEremainder 求余 +* Math.max 求两数中最大 +* Math.min 求两数中最小 +* Math.round 同上,返回int型或者long型(上一个函数返回double型) + +指数幂计算 +* Math.sqrt 求开方 +* Math.pow 求某数的任意次方, 抛出ArithmeticException处理溢出异常 +* Math.exp 求e的任意次方 +* Math.log10 以10为底的对数 +* Math.log 自然对数 +* Math.rint 求距离某数最近的整数(可能比某数大,也可能比它小) + +随机数 +* Math.random 返回0,1之间的一个随机数 + +## 2 BigDecimal类 + +### 概述 + +| 相关内容 | 具体描述 | +| -------- | :----------------------------------------------------------- | +| 包 | java.math 使用时需要导包 | +| 类声明 | public class BigDecimal extends Number implements Comparable | +| 描述 | BigDecimal类提供了算术,缩放操作,舍入,比较,散列和格式转换的操作。提供了更加精准的数据计算方式 | + +### 构造方法 + +| 构造方法名 | 描述 | +| ---------------------- | ----------------------------------------------- | +| BigDecimal(double val) | 将double类型的数据封装为BigDecimal对象 | +| BigDecimal(String val) | 将 BigDecimal 的字符串表示形式转换为 BigDecimal | + +注意:推荐使用第二种方式,第一种存在精度问题; + +### 常用方法 + +BigDecimal类中使用最多的还是提供的进行四则运算的方法,如下: + +| 方法声明 | 描述 | +| -------------------------------------------- | -------- | +| public BigDecimal add(BigDecimal value) | 加法运算 | +| public BigDecimal subtract(BigDecimal value) | 减法运算 | +| public BigDecimal multiply(BigDecimal value) | 乘法运算 | +| public BigDecimal divide(BigDecimal value) | 触发运算 | + +注意:对于divide方法来说,如果除不尽的话,就会出现java.lang.ArithmeticException异常。此时可以使用divide方法的另一个重载方法; + +> BigDecimal divide(BigDecimal divisor, int scale, int roundingMode): divisor:除数对应的BigDecimal对象;scale:精确的位数;roundingMode取舍模式 + +> 小结:Java中小数运算有可能会有精度问题,如果要解决这种精度问题,可以使用BigDecimal \ No newline at end of file diff --git a/Java基础教程/Java标准库/05 日期时间.md b/Java基础教程/Java标准库/05 日期时间.md index 32287666..72c41eda 100644 --- a/Java基础教程/Java标准库/05 日期时间.md +++ b/Java基础教程/Java标准库/05 日期时间.md @@ -47,7 +47,7 @@ Java使用以下三种方法来比较两个日期: ## 2 日期格式化SimpleDateFormat -### 格式化的方法 +### 格式化的方法format SimpleDateFormat 是一个以语言环境敏感的方式来格式化和分析日期的类。SimpleDateFormat 允许你选择任何用户自定义日期时间格式来运行。 ```java @@ -65,7 +65,27 @@ public class DateDemo { } ``` -### 格式化编码 +### 反格式化方法parse + +```java +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +/** + * 把String转换成Date对象 +*/ +public class Demo04DateFormatMethod { + public static void main(String[] args) throws ParseException { + DateFormat df = new SimpleDateFormat("yyyy年MM月dd日"); + String str = "2020年6月11日"; + Date date = df.parse(str); + System.out.println(date); + } +} +``` + +### SimpleDateFormat格式化编码 | 字母 | 描述 | 示例 | |---|---|---| @@ -90,7 +110,7 @@ public class DateDemo { | ' | 文字定界符 | Delimiter | | " | 单引号 | ` | -### printf格式化日期 +### Printf&String.format格式化日期 printf 方法可以很轻松地格式化时间和日期。使用两个字母格式,它以 %t 开头并且以下面表格中的一个字母结尾。 @@ -174,4 +194,144 @@ public class DiffDemo { ## 4 Calendar类 -> 日后补充把 \ No newline at end of file + +## 概念 + +`java.util.Calendar`是日历类,在Date后出现,替换掉了许多Date的方法。该类将所有可能用到的时间信息封装为静态成员变量,方便获取。日历类就是方便获取各个时间属性的。 + +## 获取方式 + +Calendar为抽象类,由于语言敏感性,Calendar类在创建对象时并非直接创建,而是通过静态方法创建,返回子类对象,如下: + +Calendar静态方法 + +* `public static Calendar getInstance()`:使用默认时区和语言环境获得一个日历 + +例如: + +```java +import java.util.Calendar; + +public class Demo06CalendarInit { + public static void main(String[] args) { + Calendar cal = Calendar.getInstance(); + } +} +``` + +## 常用方法 + +根据Calendar类的API文档,常用方法有: + +- `public int get(int field)`:返回给定日历字段的值。 +- `public void set(int field, int value)`:将给定的日历字段设置为给定值。 +- `public abstract void add(int field, int amount)`:根据日历的规则,为给定的日历字段添加或减去指定的时间量。 +- `public Date getTime()`:返回一个表示此Calendar时间值(从历元到现在的毫秒偏移量)的Date对象。 + +Calendar类中提供很多成员常量,代表给定的日历字段: + +| 字段值 | 含义 | +| ------------ | ------------------------------------- | +| YEAR | 年 | +| MONTH | 月(从0开始,可以+1使用) | +| DAY_OF_MONTH | 月中的天(几号) | +| HOUR | 时(12小时制) | +| HOUR_OF_DAY | 时(24小时制) | +| MINUTE | 分 | +| SECOND | 秒 | +| DAY_OF_WEEK | 周中的天(周几,周日为1,可以-1使用) | + +### get/set方法 + +get方法用来获取指定字段的值,set方法用来设置指定字段的值,代码使用演示: + +```java +import java.util.Calendar; + +public class CalendarUtil { + public static void main(String[] args) { + // 创建Calendar对象 + Calendar cal = Calendar.getInstance(); + // 获取年 + int year = cal.get(Calendar.YEAR); + // 获取月 + int month = cal.get(Calendar.MONTH) + 1; + // 获取日 + int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + System.out.print(year + "年" + month + "月" + dayOfMonth + "日"); + } +} +``` + +```java +import java.util.Calendar; + +public class Demo07CalendarMethod { + public static void main(String[] args) { + Calendar cal = Calendar.getInstance(); + // 设置年 + cal.set(Calendar.YEAR, 2020); + System.out.print(year + "年" + month + "月" + dayOfMonth + "日"); // 2020年6月11日 + } +} +``` + +### add方法 + +add方法可以对指定日历字段的值进行加减操作,如果第二个参数为正数则加上偏移量,如果为负数则减去偏移量。代码如: + +```java +import java.util.Calendar; + +public class CalendarMethod { + public static void main(String[] args) { + Calendar cal = Calendar.getInstance(); + // 获取年 + int year = cal.get(Calendar.YEAR); + // 获取月 + int month = cal.get(Calendar.MONTH) + 1; + // 获取日 + int dayOfMonth = cal.get(Calendar.DAY_OF_MONTH); + // 2020年6月11日 + System.out.println(year + "年" + month + "月" + dayOfMonth + "日"); + // 使用add方法 + // 加2天 + cal.add(Calendar.DAY_OF_MONTH, 2); + // 减3年 + cal.add(Calendar.YEAR, -3); + // 获取年 + int year1 = cal.get(Calendar.YEAR); + // 获取月 + int month1 = cal.get(Calendar.MONTH) + 1; + // 获取日 + int dayOfMonth1 = cal.get(Calendar.DAY_OF_MONTH); + // 2017年6月13日; + System.out.println(year1 + "年" + month1 + "月" + dayOfMonth1 + "日"); + } +} +``` + +### getTime方法 + +Calendar中的getTime方法并不是获取毫秒时刻,而是拿到对应的Date对象。 + +```java +import java.util.Calendar; +import java.util.Date; + +public class Demo09CalendarMethod { + public static void main(String[] args) { + Calendar cal = Calendar.getInstance(); + Date date = cal.getTime(); + System.out.println(date); // Thu Jun 11 09:37:57 CST 2020 + } +} +``` + +> 小贴士: +> +> 西方星期的开始为周日,中国为周一。 +> +> 在Calendar类中,月份的表示是以0-11代表1-12月。 +> +> 日期是有大小关系的,时间靠后,时间越大。 \ No newline at end of file diff --git a/Java基础教程/Java标准库/07 正则表达式.md b/Java基础教程/Java标准库/07 正则表达式.md index 994cebca..b6d93ed3 100644 --- a/Java基础教程/Java标准库/07 正则表达式.md +++ b/Java基础教程/Java标准库/07 正则表达式.md @@ -1,2 +1,304 @@ -## Java 正则表达式 +> https://blog.csdn.net/m0_62618110/article/details/123704869 + + + +# Java 正则表达式 + +## 0 概述 + +### 简介 +正则表达式(regex)是一个字符串,由字面值字符和特殊符号组成,是用来描述匹配一个字符串集合的模式,可以用来匹配、查找字符串。 + +正则表达式的两个主要作用: +* 查找:在字符串中查找符合固定模式的子串 +* 匹配:整个字符串是否符合某个格式 + +在匹配和查找的基础上,实现替换、分割等操作。 + +### 基本实例 + +```java +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexMatches +{ + public static void main( String[] args ){ + + // 按指定模式在字符串查找 + String line = "This order was placed for QT3000! OK?"; + String pattern = "(\\D*)(\\d+)(.*)"; + + // 创建 Pattern 对象 + Pattern r = Pattern.compile(pattern); + + // 现在创建 matcher 对象 + Matcher m = r.matcher(line); + if (m.find( )) { + System.out.println("Found value: " + m.group(0) ); + System.out.println("Found value: " + m.group(1) ); + System.out.println("Found value: " + m.group(2) ); + System.out.println("Found value: " + m.group(3) ); + } else { + System.out.println("NO MATCH"); + } + } +} +``` + + +## 1 正则表达式语法 + +* 在其他语言中,`\\`表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。 +* 在 Java 中,`\\` 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。 +* 不要在重复词符中使用空白。如B{3,6} ,不能写成 B{3, 6}。空格也是有含义的。 +* 可以使用括号来将模式分组。(ab){3}匹配ababab , 而ab{3} 匹配 abbb。 + +| 字符 | 匹配 | 示例 | +|---|---|---| +| . | 任意单个字符,除换行符外 | jav.匹配java | +| [ ] | [ ] 中的任意一个字符 | java匹配j[abc]va | +| - | [ ] 内表示字符范围 | java匹配[a-z]av[a-g] | +| ^ | 在[ ]内的开头,匹配除[ ]内的字符之外的任意一个字符 | java匹配j[^b-f]va | +| | | 或 | x|y匹配x或y | +| \ | 将下一字符标记为特殊字符、文本、反向引用或八进制转义符 | \(匹配( | +| $ | 匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。 | ;$匹配位于一行及外围的;号 | +| * | 零次或多次匹配前面的字符 | zo*匹配zoo或z | +| + | 一次或多次匹配前面的字符 | zo+匹配zo或zoo | +| ? | 零次或一次匹配前面的字符 | zo?匹配z或zo | +| p{n} | n 是非负整数。正好匹配 n 次 | o{2}匹配food中的两个o | +| p{n,} | n 是非负整数。至少匹配 n 次 | o{2}匹配foood中的所有o | +| p{n,m} | M 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次 | o{1,3}匹配fooood中的三个o | +| \p{P} | 一个标点字符 !"#$%&'()*+,-./:;<=>?@[\]^_'{|}~ | J\p{P}a匹配J?a | +| \b | 匹配一个字边界 | va\b匹配java中的va,但不匹配javar中的va | +| \B | 非字边界匹配 | va\B匹配javar中的va,但不匹配java中的va | +| \d | 数字字符匹配 | 1[\\d]匹配13 | +| \D | 非数字字符匹配 | [\\D]java匹配Jjava | +| \w | 单词字符 | java匹配[\\w]ava | +| \W | 非单词字符 | $java匹配[\\W]java | +| \s | 空白字符 | Java 2匹配Java\\s2 | +| \S | 非空白字符 | java匹配 j[\\S]va | +| \f | 匹配换页符 | 等效于\x0c和\cL | +| \n | 匹配换行符 | 等效于\x0a和\cJ | + + + + +### 分组说明 +``` +正则表达式-字符类 + +- 语法示例: + +1. \[abc\]:代表a或者b,或者c字符中的一个。 +2. \[^abc\]:代表除a,b,c以外的任何字符。 +3. [a-z]:代表a-z的所有小写字符中的一个。 +4. [A-Z]:代表A-Z的所有大写字符中的一个。 +5. [0-9]:代表0-9之间的某一个数字字符。 +6. [a-zA-Z0-9]:代表a-z或者A-Z或者0-9之间的任意一个字符。 +7. [a-dm-p]:a 到 d 或 m 到 p之间的任意一个字符。 + +正则表达式-逻辑运算符 + +- 语法示例: + 1. &&:并且 + 2. | :或者 + +正则表达式-预定义字符 + +- 语法示例: + 1. "." : 匹配任何字符。 + 2. "\d":任何数字[0-9]的简写; + 3. "\D":任何非数字\[^0-9\]的简写; + 4. "\s": 空白字符:[ \t\n\x0B\f\r] 的简写 + 5. "\S": 非空白字符:\[^\s\] 的简写 + 6. "\w":单词字符:[a-zA-Z_0-9]的简写 + 7. "\W":非单词字符:\[^\w\] + +正则表达式-数量词 + +- 语法示例: + 1. X? : 0次或1次 + 2. X* : 0次到多次 + 3. X+ : 1次或多次 + 4. X{n} : 恰好n次 + 5. X{n,} : 至少n次 + 6. X{n,m}: n到m次(n和m都是包含的) + + +正则表达式-分组括号() +``` + + +## 2 基本概念 +### Patter类和Matcher类 +Pattern 类: +pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。 + +Matcher 类: +Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。 + +### 捕获组 + + +1. 捕获组是把多个字符当成一个单独单元进行处理的方法,它通过对括号内的字符分组来创建。 +``` +捕获组通过从左到右计算其括号来编号。 + +例如:在表达式((A)(B(C))) 中,存在四个这样的组: + +((A)(B(C))) +(A) +(B(C)) +(C) +``` +2. 捕获组可以通过调用matcher对象的groupCount方法来查看表达式有多少个分组。(groupCount方法返回一个int值,来表示matcher对象当前有多少个捕获组) + +3. 还有一个特殊的组零(group(0)),它代表整个表达式。(该组不包括在groupCount的返回值中) + +4. 以 (?) 开头的组是纯的非捕获 组,它不捕获文本,也不针对组合计进行计数。 + + +## 3 Matcher用法 + + + +### 索引方法 + +1. public int start() +返回以前匹配的初始索引。 +2. public int start(int group) + 返回在以前的匹配操作期间,由给定组所捕获的子序列的初始索引 +3. public int end() +返回最后匹配字符之后的偏移量。 +4. public int end(int group) +返回在以前的匹配操作期间,由给定组所捕获子序列的最后字符之后的偏移量。 + +```java +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexMatches +{ + private static final String REGEX = "\\bcat\\b"; + private static final String INPUT = + "cat cat cat cattie cat"; + + public static void main( String[] args ){ + Pattern p = Pattern.compile(REGEX); + Matcher m = p.matcher(INPUT); // 获取 matcher 对象 + int count = 0; + + while(m.find()) { + count++; + System.out.println("Match number "+count); + System.out.println("start(): "+m.start()); + System.out.println("end(): "+m.end()); + } + } +} +``` + +### 匹配和查找方法 + +1. public boolean lookingAt() + 尝试将从区域开头开始的输入序列与该模式匹配。开头匹配。 +2. public boolean find() +尝试查找与该模式匹配的输入序列的下一个子序列。 +3. public boolean find(int start) +重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。 +4. public boolean matches() +尝试将整个区域与模式匹配。全局匹配。 + +```java +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexMatches +{ + private static final String REGEX = "foo"; + private static final String INPUT = "fooooooooooooooooo"; + private static final String INPUT2 = "ooooofoooooooooooo"; + private static Pattern pattern; + private static Matcher matcher; + private static Matcher matcher2; + + public static void main( String[] args ){ + pattern = Pattern.compile(REGEX); + matcher = pattern.matcher(INPUT); + matcher2 = pattern.matcher(INPUT2); + + System.out.println("Current REGEX is: "+REGEX); + System.out.println("Current INPUT is: "+INPUT); + System.out.println("Current INPUT2 is: "+INPUT2); + + + System.out.println("lookingAt(): "+matcher.lookingAt()); + System.out.println("matches(): "+matcher.matches()); + System.out.println("lookingAt(): "+matcher2.lookingAt()); + } +} +``` +### 替换方法 + +1. public Matcher appendReplacement(StringBuffer sb, String replacement) +实现非终端添加和替换步骤。 +2. public StringBuffer appendTail(StringBuffer sb) +实现终端添加和替换步骤。 +3. public String replaceAll(String replacement) + 替换模式与给定替换字符串相匹配的输入序列的每个子序列。 +4. public String replaceFirst(String replacement) + 替换模式与给定替换字符串匹配的输入序列的第一个子序列。 +5. public static String quoteReplacement(String s) +返回指定字符串的字面替换字符串。这个方法返回一个字符串,就像传递给Matcher类的appendReplacement 方法一个字面字符串一样工作。 + + +```java +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexMatches +{ + private static String REGEX = "dog"; + private static String INPUT = "The dog says meow. " + + "All dogs say meow."; + private static String REPLACE = "cat"; + + public static void main(String[] args) { + Pattern p = Pattern.compile(REGEX); + // get a matcher object + Matcher m = p.matcher(INPUT); + INPUT = m.replaceAll(REPLACE); + System.out.println(INPUT); + } +} +``` + +```java +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RegexMatches +{ + private static String REGEX = "a*b"; + private static String INPUT = "aabfooaabfooabfoobkkk"; + private static String REPLACE = "-"; + public static void main(String[] args) { + Pattern p = Pattern.compile(REGEX); + // 获取 matcher 对象 + Matcher m = p.matcher(INPUT); + StringBuffer sb = new StringBuffer(); + while(m.find()){ + m.appendReplacement(sb,REPLACE); + } + m.appendTail(sb); + System.out.println(sb.toString()); + } +} +``` + + +## 4 String自带的正则表达式功能 + +见String \ No newline at end of file diff --git a/Java基础教程/Java标准库/08 随机数Random.md b/Java基础教程/Java标准库/08 随机数Random.md new file mode 100644 index 00000000..7dec24a1 --- /dev/null +++ b/Java基础教程/Java标准库/08 随机数Random.md @@ -0,0 +1,47 @@ + +## 2 Random + +### 简介 + +在 Java中要生成一个指定范围之内的随机数字有两种方法:一种是调用 Math 类的 random() 方法,一种是使用 Random 类。 + +Random():该构造方法使用一个和当前系统时间对应的数字作为种子数,然后使用这个种子数构造 Random 对象。 +Random(long seed):使用单个 long 类型的参数创建一个新的随机数生成器。 + +Random 类提供的所有方法生成的随机数字都是均匀分布的,也就是说区间内部的数字生成的概率是均等的 + +### 实例 + +```java +package cn.itcast.demo1; + +import java.util.Random;//使用时需要先导包 +import java.util.Scanner; + + +public class RAndom { + public static void main(String[] args) { + Random r = new Random();//以系统自身时间为种子数 + int i = r.nextInt(); + System.out.println("i"+i); + Scanner sc =new Scanner(System.in); + int j = sc.nextInt(); + Random r2 = new Random(j);//自定义种子数 + Random r3 = new Random(j);//这里是为了验证上方的注意事项:Random类是伪随机,相同种子数相同次数产生的随机数相同 + int num = r2.nextInt(); + int num2 = r3.nextInt(); + System.out.println("num"+num); + System.out.println("num2"+num2); + } +} +``` + +### 常用方法 + +| random.nextInt() | 返回值为整数,范围是int类型范围 | +|---|---| +| random.nextLong() | 返回值为长整型,范围是long类型的范围 | +| random.nextFloat() | 返回值为小数,范围是[0,0.1] | +| random.nextDouble() | 返回值为小数,范围是[0,0.1] | +| random.nextBoolean() | 返回值为boolean值,true和false概率相同 | +| radom.nextGaussian() | 返回值为呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0 | diff --git a/Java基础教程/Java标准库/09 System类.md b/Java基础教程/Java标准库/09 System类.md new file mode 100644 index 00000000..2bbcb2e4 --- /dev/null +++ b/Java基础教程/Java标准库/09 System类.md @@ -0,0 +1,59 @@ +## 1 System + + +### 比较有用的方法 +```java +static void setIn(InputStream in) // 标准输入的重定向 +static void setOut(PrintStream out) // 标准输出的重定向 +static void setErr(PrintStream err) // 标准错误的重定向 +/******************************/ +static Map getenv() // 返回所有的环境变量的键值对 +static String getenv(String name) // 返回特定环境变量的值 +/******************************/ +static Properties getProperties() // 返回所有的系统属性 +static String getProperty(String key) // 返回特定的系统属性的值 +/******************************/ +static void setProperties(Properties props) // 设置所有的系统属性 +static String setProperty(String key, String value) // 设置特定的系统属性的值 +/******************************/ +static long currentTimeMillis() // 获取当前系统时间,用毫秒表示,从 1970 年开始。HotSpot VM 中可用 +``` + +### arraycopy(…)方法 + +arraycopy(…)方法将指定原数组中的数据从指定位置复制到目标数组的指定位置。 + +```java +static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) +``` + +```java +package com.ibelifly.commonclass.system; + +public class Test1 { + public static void main(String[] args) { + int[] arr={23,45,20,67,57,34,98,95}; + int[] dest=new int[8]; + System.arraycopy(arr,4,dest,4,4); + for (int x:dest) { + System.out.print(x+" "); + } + } +} +``` + +### exit(int status)方法 + +exit(int status)方法用于终止当前运行的Java虚拟机。如果参数是0,表示正常退出JVM;如果参数非0,表示异常退出JVM。 + +```java +package com.ibelifly.commonclass.system; + +public class Test4 { + public static void main(String[] args) { + System.out.println("程序开始了"); + System.exit(0); //因为此处已经终止当前运行的Java虚拟机,故不会执行之后的代码 + System.out.println("程序结束了"); + } +} +``` \ No newline at end of file diff --git a/Java基础教程/Java标准库/Arrays类.md b/Java基础教程/Java标准库/Arrays类.md deleted file mode 100644 index 5b1b4bbc..00000000 --- a/Java基础教程/Java标准库/Arrays类.md +++ /dev/null @@ -1,20 +0,0 @@ -## Arrays类中的方法 - -### 方法概述 - -* 给数组赋值:通过 fill 方法。 -* 对数组排序:通过 sort 方法,按升序。 -* 比较数组:通过 equals 方法比较数组中元素值是否相等。 -* 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。 - - -### 具体方法 - -* public static int binarySearch(Object[] a, Object key) -用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。 -* public static boolean equals(long[] a, long[] a2) -如果两个指定的 long 型数组彼此相等,则返回 true。如果两个数组包含相同数量的元素,并且两个数组中的所有相应元素对都是相等的,则认为这两个数组是相等的。换句话说,如果两个数组以相同顺序包含相同的元素,则两个数组是相等的。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 -* public static void fill(int[] a, int val) -将指定的 int 值分配给指定 int 型数组指定范围中的每个元素。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 -* public static void sort(Object[] a) -对指定对象数组根据其元素的自然顺序进行升序排列。同样的方法适用于所有的其他基本数据类型(Byte,short,Int等)。 \ No newline at end of file diff --git a/Java基础教程/Java标准库/StringBuffer.md b/Java基础教程/Java标准库/StringBuffer.md deleted file mode 100644 index 3b3f69c7..00000000 --- a/Java基础教程/Java标准库/StringBuffer.md +++ /dev/null @@ -1,140 +0,0 @@ -String与StringBuffer的区别 - -简单地说,就是一个变量和常量的关系。StringBuffer对象的内容可以修改;而String对象一旦产生后就不可以被修改,重新赋值其实是两个对象。 - -StringBuffer的内部实现方式和String不同,StringBuffer在进行字符串处理时,不生成新的对象,在内存使用上要优于String类。所以在实际使用时,如果经常需要对一个字符串进行修改,例如插入、删除等操作,使用StringBuffer要更加适合一些。 - -String:在String类中没有用来改变已有字符串中的某个字符的方法,由于不能改变一个java字符串中的某个单独字符,所以在JDK文档中称String类的对象是不可改变的。然而,不可改变的字符串具有一个很大的优点:编译器可以把字符串设为共享的。 - -StringBuffer:StringBuffer类属于一种辅助类,可预先分配指定长度的内存块建立一个字符串缓冲区。这样使用StringBuffer类的append方法追加字符 -比 String使用 + 操作符添加字符 到 一个已经存在的字符串后面有效率得多。因为使用 + -操作符每一次将字符添加到一个字符串中去时,字符串对象都需要寻找一个新的内存空间来容纳更大的字符串,这无凝是一个非常消耗时间的操作。添加多个字符也就意味着要一次又一次的对字符串重新分配内存。使用StringBuffer类就避免了这个问题。 - -StringBuffer是线程安全的,在多线程程序中也可以很方便的进行使用,但是程序的执行效率相对来说就要稍微慢一些。 - -StringBuffer的常用方法 - -StringBuffer类中的方法要偏重于对字符串的变化例如追加、插入和删除等,这个也是StringBuffer和String类的主要区别。 - -1、append方法 - -public StringBuffer append(boolean b) - -该方法的作用是追加内容到当前StringBuffer对象的末尾,类似于字符串的连接。调用该方法以后,StringBuffer对象的内容也发生改变,例如: - -StringBuffer sb = new StringBuffer(“abc”); - -sb.append(true); - -则对象sb的值将变成”abctrue”。 - -使用该方法进行字符串的连接,将比String更加节约内容,例如应用于数据库SQL语句的连接,例如: - -StringBuffer sb = new StringBuffer(); - -String user = “test”; - -String pwd = “123”; - -sb.append(“select \* from userInfo where username=“) - -.append(user) - -.append(“ and pwd=”) - -.append(pwd); - -这样对象sb的值就是字符串“select \* from userInfo where username=test and -pwd=123”。 - -2、deleteCharAt方法 - -public StringBuffer deleteCharAt(int index) - -该方法的作用是删除指定位置的字符,然后将剩余的内容形成新的字符串。例如: - -StringBuffer sb = new StringBuffer(“Test”); - -sb. deleteCharAt(1); - -该代码的作用删除字符串对象sb中索引值为1的字符,也就是删除第二个字符,剩余的内容组成一个新的字符串。所以对象sb的值变为”Tst”。 - -还存在一个功能类似的delete方法: - -public StringBuffer delete(int start,int end) - -该方法的作用是删除指定区间以内的所有字符,包含start,不包含end索引值的区间。例如: - -StringBuffer sb = new StringBuffer(“TestString”); - -sb. delete (1,4); - -该代码的作用是删除索引值1(包括)到索引值4(不包括)之间的所有字符,剩余的字符形成新的字符串。则对象sb的值是”TString”。 - -3、insert方法 - -public StringBuffer insert(int offset, String s) - -该方法的作用是在StringBuffer对象中插入内容,然后形成新的字符串。例如: - -StringBuffer sb = new StringBuffer(“TestString”); - -sb.insert(4,“false”); - -该示例代码的作用是在对象sb的索引值4的位置插入字符串false,形成新的字符串,则执行以后对象sb的值是”TestfalseString”。 - -4、reverse方法 - -public StringBuffer reverse() - -该方法的作用是将StringBuffer对象中的内容反转,然后形成新的字符串。例如: - -StringBuffer sb = new StringBuffer(“abc”); - -sb.reverse(); - -经过反转以后,对象sb中的内容将变为”cba”。 - -5、setCharAt方法 - -public void setCharAt(int index, char ch) - -该方法的作用是修改对象中索引值为index位置的字符为新的字符ch。例如: - -StringBuffer sb = new StringBuffer(“abc”); - -sb.setCharAt(1,’D’); - -则对象sb的值将变成”aDc”。 - -6、trimToSize方法 - -public void trimToSize() - -该方法的作用是将StringBuffer对象的中存储空间缩小到和字符串长度一样的长度,减少空间的浪费。 - -7、构造方法: - -StringBuffer s0=new StringBuffer();分配了长16字节的字符缓冲区 - -StringBuffer s1=new StringBuffer(512);分配了512字节的字符缓冲区 - -8、获取字符串的长度: length() - -StringBuffer s = new StringBuffer("www"); - -int i=s.length(); - -m.返回字符串的一部分值 - -substring(int start) //返回从start下标开始以后的字符串 - -substring(int start,int end) //返回从start到 end-1字符串 - -9.替换字符串 - -replace(int start,int end,String str) - -s.replace(0,1,"qqq"); - -10.转换为不变字符串:toString()。 diff --git a/Java基础教程/Java标准库/image/2022-07-12-11-07-56.png b/Java基础教程/Java标准库/image/2022-07-12-11-07-56.png new file mode 100644 index 00000000..5eb881b7 Binary files /dev/null and b/Java基础教程/Java标准库/image/2022-07-12-11-07-56.png differ diff --git a/Java基础教程/Java源代码/codedemo/aspectj/BusinessService.java b/Java基础教程/Java源代码/codedemo/aspectj/BusinessService.java new file mode 100644 index 00000000..36f465c4 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/aspectj/BusinessService.java @@ -0,0 +1,142 @@ +package cn.aofeng.demo.aspectj; + +/** + * 模拟业务方法,将被Aspectj织入代码,增加功能。 + * + * @author 聂勇 + */ +public class BusinessService { + + public long add(int a, int b) { + return a+b; + } + + public long add(int a, int b, int... other) { + long result = a + b; + for (int i : other) { + result += i; + } + + return result; + } + + public String join(String first, String... appends) { + if (null == first) { + throw new IllegalArgumentException("first is null"); + } + StringBuilder buffer = new StringBuilder(); + buffer.append(first); + for (String str : appends) { + buffer.append(str); + } + + return buffer.toString(); + } + + public String addPrefix(String src) { + if (null == src) { + throw new IllegalArgumentException("src is null"); + } + + return "-->"+src; + } + + public static void printLine(char style) { + if ('=' == style) { + System.out.println("========================================================================================"); + } else if ('-' == style) { + System.out.println("----------------------------------------------------------------------------------------"); + } else { + System.out.println(" "); + } + } + + public static void main(String[] args) { + final BusinessService bs = new BusinessService(); + + System.out.println("1、执行方法add(int, int)"); + RunMethod rm = new RunMethod() { + + @Override + public void run() { + long result = bs.add(1, 2); + System.out.println(">>> 结果:" + result); + } + }; + rm.execute(); + + System.out.println("2、执行方法add(int, int, int...)"); + rm = new RunMethod() { + + @Override + public void run() { + long result = bs.add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + System.out.println(">>> 结果:" + result); + } + }; + rm.execute(); + + System.out.println("3、执行方法join(String, String...)"); + rm = new RunMethod() { + + @Override + public void run() { + String str = bs.join("first", "-second", "-third"); + System.out.println(">>> 结果:" + str); + } + }; + rm.execute(); + + System.out.println("4、执行方法join(String, String...)"); + rm = new RunMethod() { + + @Override + public void run() { + String str = bs.join(null, "-second", "-third"); + System.out.println(">>> 结果:" + str); + } + }; + rm.execute(); + + System.out.println("5、执行方法addPrefix(String)"); + rm = new RunMethod() { + + @Override + public void run() { + String str = bs.addPrefix("原字符串"); + System.out.println(">>> 结果:" + str); + } + }; + rm.execute(); + + System.out.println("6、执行方法addPrefix(String)"); + rm = new RunMethod() { + + @Override + public void run() { + String str = bs.addPrefix(null); + System.out.println(">>> 结果:" + str); + } + }; + rm.execute(); + } + + public static abstract class RunMethod { + + private char _style = '='; + + public void execute() { + printLine(_style); + try { + run(); + } catch (Exception e) { + e.printStackTrace(System.err); + } + printLine(_style); + printLine(' '); + } + + public abstract void run(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/aspectj/BusinessServiceInterceptor.java b/Java基础教程/Java源代码/codedemo/aspectj/BusinessServiceInterceptor.java new file mode 100644 index 00000000..847a2a41 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/aspectj/BusinessServiceInterceptor.java @@ -0,0 +1,122 @@ +package cn.aofeng.demo.aspectj; + +import org.apache.commons.lang.ArrayUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 拦截器。 + * + * @author 聂勇 + */ +@Aspect +public class BusinessServiceInterceptor { + + private static Logger _logger = LoggerFactory.getLogger(BusinessServiceInterceptor.class); + + private char _style = '-'; + + /** + *
+     * Before通知不能修改方法传入的参数。
+     * 
+ */ + @Before("execution(public * cn.aofeng.demo.aspectj.BusinessService.add(..))") + public void beforeAdd(JoinPoint joinPoint) { + _logger.info( String.format("拦截到方法:%s, 传入参数:%s", + joinPoint.getSignature().getName(), + ArrayUtils.toString(joinPoint.getArgs()) ) ); + BusinessService.printLine(_style); + } + + /** + *
+     * After通知不能修改方法的返回值。
+     * 如果被拦截的方法抛出异常,拦截代码仍然正常执行。
+     * 
+ */ + @After("execution(public * cn.aofeng.demo.aspectj.BusinessService.join(..))") + public void beforeAddSupportMultiArgs(JoinPoint joinPoint) { + _logger.info("Signature.name:"+joinPoint.getSignature().getName()); + _logger.info("Args:" + ArrayUtils.toString(joinPoint.getArgs())); + _logger.info("Target:" + ArrayUtils.toString(joinPoint.getTarget())); + _logger.info("This:" + ArrayUtils.toString(joinPoint.getThis())); + _logger.info("Kind:" + ArrayUtils.toString(joinPoint.getKind())); + _logger.info("SourceLocation:" + ArrayUtils.toString(joinPoint.getSourceLocation())); + + // 试图修改传入的参数 + joinPoint.getArgs()[0] = "100"; + + BusinessService.printLine(_style); + } + + /** + *
+     * AfterReturning通知不能修改方法的返回值。
+     * 如果被拦截的方法抛出异常,拦截代码不再执行。
+     * 
+ */ + @AfterReturning(pointcut="execution(public * cn.aofeng.demo.aspectj.BusinessService.join(..))", returning="result") + public void afterReturnAdd(JoinPoint joinPoint, Object result) { + _logger.info( String.format("拦截到方法:%s, 传入参数:%s, 执行结果:%s", + joinPoint.getSignature().getName(), + ArrayUtils.toString(joinPoint.getArgs()), + result) ); + + // 试图修改返回值 + result = "hello, changed"; + + BusinessService.printLine(_style); + } + + /** + *
+     * 只在被拦截的方法抛出异常时才执行。
+     * 
+ */ + @AfterThrowing(pointcut="execution(public * cn.aofeng.demo.aspectj.BusinessService.join(..))", throwing="ex") + public void afterThrowingAdd(JoinPoint joinPoint, Exception ex) { + _logger.info( String.format("拦截到方法:%s, 传入参数:%s", + joinPoint.getSignature().getName(), + ArrayUtils.toString(joinPoint.getArgs()) ) ); + if (null != ex) { + _logger.info("拦截到异常:", ex); + } + + BusinessService.printLine(_style); + } + + /** + *
+     * {@link ProceedingJoinPoint}只能在Around通知中使用。
+     * Around通知可以修改被拦截方法的传入参数和返回值。
+     * 
+ */ + @Around("execution(public * cn.aofeng.demo.aspectj.BusinessService.addPrefix(..))") + public Object afterAround(ProceedingJoinPoint joinPoint) throws Throwable { + _logger.info( String.format("拦截到方法:%s, 传入参数:%s", + joinPoint.getSignature().getName(), + ArrayUtils.toString(joinPoint.getArgs()) ) ); + + Object result = null; + try { + result = joinPoint.proceed(); + _logger.info("执行结果:" + result); + } catch (Throwable e) { + throw e; + } finally { + BusinessService.printLine(_style); + } + + return result; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/aspectj/build.xml b/Java基础教程/Java源代码/codedemo/aspectj/build.xml new file mode 100644 index 00000000..b5375f9c --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/aspectj/build.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/dbutils/DbUtilsDemo.java b/Java基础教程/Java源代码/codedemo/dbutils/DbUtilsDemo.java new file mode 100644 index 00000000..08f60967 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/dbutils/DbUtilsDemo.java @@ -0,0 +1,205 @@ +package cn.aofeng.demo.dbutils; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.apache.commons.dbutils.QueryRunner; +import org.apache.commons.dbutils.ResultSetHandler; +import org.apache.commons.dbutils.handlers.BeanHandler; +import org.apache.commons.dbutils.handlers.BeanListHandler; +import org.apache.commons.dbutils.handlers.MapHandler; +import org.apache.commons.dbutils.handlers.MapListHandler; +import org.apache.log4j.Logger; + +import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; + +/** + * Apache DbUtils使用示例。 + * + * @author 聂勇 + */ +public class DbUtilsDemo { + + private static Logger _logger = Logger.getLogger(DbUtilsDemo.class); + + /** + * 创建JDBC连接池。 + * + * @param pros 数据库连接信息,里面包含4个键值对。 + *
+     * jdbcDriver => JDBC驱动类名称
+     * url => 数据库JDBC连接地址
+     * user => 连接数据库的用户名
+     * password => 连接数据库的密码
+     * 
+     * @return 连接池对象
+     */
+    private DataSource createDataSource(Properties pros) {
+        MysqlDataSource ds = new MysqlDataSource();
+        ds.setURL(pros.getProperty("url"));
+        ds.setUser(pros.getProperty("user"));
+        ds.setPassword(pros.getProperty("password"));
+        
+        return ds;
+    }
+    
+    /**
+     * 将查询结果集转换成Bean列表返回。
+     * 
+     * @param ds JDBC连接池
+     */
+    public void queryBeanList(DataSource ds) {
+        String sql = "select userId, userName, gender, age from student";
+        
+        QueryRunner run = new QueryRunner(ds);
+        ResultSetHandler> handler = new BeanListHandler(Student.class);
+        List result = null;
+        try {
+            result = run.query(sql, handler);
+        } catch (SQLException e) {
+            _logger.error("获取JDBC连接出错或执行SQL出错", e);
+        }
+        
+        if (null == result) {
+            return;
+        }
+        for (Student student : result) {
+            System.out.println(student);
+        }
+    }
+    
+    /**
+     * 将查询结果转换成Bean返回。
+     * 
+     * @param ds JDBC连接池
+     */
+    public void queryBean(DataSource ds, int userId) {
+        String sql = "select userId, userName, gender, age from student where userId=?";
+        
+        QueryRunner run = new QueryRunner(ds);
+        ResultSetHandler handler = new BeanHandler(Student.class);
+        Student result = null;
+        try {
+            result = run.query(sql, handler, userId);
+        } catch (SQLException e) {
+            _logger.error("获取JDBC连接出错或执行SQL出错", e);
+        }
+        
+        System.out.println(result);
+    }
+    
+    /**
+     * 将查询结果集转换成键值对列表返回。
+     * 
+     * @param ds JDBC连接池
+     */
+    public void queryMapList(DataSource ds) {
+        String sql = "select userId, userName, gender, age from student";
+        
+        QueryRunner run = new QueryRunner(ds);
+        ResultSetHandler>> handler = new MapListHandler();
+        List> result = null;
+        try {
+            result = run.query(sql, handler);
+        } catch (SQLException e) {
+            _logger.error("获取JDBC连接出错或执行SQL出错", e);
+        }
+        
+        if (null == result) {
+            return;
+        }
+        for (Map map : result) {
+            System.out.println(map);
+        }
+    }
+    
+    /**
+     * 将查询结果集转换成键值对列表返回。
+     * 
+     * @param ds JDBC连接池
+     * @param userId 用户编号
+     */
+    public void queryMap(DataSource ds, int userId) {
+        String sql = "select userId, userName, gender, age from student where userId=?";
+        
+        QueryRunner run = new QueryRunner(ds);
+        ResultSetHandler> handler = new MapHandler();
+        Map result = null;
+        try {
+            result = run.query(sql, handler, userId);
+        } catch (SQLException e) {
+            _logger.error("获取JDBC连接出错或执行SQL出错", e);
+        }
+        
+        System.out.println(result);
+    }
+    
+    /**
+     * 将查询结果集转换成键值对列表返回。
+     * 
+     * @param ds JDBC连接池
+     */
+    public void queryCustomHandler(DataSource ds) {
+        String sql = "select userId, userName, gender, age from student";
+        
+        // 新实现一个ResultSetHandler
+        ResultSetHandler> handler = new ResultSetHandler>() {
+            @Override
+            public List handle(ResultSet resultset)
+                    throws SQLException {
+                List result = new ArrayList();
+                while (resultset.next()) {
+                    Student student = new Student();
+                    student.setUserId(resultset.getInt("userId"));
+                    student.setUserName(resultset.getString("userName"));
+                    student.setGender(resultset.getString("gender"));
+                    student.setAge(resultset.getInt("age"));
+                    result.add(student);
+                }
+                
+                return result;
+            }
+            
+        };
+        
+        QueryRunner run = new QueryRunner(ds);
+        List result = null;
+        try {
+            result = run.query(sql, handler);
+        } catch (SQLException e) {
+            _logger.error("获取JDBC连接出错或执行SQL出错", e);
+        }
+        
+        if (null == result) {
+            return;
+        }
+        for (Student student : result) {
+            System.out.println(student);
+        }
+    }
+    
+    public static void main(String[] args) {
+        Properties pros = new Properties();
+        pros.put("jdbcDriver", "com.mysql.jdbc.Driver");
+        pros.put("url", "jdbc:mysql://192.168.56.102:19816/test?useUnicode=true&characterEncoding=UTF8");
+        pros.put("user", "uzone");
+        pros.put("password", "uzone");
+        
+        DbUtilsDemo demo = new DbUtilsDemo();
+        DataSource ds = demo.createDataSource(pros);
+        demo.queryBeanList(ds);
+        demo.queryBean(ds, 1);
+        demo.queryBean(ds, 9);
+        demo.queryMapList(ds);
+        demo.queryMap(ds, 3);
+        demo.queryMap(ds, 9);
+        demo.queryCustomHandler(ds);
+    }
+
+}
diff --git a/Java基础教程/Java源代码/codedemo/dbutils/README.md b/Java基础教程/Java源代码/codedemo/dbutils/README.md
new file mode 100644
index 00000000..ee80be8e
--- /dev/null
+++ b/Java基础教程/Java源代码/codedemo/dbutils/README.md
@@ -0,0 +1,226 @@
+Apache DbUtils 使用教程
+===
+用JDBC编程时,需要关注和处理的内容非常多,而且很容易造成连接资源没有释放导致泄漏的问题。一个普通的查询操作其处理过程如下:
+
+1. 创建Connection。
+1. 创建Statement。
+1. 执行SQL生成ResultSet,历遍ResultSet中的所有行记录提取列数据并转换成所需的对象。
+1. 关闭ResultSet。
+1. 关闭Statement。
+1. 关闭Connection。
+
+`Apache DbUtils`对JDBC操作进行了轻量地封装,解决了两个问题:
+
+1. 自动创建和释放连接资源,不再有泄漏问题。
+2. 自动将Result转换成对象。填入不同的`ResultSetHandler`,可将ResultSet转换成不同的对象。
+
+使得代码更加简洁和健壮,让你将精力更多地投入到业务处理中。
+
+预备
+---
+* MySQL数据库
+* commons-dbutils-1.5.jar
+* mysql-connector-java-5.1.22-bin.jar
+
+创建表和初始化数据
+---
+1、表结构。
+```sql
+CREATE TABLE `student` (
+  `userId` int(11) NOT NULL,
+  `userName` varchar(30) NOT NULL,
+  `gender` char(1) NOT NULL,
+  `age` int(11) DEFAULT NULL,
+  PRIMARY KEY (`userId`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
+```
+
+2、初始化数据。
+
+| userId | userName | gender | age |
+| :--- | :--- | :--- | :--- |
+| 1 |  张三 | M | 20 |
+| 2 | 李四 | M | 21 |
+| 3 | 王五 | M | 22 |
+| 4 | 小明 | M | 6 |
+| 5 | 小丽 | F | 9 |
+
+编写查询代码
+---
+###1、将查询结果转换成POJO对象列表。
+
+1)定义一个与表student相对应的对象Student([源代码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/dbutils/Student.java))。
+
+2)使用`BeanListHandler`将查询结果转换成POJO对象列表。
+```java
+public void queryBeanList(DataSource ds) {
+    String sql = "select userId, userName, gender, age from student";
+    
+    QueryRunner run = new QueryRunner(ds);
+    ResultSetHandler> handler = new BeanListHandler(Student.class);
+    List result = null;
+    try {
+        result = run.query(sql, handler);
+    } catch (SQLException e) {
+        _logger.error("获取JDBC连接出错或执行SQL出错", e);
+    }
+     
+    if (null == result) {
+        return;
+    }
+    for (Student student : result) {
+        System.out.println(student);
+    }
+}
+```
+执行queryBeanList方法,控制台输出如下信息:
+```
+Student [userId=1, userName=张三, gender=M, age=20]
+Student [userId=2, userName=李四, gender=M, age=21]
+Student [userId=3, userName=王五, gender=M, age=22]
+Student [userId=4, userName=小明, gender=M, age=6]
+Student [userId=5, userName=小丽, gender=F, age=9]
+```
+
+###2、将查询结果转换成一个POJO象。
+
+1)定义一个与表student相对应的对象Student([源代码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/dbutils/Student.java))。
+
+2)使用`BeanHandler`将查询结果转换成一个POJO对象。
+```java
+public void queryBean(DataSource ds, int userId) {
+    String sql = "select userId, userName, gender, age from student where userId=?";
+    
+    QueryRunner run = new QueryRunner(ds);
+    ResultSetHandler handler = new BeanHandler(Student.class);
+    Student result = null;
+    try {
+        result = run.query(sql, handler, userId);
+    } catch (SQLException e) {
+        _logger.error("获取JDBC连接出错或执行SQL出错", e);
+    }
+       
+    System.out.println(result);
+}
+```
+执行queryBean方法,控制台输出如下信息:
+```
+Student [userId=1, userName=张三, gender=M, age=20]
+```
+
+###3、将查询结果转换成Map对象列表。
+
+1)使用`MapListHandler`将ResultSet转换成Map对象列表。
+```java
+public void queryMapList(DataSource ds) {
+    String sql = "select userId, userName, gender, age from student";
+    
+    QueryRunner run = new QueryRunner(ds);
+    ResultSetHandler>> handler = new MapListHandler();
+    List> result = null;
+    try {
+        result = run.query(sql, handler);
+    } catch (SQLException e) {
+        _logger.error("获取JDBC连接出错或执行SQL出错", e);
+    }
+    
+    if (null == result) {
+        return;
+    }
+    for (Map map : result) {
+        System.out.println(map);
+    }
+}
+```
+执行queryMapList方法,控制台输出如下信息:
+```
+{age=20, userId=1, gender=M, userName=张三}
+{age=21, userId=2, gender=M, userName=李四}
+{age=22, userId=3, gender=M, userName=王五}
+{age=6, userId=4, gender=M, userName=小明}
+{age=9, userId=5, gender=F, userName=小丽}
+```
+
+###4、将查询结果转换成一个Map对象。
+
+1)使用`MapHandler`将ResultSet转换成一个Map对象。
+```java
+public void queryMap(DataSource ds, int userId) {
+    String sql = "select userId, userName, gender, age from student where userId=?";
+    
+    QueryRunner run = new QueryRunner(ds);
+    ResultSetHandler> handler = new MapHandler();
+    Map result = null;
+    try {
+        result = run.query(sql, handler, userId);
+    } catch (SQLException e) {
+        _logger.error("获取JDBC连接出错或执行SQL出错", e);
+    }
+    
+    System.out.println(result);
+}
+```
+执行queryMap方法,控制台输出如下信息:
+```
+{age=22, userId=3, gender=M, userName=王五}
+```
+
+###5、自定义ResultSetHandler。
+
+`Apache DbUtils`自带的各种`ResultSetHandler`已经可以满足绝大部分的查询需求,如果有一些特殊的要求满足不了,可以自己实现一个。
+```java
+public void queryCustomHandler(DataSource ds) {
+    String sql = "select userId, userName, gender, age from student";
+    
+    // 新实现一个ResultSetHandler
+    ResultSetHandler> handler = new ResultSetHandler>() {
+        @Override
+        public List handle(ResultSet resultset)
+                throws SQLException {
+            List result = new ArrayList();
+            while (resultset.next()) {
+                Student student = new Student();
+                student.setUserId(resultset.getInt("userId"));
+                student.setUserName(resultset.getString("userName"));
+                student.setGender(resultset.getString("gender"));
+                student.setAge(resultset.getInt("age"));
+                result.add(student);
+            }
+            
+            return result;
+        }
+        
+    };
+    
+    QueryRunner run = new QueryRunner(ds);
+    List result = null;
+    try {
+        result = run.query(sql, handler);
+    } catch (SQLException e) {
+        _logger.error("获取JDBC连接出错或执行SQL出错", e);
+    }
+      
+    if (null == result) {
+        return;
+    }
+    for (Student student : result) {
+        System.out.println(student);
+    }
+}
+```
+执行queryCustomHandler方法,控制台输出如下信息:
+```
+Student [userId=1, userName=张三, gender=M, age=20]
+Student [userId=2, userName=李四, gender=M, age=21]
+Student [userId=3, userName=王五, gender=M, age=22]
+Student [userId=4, userName=小明, gender=M, age=6]
+Student [userId=5, userName=小丽, gender=F, age=9]
+```
+
+附录
+---
+* [DEMO完整源代码](https://github.com/aofeng/JavaDemo/edit/master/src/cn/aofeng/demo/dbutils)
+
+参考资料
+---
+* [commons-dbutils example](http://commons.apache.org/proper/commons-dbutils/examples.html)
diff --git a/Java基础教程/Java源代码/codedemo/dbutils/Student.java b/Java基础教程/Java源代码/codedemo/dbutils/Student.java
new file mode 100644
index 00000000..7e910bd3
--- /dev/null
+++ b/Java基础教程/Java源代码/codedemo/dbutils/Student.java
@@ -0,0 +1,86 @@
+package cn.aofeng.demo.dbutils;
+
+/**
+ * 表student的POJO对象。
+ * 
+ * @author 聂勇
+ */
+public class Student {
+
+    private Integer userId;
+    
+    private String userName;
+    
+    private String gender;
+    
+    private Integer age;
+
+    public Integer getUserId() {
+        return userId;
+    }
+
+    public void setUserId(Integer userId) {
+        this.userId = userId;
+    }
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public String getGender() {
+        return gender;
+    }
+
+    public void setGender(String gender) {
+        this.gender = gender;
+    }
+
+    public Integer getAge() {
+        return age;
+    }
+
+    public void setAge(Integer age) {
+        this.age = age;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((age == null) ? 0 : age.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof Student)) {
+            return false;
+        }
+        Student other = (Student) obj;
+        if (age == null) {
+            if (other.age != null) {
+                return false;
+            }
+        } else if (!age.equals(other.age)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "Student [userId=" + userId + ", userName=" + userName
+                + ", gender=" + gender + ", age=" + age + "]";
+    }
+
+}
diff --git a/Java基础教程/Java源代码/codedemo/easymock/README.md b/Java基础教程/Java源代码/codedemo/easymock/README.md
new file mode 100644
index 00000000..f23550c2
--- /dev/null
+++ b/Java基础教程/Java源代码/codedemo/easymock/README.md
@@ -0,0 +1,258 @@
+用EasyMock生成模拟对象进行单元测试 | Testing with EasyMock
+===
+当项目达到一定规模时会根据业务或功能进行模块化,这时研发团队在协作时会碰到一个问题:如何让相互依赖的各模块并行开发?常用的办法是:各模块之间有清晰的边界,模块之间的交互通过接口。在设计阶段定义模块的接口,然后各个模块开发时遵循接口的定义进行实现和调用。
+
+OK,并行开发的问题解决了。但不同的模块因其复杂度和工作量不同,进度不一致。当研发同学完成所负责的模块时,但依赖的模块还没有完成开发,不好进行测试。
+这里就讲如何用EasyMock生成Mock Object模拟所依赖模块的接口来完成单元测试。
+
+![EasyMock](http://img1.ph.126.net/wErY4T7Ne-KeyB66As_PLA==/6608670713840748025.gif)
+
+预备
+---
+* easymock-3.2.jar
+* easymockclassextension-3.2.jar
+* cglib-2.2.2.jar
+* objenesis-1.2.jar
+* asm-3.1.jar
+* asm-commons-3.1.jar
+* asm-util-3.1.jar
+* gson-2.2.4.jar   // *用于处理JSON*
+
+注:上面的jar文件均可从[MAVEN仓库](http://mvnrepository.com)下载。
+
+
+使用EasyMock的五部曲
+---
+1、引入EasyMock。
+```java
+import static org.easymock.EasyMock.*;
+```
+2、创建Mock Object。
+```java
+mock = createMock(InterfaceOrClass.class);
+```
+3、设置Mock Object的行为和预期结果。
+```java
+mock.doSomeThing();
+expectLastCall().times(1);   // 至少要调用一次doSomeThing方法
+```
+或者
+```java
+// 模拟抛出异常
+expect(mock.getSomeThing(anyString()))
+    .andThrow(new IOException("单元测试特意抛的异常")); 
+
+// 模拟返回指定的数据
+expect(mock.getSomeThing(anyString()))
+    .andReturn("abcd");  
+```
+4、设置Mock Object变成可用状态。
+```java
+replay(mock);
+```
+5、运行单元测试代码(会调用到Mock Object的方法)。
+```java
+// 如果是校验调用次数就要用到verify方法
+verify(mock);
+```
+
+实践
+---
+### 业务代码
+[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/easymock/UserService.java)
+```java
+package cn.aofeng.demo.easymock;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+
+import cn.aofeng.demo.jetty.HttpGet;
+
+/**
+ * 用户相关服务。如:获取用户昵称。
+ * 
+ * @author 聂勇
+ */
+public class UserService {
+
+    private static Logger _logger = Logger.getLogger(UserService.class);
+    
+    private HttpGet _httpGet = new HttpGet();
+    
+    /**
+     * 根据用户的账号ID获取昵称。
+     * 
+     * @param accountId 用户的账号ID
+     * @return 如果账号ID有效且请求成功,返回昵称;否则返回默认的昵称"用户xxx"。
+     */
+    public String getNickname(String accountId) {
+        String targetUrl = "http://192.168.56.102:8080/user?method=getNickname&accountId="+accountId;
+        String response = null;
+        try {
+            response = _httpGet.getSomeThing(targetUrl);
+        } catch (IOException e) {
+            _logger.error("获取用户昵称时出错,账号ID:"+accountId, e);
+        }
+        
+        if (null != response) {
+            // 响应数据结构示例:{"nickname":"张三"}
+            Type type = new TypeToken>() {}.getType();
+            Map data = new Gson().fromJson(response, type);
+            if (null != data && data.containsKey("nickname")) {
+                return data.get("nickname");
+            }
+        }
+        
+        return "用户"+accountId;
+    }
+
+    protected void setHttpGet(HttpGet httpGet) {
+        this._httpGet = httpGet;
+    }
+
+}
+```
+
+[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/jetty/HttpGet.java)
+```java
+package cn.aofeng.demo.jetty;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * HTTP GET请求。
+ * 
+ * @author 聂勇
+ */
+public class HttpGet {
+
+    public String getSomeThing(String urlStr) throws IOException {
+        URL url = new URL(urlStr);
+        HttpURLConnection urlConn = (HttpURLConnection) url.openConnection();
+        urlConn.setConnectTimeout(3000);
+        urlConn.setRequestMethod("GET");
+        urlConn.connect();
+        
+        InputStream ins = null;
+        try {
+            if (200 == urlConn.getResponseCode()) {
+                ins = urlConn.getInputStream();
+                ByteArrayOutputStream outs = new ByteArrayOutputStream(1024);
+                IOUtils.copy(ins, outs);
+                return outs.toString("UTF-8");
+            }
+        } catch (IOException e) {
+            throw e;
+        } finally {
+            IOUtils.closeQuietly(ins);
+        }
+        
+        return null;
+    }
+
+}
+```
+
+### 单元测试代码
+[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/easymock/UserServiceTest.java)
+```java
+package cn.aofeng.demo.easymock;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+import static org.easymock.EasyMock.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import cn.aofeng.demo.jetty.HttpGet;
+
+/**
+ * {@link UserService}的单元测试用例。
+ * 
+ * @author 聂勇
+ */
+public class UserServiceTest {
+
+    private HttpGet _mock = createMock(HttpGet.class);
+    
+    @Before
+    public void setUp() throws Exception {
+    }
+    
+    @After
+    public void tearDown() throws Exception {
+        reset(_mock);
+    }
+    
+    
+    /**
+     * 测试用例:获取用户昵称 
+ * 前置条件: + *
+     * 网络请求时发生IO异常
+     * 
+ * + * 测试结果: + *
+     * 返回默认的用户昵称"用户xxx"
+     * 
+ */ + @Test + public void testGetNickname4OccursIOError() throws IOException { + // 设置Mock + expect(_mock.getSomeThing(anyString())) + .andThrow(new IOException("单元测试特意抛的异常")); + replay(_mock); + + UserService us = new UserService(); + us.setHttpGet(_mock); + String nickname = us.getNickname("123456"); + verify(_mock); // 校验mock + assertEquals("用户123456", nickname); // 检查返回值 + } + + /** + * 测试用例:获取用户昵称
+ * 前置条件: + *
+     * 1、网络请求成功。
+     * 2、响应状态码为200且响应内容符合接口定义({\"nickname\":\"张三\"})。
+     * 
+ * + * 测试结果: + *
+     * 返回"张三"
+     * 
+ */ + @Test + public void testGetNickname4Success() throws IOException { + // 设置Mock + _mock.getSomeThing(anyString()); + expectLastCall().andReturn("{\"nickname\":\"张三\"}"); + expectLastCall().times(1); + replay(_mock); + + UserService us = new UserService(); + us.setHttpGet(_mock); + String nickname = us.getNickname("123456"); + verify(_mock); // 校验方法的调用次数 + assertEquals("张三", nickname); // 校验返回值 + } + +} +``` diff --git a/Java基础教程/Java源代码/codedemo/easymock/UserService.java b/Java基础教程/Java源代码/codedemo/easymock/UserService.java new file mode 100644 index 00000000..ea0af00e --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/easymock/UserService.java @@ -0,0 +1,56 @@ +package cn.aofeng.demo.easymock; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Map; + +import org.apache.log4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import cn.aofeng.demo.jetty.HttpGet; + +/** + * 用户相关服务。如:获取用户昵称。 + * + * @author 聂勇 + */ +public class UserService { + + private static Logger _logger = Logger.getLogger(UserService.class); + + private HttpGet _httpGet = new HttpGet(); + + /** + * 根据用户的账号ID获取昵称。 + * + * @param accountId 用户的账号ID + * @return 如果账号ID有效且请求成功,返回昵称;否则返回默认的昵称"用户xxx"。 + */ + public String getNickname(String accountId) { + String targetUrl = "http://192.168.56.102:8080/user?method=getNickname&accountId="+accountId; + String response = null; + try { + response = _httpGet.getSomeThing(targetUrl); + } catch (IOException e) { + _logger.error("获取用户昵称时出错,账号ID:"+accountId, e); + } + + if (null != response) { + // 响应数据结构示例:{"nickname":"张三"} + Type type = new TypeToken>() {}.getType(); + Map data = new Gson().fromJson(response, type); + if (null != data && data.containsKey("nickname")) { + return data.get("nickname"); + } + } + + return "用户"+accountId; + } + + protected void setHttpGet(HttpGet httpGet) { + this._httpGet = httpGet; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/easymock/UserServiceTest.java b/Java基础教程/Java源代码/codedemo/easymock/UserServiceTest.java new file mode 100644 index 00000000..a0cac709 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/easymock/UserServiceTest.java @@ -0,0 +1,87 @@ +package cn.aofeng.demo.easymock; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import static org.easymock.EasyMock.*; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import cn.aofeng.demo.jetty.HttpGet; + +/** + * {@link UserService}的单元测试用例。 + * + * @author 聂勇 + */ +public class UserServiceTest { + + private HttpGet _mock = createMock(HttpGet.class); + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + reset(_mock); + } + + + /** + * 测试用例:获取用户昵称
+ * 前置条件: + *
+     * 网络请求时发生IO异常
+     * 
+ * + * 测试结果: + *
+     * 返回默认的用户昵称"用户xxx"
+     * 
+ */ + @Test + public void testGetNickname4OccursIOError() throws IOException { + // 设置Mock + expect(_mock.getSomeThing(anyString())) + .andThrow(new IOException("单元测试特意抛的异常")); + replay(_mock); + + UserService us = new UserService(); + us.setHttpGet(_mock); + String nickname = us.getNickname("123456"); + verify(_mock); // 校验mock + assertEquals("用户123456", nickname); // 检查返回值 + } + + /** + * 测试用例:获取用户昵称
+ * 前置条件: + *
+     * 1、网络请求成功。
+     * 2、响应状态码为200且响应内容符合接口定义({\"nickname\":\"张三\"})。
+     * 
+ * + * 测试结果: + *
+     * 返回"张三"
+     * 
+ */ + @Test + public void testGetNickname4Success() throws IOException { + // 设置Mock + _mock.getSomeThing(anyString()); + expectLastCall().andReturn("{\"nickname\":\"张三\"}"); + expectLastCall().times(1); + replay(_mock); + + UserService us = new UserService(); + us.setHttpGet(_mock); + String nickname = us.getNickname("123456"); + verify(_mock); // 校验方法的调用次数 + assertEquals("张三", nickname); // 校验返回值 + } + +} diff --git a/Java基础教程/Java源代码/codedemo/encrypt/AES.java b/Java基础教程/Java源代码/codedemo/encrypt/AES.java new file mode 100644 index 00000000..253119c8 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/encrypt/AES.java @@ -0,0 +1,54 @@ +package cn.aofeng.demo.encrypt; + +import static cn.aofeng.demo.util.LogUtil.log; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import org.apache.commons.codec.binary.Base64; + +/** + * AES加密与解密。 + * + * @author 聂勇 + */ +public class AES extends EncryptAndDecrypt { + + public final String encryptType = "AES/CBC/PKCS5Padding"; + public final String algorithmParam = "abcdefgh12345678"; + public final String key = "abcdefgh_1234567"; + + public void execute(String data) throws InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + UnsupportedEncodingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + SecretKey secretKey = createSecretKey("AES", key); + byte[] secretData = encrypt(encryptType, secretKey, data, + algorithmParam); + log("使用%s加密后的数据:", encryptType); + log(Base64.encodeBase64String(secretData)); + + String srcStr = decrypt(encryptType, secretKey, secretData, + algorithmParam); + log("解密后的数据:\n%s", srcStr); + } + + public static void main(String[] args) throws UnsupportedEncodingException, + InvalidKeyException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + String data = "炎黄,汉字,english,do it,abcdefghijklmnopqrstuvwxyz,0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ, ~!@#$%^&*()_+=-"; + log("待加密的数据:\n%s", data); + + AES aes = new AES(); + aes.execute(data); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/encrypt/Blowfish.java b/Java基础教程/Java源代码/codedemo/encrypt/Blowfish.java new file mode 100644 index 00000000..c5186ced --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/encrypt/Blowfish.java @@ -0,0 +1,51 @@ +package cn.aofeng.demo.encrypt; + +import static cn.aofeng.demo.util.LogUtil.log; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import org.apache.commons.codec.binary.Base64; + +/** + * Blowfish加密解密。 + * + * @author 聂勇 + */ +public class Blowfish extends EncryptAndDecrypt { + + public final String encryptType = "Blowfish"; + public final String key = "abcdefgh_1234567"; + + public void execute(String data) throws InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + UnsupportedEncodingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + SecretKey secretKey = createSecretKey(encryptType, key); + byte[] secretData = encrypt(encryptType, secretKey, data); + log("使用%s加密后的数据:", encryptType); + log(Base64.encodeBase64String(secretData)); + + String srcStr = decrypt(encryptType, secretKey, secretData); + log("解密后的数据:\n%s", srcStr); + } + + public static void main(String[] args) throws UnsupportedEncodingException, + InvalidKeyException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + String data = "炎黄,汉字,english,do it,abcdefghijklmnopqrstuvwxyz,0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ, ~!@#$%^&*()_+=-"; + log("待加密的数据:\n%s", data); + + Blowfish bf = new Blowfish(); + bf.execute(data); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/encrypt/EncryptAndDecrypt.java b/Java基础教程/Java源代码/codedemo/encrypt/EncryptAndDecrypt.java new file mode 100644 index 00000000..475523e5 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/encrypt/EncryptAndDecrypt.java @@ -0,0 +1,125 @@ +package cn.aofeng.demo.encrypt; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * 加密与解密。 + * + * @author 聂勇 + */ +public class EncryptAndDecrypt { + + public final static String CHARSET = "utf8"; + + /** + * 创建安全密钥。 + * + * @param encryptType + * 加密方式,如:AES,Blowfish。详情查看Java Cryptography Architecture Standard Algorithm Name Documentation + * @param keyStr + * 密钥明文 + * @return 安全密钥 + * @throws UnsupportedEncodingException + * 不支持指定的字符集编码 + */ + public SecretKey createSecretKey(String encryptType, String keyStr) + throws UnsupportedEncodingException { + byte[] secretKeyData = keyStr.getBytes(CHARSET); + SecretKeySpec sks = new SecretKeySpec(secretKeyData, encryptType); + + return sks; + } + + /** + * 加密数据。 + * + * @param encryptType 加密方式,如:AES,Blowfish。详情查看Java Cryptography Architecture Standard Algorithm Name Documentation + * @param secretKey 密钥 + * @param srcData 待加密的源数据 + * @return 加密后的二进制数据(字节数组) + * @see #encrypt(String, SecretKey, String, String) + */ + public byte[] encrypt(String encryptType, SecretKey secretKey, + String srcData) throws InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + UnsupportedEncodingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + return encrypt(encryptType, secretKey, srcData, null); + } + + /** + * 加密数据。 + * + * @param encryptType 加密类型,如:AES/CBC/PKCS5Padding + * @param secretKey 密钥 + * @param srcData 待加密的源数据 + * @param algorithmParam 某些加密算法的附加参数 + * @return 加密后的二进制数据(字节数组) + */ + public byte[] encrypt(String encryptType, SecretKey secretKey, + String srcData, String algorithmParam) throws InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + UnsupportedEncodingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + Cipher encrpyt = Cipher.getInstance(encryptType); + if (null == algorithmParam) { + encrpyt.init(Cipher.ENCRYPT_MODE, secretKey); + } else { + IvParameterSpec iv = new IvParameterSpec( + algorithmParam.getBytes(CHARSET)); + encrpyt.init(Cipher.ENCRYPT_MODE, secretKey, iv); + } + byte[] secretData = encrpyt.doFinal(srcData.getBytes(CHARSET)); + + return secretData; + } + + /** + * 解密数据。 + * + * @param decryptType 解密方式,如:AES,Blowfish。详情查看Java Cryptography Architecture Standard Algorithm Name Documentation + * @param secretKey 密钥 + * @param secretData 待解密的数据 + * @return 解密后的数据 + * @see #decrypt(String, SecretKey, byte[], String) + */ + public String decrypt(String decryptType, SecretKey secretKey, + byte[] secretData) throws InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + UnsupportedEncodingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + return decrypt(decryptType, secretKey, secretData, null); + } + + public String decrypt(String decryptType, SecretKey secretKey, + byte[] secretData, String algorithmParam) + throws InvalidKeyException, IllegalBlockSizeException, + BadPaddingException, UnsupportedEncodingException, + NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Cipher decrypt = Cipher.getInstance(decryptType); + if (null == algorithmParam) { + decrypt.init(Cipher.DECRYPT_MODE, secretKey); + } else { + IvParameterSpec iv = new IvParameterSpec( + algorithmParam.getBytes(CHARSET)); + decrypt.init(Cipher.DECRYPT_MODE, secretKey, iv); + } + byte[] srcData = decrypt.doFinal(secretData); + String srcStr = new String(srcData, CHARSET); + + return srcStr; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/encrypt/HmacSha1.java b/Java基础教程/Java源代码/codedemo/encrypt/HmacSha1.java new file mode 100644 index 00000000..4a6864e2 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/encrypt/HmacSha1.java @@ -0,0 +1,53 @@ +package cn.aofeng.demo.encrypt; + +import static cn.aofeng.demo.util.LogUtil.log; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import org.apache.commons.codec.binary.Base64; + +/** + * HMAC-SHA1签名算法。 + * + * @author 聂勇 + */ +public class HmacSha1 { + + public final String encryptType = "HmacSHA1"; + public final String key = "abcdefgh_1234567"; + + public void execute(String data) throws UnsupportedEncodingException, + NoSuchAlgorithmException, InvalidKeyException { + EncryptAndDecrypt ead = new EncryptAndDecrypt(); + + byte[] srcData = data.getBytes(EncryptAndDecrypt.CHARSET); + SecretKey secretKey = ead.createSecretKey(encryptType, key); // 生成密钥对象 + Mac mac = Mac.getInstance(encryptType); + mac.init(secretKey); + byte[] result = mac.doFinal(srcData); + + log("使用%s签名后的数据:", encryptType); + log(Base64.encodeBase64String(result)); + } + + public static void main(String[] args) throws InvalidKeyException, + UnsupportedEncodingException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + String data = "炎黄,汉字,english,do it,abcdefghijklmnopqrstuvwxyz,0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ, ~!@#$%^&*()_+=-"; + log("待签名的数据:\n%s", data); + + HmacSha1 hs = new HmacSha1(); + hs.execute(data); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/encrypt/PerformanceCompare.java b/Java基础教程/Java源代码/codedemo/encrypt/PerformanceCompare.java new file mode 100644 index 00000000..8bc697cb --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/encrypt/PerformanceCompare.java @@ -0,0 +1,70 @@ +package cn.aofeng.demo.encrypt; + +import static cn.aofeng.demo.util.LogUtil.log; + +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +/** + * 加密解密性能比较。 + * + * @author 聂勇 + */ +public class PerformanceCompare extends EncryptAndDecrypt { + + public void blowfishPerformence(String data) + throws UnsupportedEncodingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + Blowfish bf = new Blowfish(); + SecretKey secretKey = createSecretKey(bf.encryptType, bf.key); + long startTime = System.currentTimeMillis(); + for (int j = 0; j < 100000; j++) { + bf.encrypt(bf.encryptType, secretKey, data+j); + } + long endTime = System.currentTimeMillis(); + long usedTime = endTime - startTime; + log("使用%s进行%d次加密消耗时间%d毫秒", bf.encryptType, 100000, usedTime); + } + + public void aesPerformence(String data) + throws UnsupportedEncodingException, InvalidKeyException, + IllegalBlockSizeException, BadPaddingException, + NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + AES aes = new AES(); + SecretKey secretKey = createSecretKey("AES", aes.key); + long startTime = System.currentTimeMillis(); + for (int j = 0; j < 100000; j++) { + aes.encrypt(aes.encryptType, secretKey, data+j, + aes.algorithmParam); + } + long endTime = System.currentTimeMillis(); + long usedTime = endTime - startTime; + log("使用%s进行%d次加密消耗时间%d毫秒", aes.encryptType, 100000, usedTime); + } + + public static void main(String[] args) throws InvalidKeyException, + UnsupportedEncodingException, IllegalBlockSizeException, + BadPaddingException, NoSuchAlgorithmException, + NoSuchPaddingException, InvalidAlgorithmParameterException { + String data = "炎黄,汉字,english,do it,abcdefghijklmnopqrstuvwxyz,0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ, ~!@#$%^&*()_+=-"; + log("待加密的数据:\n%s", data); + + PerformanceCompare pc = new PerformanceCompare(); + // AES + pc.aesPerformence(data); + + // Blowfish + pc.blowfishPerformence(data); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_improve/ClientMain.java b/Java基础教程/Java源代码/codedemo/eventdriver_improve/ClientMain.java new file mode 100644 index 00000000..a3341f2e --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_improve/ClientMain.java @@ -0,0 +1,38 @@ +package cn.aofeng.demo.eventdriver_improve; +/** + * 事件驱动调用示例 + * @author 聂勇 aofengblog@163.com + */ +public class ClientMain { + + public static void main(String[] args) { + EventManagement eventManagement = new EventManagement(); + eventManagement.addListener("read", new HelloWorldListener()); + eventManagement.addListener("write", new SimpleListener()); + + EventSource eventSource = new EventSource(eventManagement); + eventSource.fire(new Event("read", "this is a read event")); + eventSource.fire(new Event("write", "this is a write event")); + } + + public static class HelloWorldListener implements EventListener { + + @Override + public void execute(Event event) { + System.out.println("监听器:"+this + "接收到事件,事件类型是:" + + event.getEventType() + ", 事件附带的数据:" + event.getData()); + } + + } + + public static class SimpleListener implements EventListener { + + @Override + public void execute(Event event) { + System.out.println("监听器:"+this + "接收到事件,事件类型是:" + + event.getEventType() + ", 事件附带的数据:" + event.getData()); + } + + } + +} diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_improve/Event.java b/Java基础教程/Java源代码/codedemo/eventdriver_improve/Event.java new file mode 100755 index 00000000..09d15724 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_improve/Event.java @@ -0,0 +1,28 @@ +package cn.aofeng.demo.eventdriver_improve; +/** + * 事件 + * + * @author aofeng + */ +public class Event { + + // 事件附带的数据 + private Object data; + + // 事件类型 + private String eventType; + + public Event(String eventType, Object obj){ + this.eventType = eventType; + this.data = obj; + } + + public Object getData() { + return this.data; + } + + public String getEventType() { + return this.eventType; + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventListener.java b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventListener.java new file mode 100755 index 00000000..7d6e82d1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventListener.java @@ -0,0 +1,16 @@ +package cn.aofeng.demo.eventdriver_improve; +/** + * 事件监听器(监听一个或多个事件并进行具体的处理) + * + * @author aofeng + */ +public interface EventListener { + + /** + * 处理事件 + * + * @param event 事件 + */ + public void execute(Event event); + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventManagement.java b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventManagement.java new file mode 100755 index 00000000..cde8fa35 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventManagement.java @@ -0,0 +1,64 @@ +package cn.aofeng.demo.eventdriver_improve; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 事件管理器。负责 + * + * @author aofeng + */ +public class EventManagement { + + private Map> map = new HashMap>(); + + public EventManagement(){ + + } + + /** + * 向指定事件添加一个监听器 + * + * @param eventType 事件类型 + * @param listener 事件监听器 + * @return 添加成功返回true;添加失败返回false + */ + public boolean addListener(String eventType, EventListener listener){ + List listeners = map.get(eventType); + if (null == listeners) { + listeners = new ArrayList(); + } + boolean result = listeners.add(listener); + map.put(eventType, listeners); + + return result; + } + + /** + * 移除事件的某一个监听器 + * + * @param eventType 事件类型 + * @param listener 事件监听器 + * @return 移除成功返回true;移除失败返回false + */ + public boolean removeListener(String eventType, EventListener listener){ + List listeners = map.get(eventType); + if (null != listeners) { + return listeners.remove(listener); + } + + return false; + } + + /** + * 获取指定事件的监听器 + * + * @param eventType 事件类型 + * @return 如果指定的事件没有监听器返回null;否则返回监听器列表 + */ + public List getEventListeners(String eventType) { + return map.get(eventType); + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventSource.java b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventSource.java new file mode 100755 index 00000000..ecd09ebf --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_improve/EventSource.java @@ -0,0 +1,38 @@ +package cn.aofeng.demo.eventdriver_improve; +import java.util.List; + +/** + * 事件源(事件发送者) + * + * @author aofeng + */ +public class EventSource { + + // 事件管理器 + private EventManagement eventManagement;; + + public EventSource(EventManagement eventManagement){ + this.eventManagement = eventManagement; + } + + /** + * 派发事件 + * + * @param data 事件 + */ + public void fire(Event event) { + if (null == event) { + return; + } + + List listeners = eventManagement.getEventListeners(event.getEventType()); + if (null == listeners) { + return; + } + + for (EventListener listener : listeners) { + listener.execute(event); + } + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_normal/ClientMain.java b/Java基础教程/Java源代码/codedemo/eventdriver_normal/ClientMain.java new file mode 100644 index 00000000..126ddfec --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_normal/ClientMain.java @@ -0,0 +1,33 @@ +package cn.aofeng.demo.eventdriver_normal; +/** + * 事件驱动调用示例 + * @author aofeng + */ +public class ClientMain { + + public static void main(String[] args) { + EventSource eventSource = new EventSource(); + eventSource.addListener(new HelloWorldListener()); + eventSource.addListener(new SimpleListener()); + eventSource.fire("hello, world!"); + } + + public static class HelloWorldListener implements EventListener { + + @Override + public void execute(Event event) { + System.out.println("监听器:"+this + "接收到事件,事件附带的数据:" + event.getData()); + } + + } + + public static class SimpleListener implements EventListener { + + @Override + public void execute(Event event) { + System.out.println("监听器:"+this + "接收到事件,事件附带的数据:" + event.getData()); + } + + } + +} diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_normal/Event.java b/Java基础教程/Java源代码/codedemo/eventdriver_normal/Event.java new file mode 100755 index 00000000..4a1f9fc6 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_normal/Event.java @@ -0,0 +1,17 @@ +package cn.aofeng.demo.eventdriver_normal; +/** + * 事件 + * @author aofeng + */ +public class Event { + + private Object data; + + public Event(Object obj){ + this.data = obj; + } + + public Object getData() { + return data; + } +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventListener.java b/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventListener.java new file mode 100755 index 00000000..a1475a21 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventListener.java @@ -0,0 +1,16 @@ +package cn.aofeng.demo.eventdriver_normal; +/** + * 事件监听器(监听一个或多个事件并进行具体的处理) + * + * @author aofeng + */ +public interface EventListener { + + /** + * 处理事件 + * + * @param event 事件 + */ + public void execute(Event event); + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventSource.java b/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventSource.java new file mode 100755 index 00000000..ef87d222 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/eventdriver_normal/EventSource.java @@ -0,0 +1,47 @@ +package cn.aofeng.demo.eventdriver_normal; +import java.util.ArrayList; +import java.util.List; + +/** + * 事件源(事件发送者) + * + * @author aofeng + */ +public class EventSource { + + private List listeners = new ArrayList(); + + public EventSource() { + + } + + /** + * 添加事件监听器 + * + * @param listener 事件监听器 + */ + public boolean addListener(EventListener listener) { + return listeners.add(listener); + } + + /** + * 移除事件监听器 + * + * @param listener 移除事件监听器 + */ + public boolean removeListener(EventListener listener) { + return listeners.remove(listener); + } + + /** + * 派发事件 + * + * @param data 事件 + */ + public void fire(Object data) { + for (EventListener listener : listeners) { + listener.execute(new Event(data)); + } + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/httpclient/FluentApi.java b/Java基础教程/Java源代码/codedemo/httpclient/FluentApi.java new file mode 100644 index 00000000..89340d40 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/FluentApi.java @@ -0,0 +1,85 @@ +/** + * 创建时间:2016-8-5 + */ +package cn.aofeng.demo.httpclient; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.fluent.Request; +import org.apache.http.client.fluent.Response; +import org.apache.http.message.BasicNameValuePair; +import org.apache.log4j.Logger; + +import cn.aofeng.demo.httpclient.server.SimpleHttpServer; + +/** + * 使用Fluent API快速地进行简单的HTTP的请求和响应处理,启动{@link SimpleHttpServer}作为请求的服务端。 + * + * @author 聂勇 + */ +public class FluentApi { + + private static Logger _logger = Logger.getLogger(FluentApi.class); + + private static String _targetHost = "http://127.0.0.1:8888"; + + private static String _charset = "utf-8"; + + /** + * HTTP GET请求,关闭Keep-Alive。 + * + * @param targetUrl 请求的地址 + * @param charset 将响应流转换成字符串时使用的编码 + */ + public static void get(String targetUrl, String charset) { + Response response = null; + try { + response = Request.Get(targetUrl).setHeader("Connection", "close").execute(); + String content = response.returnContent().asString(Charset.forName(charset)); + _logger.info(content); + } catch (ClientProtocolException e) { + _logger.error("协议问题", e); + } catch (IOException e) { + _logger.error("连接服务器或读取响应出错", e); + } + } + + /** + * HTTP POST请求,默认开启Keep-Alive,表单数据使用utf-8编码。 + * + * @param targetUrl 请求的地址 + * @param charset 请求表单内容处理和将响应流转换成字符串时使用的编码 + */ + public static void post(String targetUrl, String charset) { + List form = new ArrayList(); + form.add(new BasicNameValuePair("hello", "喂")); + form.add(new BasicNameValuePair("gogogo", "走走走")); + + Response response = null; + try { + response = Request.Post(targetUrl) + .bodyForm(form, Charset.forName(charset)) + .execute(); + String content = response.returnContent().asString(Charset.forName(charset)); + _logger.info(content); + } catch (ClientProtocolException e) { + _logger.error("协议问题", e); + } catch (IOException e) { + _logger.error("连接服务器或读取响应出错", e); + } + } + + /** + * @param args + */ + public static void main(String[] args) { + get(_targetHost+"/get", _charset); + post(_targetHost+"/post", _charset); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/httpclient/HttpClientBasic.java b/Java基础教程/Java源代码/codedemo/httpclient/HttpClientBasic.java new file mode 100644 index 00000000..3be72816 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/HttpClientBasic.java @@ -0,0 +1,110 @@ +/** + * 创建时间:2016-2-23 + */ +package cn.aofeng.demo.httpclient; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.FileEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.apache.log4j.Logger; + +/** + * HttpClient的基本操作。 + * + * @author 聂勇 + */ +public class HttpClientBasic { + + private static Logger _logger = Logger.getLogger(HttpClientBasic.class); + + private static String _targetHost = "http://127.0.0.1:8888"; + + private static String _charset = "utf-8"; + + public void get() throws URISyntaxException, ClientProtocolException, IOException { + CloseableHttpClient client = HttpClients.createDefault(); + HttpGet get = new HttpGet(_targetHost+"/get"); + CloseableHttpResponse response = client.execute(get); + processResponse(response); + } + + public void post() throws ClientProtocolException, IOException { + List params = new ArrayList(); + params.add(new BasicNameValuePair("chinese", "中文")); + params.add(new BasicNameValuePair("english", "英文")); + UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, _charset); + + CloseableHttpClient client = HttpClients.createDefault(); + HttpPost post = new HttpPost(_targetHost+"/post"); + post.addHeader("Cookie", "character=abcdefghijklmnopqrstuvwxyz; sign=abc-123-jkl-098"); + post.setEntity(entity); + CloseableHttpResponse response = client.execute(post); + processResponse(response); + } + + public void sendFile(String filePath) throws UnsupportedOperationException, IOException { + CloseableHttpClient client = HttpClients.createDefault(); + HttpPost post = new HttpPost(_targetHost+"/file"); + File file = new File(filePath); + FileEntity entity = new FileEntity(file, ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), _charset)); + post.setEntity(entity); + CloseableHttpResponse response = client.execute(post); + processResponse(response); + } + + private void processResponse(CloseableHttpResponse response) + throws UnsupportedOperationException, IOException { + try { + // 获取响应头 + Header[] headers = response.getAllHeaders(); + for (Header header : headers) { + _logger.info(header.getName() + ":" + header.getValue()); + } + + // 获取状态信息 + StatusLine sl =response.getStatusLine(); + _logger.info( String.format("ProtocolVersion:%s, StatusCode:%d, Desc:%s", + sl.getProtocolVersion().toString(), sl.getStatusCode(), sl.getReasonPhrase()) ); + + // 获取响应内容 + HttpEntity entity = response.getEntity(); + _logger.info( String.format("ContentType:%s, Length:%d, Encoding:%s", + null == entity.getContentType() ? "" : entity.getContentType().getValue(), + entity.getContentLength(), + null == entity.getContentEncoding() ? "" : entity.getContentEncoding().getValue()) ); + _logger.info(EntityUtils.toString(entity, _charset)); +// _logger.info( IOUtils.toString(entity.getContent(), _charset) ); // 大部分情况下效果与上行语句等同,但实现上的编码处理不同 + } finally { + response.close(); + } + } + + /** + * @param args + */ + public static void main(String[] args) throws Exception { + HttpClientBasic basic = new HttpClientBasic(); +// basic.get(); +// basic.post(); + basic.sendFile("/devdata/projects/open_source/mine/JavaTutorial/LICENSE"); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/httpclient/README.md b/Java基础教程/Java源代码/codedemo/httpclient/README.md new file mode 100644 index 00000000..33c669b9 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/README.md @@ -0,0 +1,9 @@ +#HTTP +##HTTP客户端 +###代码 +* [使用Fluent API发起HTTP请求(Get和Post)](cn/aofeng/demo/httpclient/FluentApi.java) +* [使用HttpClient发起HTTP请求(Get, Post和上传文件)](cn/aofeng/demo/httpclient/FluentApi.java) + +##HTTP服务端 +###代码 +* [使用JDK中的API建立简单的HTTP Server](src/cn/aofeng/demo/httpclient/SimpleHttpServer.java) diff --git a/Java基础教程/Java源代码/codedemo/httpclient/server/AbstractHandler.java b/Java基础教程/Java源代码/codedemo/httpclient/server/AbstractHandler.java new file mode 100644 index 00000000..ce439a4c --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/server/AbstractHandler.java @@ -0,0 +1,71 @@ +/** + * 创建时间:2016-8-18 + */ +package cn.aofeng.demo.httpclient.server; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +/** + * @author 聂勇 + */ +public abstract class AbstractHandler implements HttpHandler { + + private static Logger _logger = Logger.getLogger(AbstractHandler.class); + + protected String _charset = "utf-8"; + + public AbstractHandler(String charset) { + this._charset = charset; + } + + /** + * 处理请求头。 + */ + public void handleHeader(HttpExchange httpEx) { + InetSocketAddress remoteAddress = httpEx.getRemoteAddress(); + _logger.info("收到来自"+remoteAddress.getAddress().getHostAddress()+":"+remoteAddress.getPort()+"的请求"); + + URI rUri = httpEx.getRequestURI(); + _logger.info("请求地址:"+rUri.toString()); + + String method = httpEx.getRequestMethod(); + _logger.info("请求方法:"+method); + + Headers headers = httpEx.getRequestHeaders(); + Set>> headerSet = headers.entrySet(); + _logger.info("请求头:"); + for (Entry> header : headerSet) { + _logger.info(header.getKey()+":"+header.getValue()); + } + } + + /** + * 处理响应。 + */ + public void handleResponse(HttpExchange httpEx, String content) + throws UnsupportedEncodingException, IOException { + String rc = "冒号后面是收到的请求,原样返回:"+content; + byte[] temp = rc.getBytes(_charset); + Headers outHeaders = httpEx.getResponseHeaders(); + outHeaders.set("ABC", "123"); + httpEx.sendResponseHeaders(200, temp.length); + OutputStream outs = httpEx.getResponseBody(); + outs.write(temp); + IOUtils.closeQuietly(outs); + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/httpclient/server/BinaryHandler.java b/Java基础教程/Java源代码/codedemo/httpclient/server/BinaryHandler.java new file mode 100644 index 00000000..d9336ed2 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/server/BinaryHandler.java @@ -0,0 +1,66 @@ +/** + * 创建时间:2016-8-18 + */ +package cn.aofeng.demo.httpclient.server; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +/** + * 二进制处理器。 + * + * @author 聂勇 + */ +public class BinaryHandler extends AbstractHandler implements HttpHandler { + + private static Logger _logger = Logger.getLogger(BinaryHandler.class); + + private String _dir = "/home/nieyong/temp/JavaTutorial"; + + public BinaryHandler(String charset) { + super(charset); + } + + @Override + public void handle(HttpExchange httpEx) throws IOException { + super.handleHeader(httpEx); + handleRequest(httpEx); + String content = "收到一个二进制的请求"; + super.handleResponse(httpEx, content); + } + + /** + * 处理请求。 + * @throws IOException + */ + public String handleRequest(HttpExchange httpEx) throws IOException { + OutputStream outs = null; + InputStream ins = null; + try { + File file = new File(_dir, ""+System.currentTimeMillis()); + if (!file.exists()) { + file.createNewFile(); + } + outs = new FileOutputStream(file); + ins = httpEx.getRequestBody(); + IOUtils.copyLarge(ins, outs); + } catch (Exception e) { + _logger.error("read request or write file occurs error", e); + } finally { + IOUtils.closeQuietly(ins); + IOUtils.closeQuietly(outs); + } + + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/httpclient/server/CharacterHandler.java b/Java基础教程/Java源代码/codedemo/httpclient/server/CharacterHandler.java new file mode 100644 index 00000000..ac5e444b --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/server/CharacterHandler.java @@ -0,0 +1,50 @@ +/** + * 创建时间:2016-8-18 + */ +package cn.aofeng.demo.httpclient.server; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import org.apache.commons.io.IOUtils; +import org.apache.log4j.Logger; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +/** + * 字符串处理器。 + * + * @author 聂勇 + */ +public class CharacterHandler extends AbstractHandler implements HttpHandler { + + static Logger _logger = Logger .getLogger(CharacterHandler.class); + + public CharacterHandler(String charset) { + super(charset); + } + + @Override + public void handle(HttpExchange httpEx) throws IOException { + super.handleHeader(httpEx); + String content = handleRequest(httpEx); + super.handleResponse(httpEx, content); + } + + /** + * 处理请求。 + */ + public String handleRequest(HttpExchange httpEx) + throws UnsupportedEncodingException, IOException { + InputStream ins = httpEx.getRequestBody(); + String content = URLDecoder.decode( + IOUtils.toString(ins, _charset), _charset); + _logger.info("请求内容:"+content); + IOUtils.closeQuietly(ins); + return content; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/httpclient/server/SimpleHttpServer.java b/Java基础教程/Java源代码/codedemo/httpclient/server/SimpleHttpServer.java new file mode 100644 index 00000000..d82088e7 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/httpclient/server/SimpleHttpServer.java @@ -0,0 +1,41 @@ +/** + * 创建时间:2016-8-5 + */ +package cn.aofeng.demo.httpclient.server; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import org.apache.log4j.Logger; + +import com.sun.net.httpserver.HttpServer; + +/** + * 简单的HTTP Server。 + * + * @author 聂勇 + */ +public class SimpleHttpServer { + + private static Logger _logger = Logger.getLogger(SimpleHttpServer.class); + + private static String _charset = "utf-8"; + + /** + * @param args + */ + public static void main(String[] args) { + int port = 8888; + try { + HttpServer server = HttpServer.create(new InetSocketAddress(port), 128); + server.createContext("/get", new CharacterHandler(_charset)); + server.createContext("/post", new CharacterHandler(_charset)); + server.createContext("/file", new BinaryHandler(_charset)); + server.start(); + _logger.info("http server already started, listen port:"+port); + } catch (IOException e) { + _logger.error("", e); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/io/MultiThreadEchoServer.java b/Java基础教程/Java源代码/codedemo/io/MultiThreadEchoServer.java new file mode 100644 index 00000000..4bb2ef9d --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/io/MultiThreadEchoServer.java @@ -0,0 +1,53 @@ +package cn.aofeng.demo.io; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 多线程网络echo服务。每接收到一个新连接都新建一个线程处理,连接关闭后线程随之销毁。 + * + * @author NieYong + */ +public class MultiThreadEchoServer { + + private final static Logger logger = Logger.getLogger(MultiThreadEchoServer.class.getName()); + + /** + * @param args [0]-监听端口 + */ + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("无效的参数。使用示例:"); + System.err.println(" java cn.aofeng.demo.io.MultiThreadEchoServer 9090"); + System.exit(-1); + } + + int port = Integer.parseInt(args[0]); + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(port)); + if (logger.isLoggable(Level.INFO)) { + logger.info("多线程网络echo服务启动完毕,监听端口:" +port); + } + while (true) { + // 接收新的客户端连接 + Socket socket = serverSocket.accept(); + if (logger.isLoggable(Level.INFO)) { + logger.info("收到一个新的连接,客户端IP:"+socket.getInetAddress().getHostAddress()+",客户端Port:"+socket.getPort()); + } + + // 新建一个线程处理Socket连接 + Thread thread = new Thread(new Worker(socket)); + thread.start(); + } + } catch (IOException e) { + logger.log(Level.SEVERE, "处理网络连接出错", e); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/io/ThreadPoolEchoServer.java b/Java基础教程/Java源代码/codedemo/io/ThreadPoolEchoServer.java new file mode 100644 index 00000000..e53de75d --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/io/ThreadPoolEchoServer.java @@ -0,0 +1,55 @@ +package cn.aofeng.demo.io; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 线程池网络echo服务。每接收到一个新连接都由线程池中的空闲线程处理,连接关闭后释放线程(不会销毁线程,仍在线程池中)。 + * + * @author NieYong + */ +public class ThreadPoolEchoServer { + + private final static Logger logger = Logger.getLogger(MultiThreadEchoServer.class.getName()); + + /** + * @param args [0]-监听端口 + */ + public static void main(String[] args) { + if (args.length != 1) { + System.err.println("无效的参数。使用示例:"); + System.err.println(" java cn.aofeng.demo.io.ThreadPoolEchoServer 9090"); + System.exit(-1); + } + + int port = Integer.parseInt(args[0]); + ExecutorService threadpool = Executors.newFixedThreadPool(5); + ServerSocket serverSocket = null; + try { + serverSocket = new ServerSocket(); + serverSocket.bind(new InetSocketAddress(port)); + if (logger.isLoggable(Level.INFO)) { + logger.info("线程池网络echo服务启动完毕,监听端口:" +port); + } + while (true) { + // 接收新的客户端连接 + Socket socket = serverSocket.accept(); + if (logger.isLoggable(Level.INFO)) { + logger.info("收到一个新的连接,客户端IP:"+socket.getInetAddress().getHostAddress()+",客户端Port:"+socket.getPort()); + } + + // 将Socket连接交给线程池处理 + threadpool.submit(new Worker(socket)); + } + } catch (IOException e) { + logger.log(Level.SEVERE, "处理网络连接出错", e); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/io/Worker.java b/Java基础教程/Java源代码/codedemo/io/Worker.java new file mode 100644 index 00000000..45b68dd0 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/io/Worker.java @@ -0,0 +1,77 @@ +package cn.aofeng.demo.io; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 处理客户端Socket连接的工作线程。 + * + * @author 聂勇 aofengblog@163.com + */ +public class Worker implements Runnable { + + private final static Logger logger = Logger.getLogger(MultiThreadEchoServer.class.getName()); + + // 字符集编码 + private final static String CHAR_SET = "utf8"; + + // 行结束符 + private final static String CRLF = "\r\n"; + + private Socket socket = null; + + public Worker(Socket socket) { + this.socket = socket; + } + + public void setSocket(Socket socket) { + this.socket = socket; + } + + public void close(Closeable c) { + if (null != c) { + try { + c.close(); + } catch (IOException e) { + // ingore + } + } + } + + @Override + public void run() { + if (null == socket || socket.isClosed()) { + logger.warning("无效的Socket连接:" + socket); + return; + } + + String lineEnd = CRLF; + try { + BufferedReader reader = new BufferedReader( + new InputStreamReader( + socket.getInputStream())); + OutputStream outs = socket.getOutputStream(); + String line = null; + while ( null != (line = reader.readLine()) ) { + // 客户端退出 + if ("quit".equalsIgnoreCase(line) || "exit".equalsIgnoreCase(line)) { + break; + } + + outs.write(line.getBytes(CHAR_SET)); + outs.write(lineEnd.getBytes(CHAR_SET)); + } + close(reader); + close(outs); + } catch (IOException e) { + logger.log(Level.SEVERE, "读取网络连接数据出错", e); + } + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstInstrumentation.java b/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstInstrumentation.java new file mode 100644 index 00000000..d4319caf --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstInstrumentation.java @@ -0,0 +1,25 @@ +package cn.aofeng.demo.java.lang.instrument; + +import java.lang.instrument.Instrumentation; + +import org.apache.commons.lang.StringUtils; + +import cn.aofeng.demo.util.LogUtil; + +/** + * Instrument入口类。 + * + * @author 聂勇 + */ +public class FirstInstrumentation { + + public static void premain(String options, Instrumentation ins) { + if (StringUtils.isBlank(options)) { + LogUtil.log("instrument without options"); + } else { + LogUtil.log("instrument with options:%s", options); + } + + ins.addTransformer(new FirstTransformer()); + } +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstTransformer.java b/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstTransformer.java new file mode 100644 index 00000000..48ce6912 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/instrument/FirstTransformer.java @@ -0,0 +1,26 @@ +/** + * + */ +package cn.aofeng.demo.java.lang.instrument; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +import cn.aofeng.demo.util.LogUtil; + +/** + * 只输出问候语,不进行字节码修改的Class转换器。 + * + * @author 聂勇 + */ +public class FirstTransformer implements ClassFileTransformer { + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + LogUtil.log(">>> %s", className); + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/instrument/Hello.java b/Java基础教程/Java源代码/codedemo/java/lang/instrument/Hello.java new file mode 100644 index 00000000..01fd76f5 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/instrument/Hello.java @@ -0,0 +1,14 @@ +package cn.aofeng.demo.java.lang.instrument; + +/** + * Instrumentation启动类。 * + * + * @author 聂勇 + */ +public class Hello { + + public static void main(String[] args) { + // nothing + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/instrument/README.md b/Java基础教程/Java源代码/codedemo/java/lang/instrument/README.md new file mode 100644 index 00000000..53fe4e51 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/instrument/README.md @@ -0,0 +1,40 @@ +# 一、Instrumentation入门 + +* [FirstTransformer.java](FirstTransformer.java) 处理字节码,由类FirstInstrumentation执行 +* [FirstInstrumentation.java](FirstInstrumentation.java) instrumentation入口类,由javaagent载入执行 +* [build.xml](build.xml) Ant脚本,负责编译、打包和运行 + +在当前目录下执行命令: +```bash +ant +``` + +输出信息如下: +> [java] instrument with options:"Hello, Instrumentation" +> [java] >>> java/lang/invoke/MethodHandleImpl +> [java] >>> java/lang/invoke/MethodHandleImpl$1 +> [java] >>> java/lang/invoke/MethodHandleImpl$2 +> [java] >>> java/util/function/Function +> [java] >>> java/lang/invoke/MethodHandleImpl$3 +> [java] >>> java/lang/invoke/MethodHandleImpl$4 +> [java] >>> java/lang/ClassValue +> [java] >>> java/lang/ClassValue$Entry +> [java] >>> java/lang/ClassValue$Identity +> [java] >>> java/lang/ClassValue$Version +> [java] >>> java/lang/invoke/MemberName$Factory +> [java] >>> java/lang/invoke/MethodHandleStatics +> [java] >>> java/lang/invoke/MethodHandleStatics$1 +> [java] >>> sun/misc/PostVMInitHook +> [java] >>> sun/usagetracker/UsageTrackerClient +> [java] >>> java/util/concurrent/atomic/AtomicBoolean +> [java] >>> sun/usagetracker/UsageTrackerClient$1 +> [java] >>> sun/usagetracker/UsageTrackerClient$4 +> [java] >>> sun/usagetracker/UsageTrackerClient$3 +> [java] >>> java/io/FileOutputStream$1 +> [java] >>> sun/launcher/LauncherHelper +> [java] >>> cn/aofeng/demo/java/lang/instrument/Hello +> [java] >>> sun/launcher/LauncherHelper$FXHelper +> [java] >>> java/lang/Class$MethodArray +> [java] >>> java/lang/Void +> [java] >>> java/lang/Shutdown +> [java] >>> java/lang/Shutdown$Lock diff --git a/Java基础教程/Java源代码/codedemo/java/lang/instrument/build.xml b/Java基础教程/Java源代码/codedemo/java/lang/instrument/build.xml new file mode 100644 index 00000000..2abfbb1c --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/instrument/build.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Java基础教程/Java源代码/codedemo/java/lang/reflect/ClassAnalyze.java b/Java基础教程/Java源代码/codedemo/java/lang/reflect/ClassAnalyze.java new file mode 100644 index 00000000..931b7c39 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/reflect/ClassAnalyze.java @@ -0,0 +1,143 @@ +package cn.aofeng.demo.java.lang.reflect; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * 通过反射获取类的构造方法、字段、方法和注解等信息。 + * + * @author 聂勇 + */ +public class ClassAnalyze { + + final static String PREFIX = "=========="; + final static String SUFFIX = PREFIX; + + private static void parseClass(Class claz) { + // 注解 + parseAnnotation(claz); + + // 类 + StringBuilder buffer = new StringBuilder(32) + .append( Modifier.toString(claz.getModifiers()) ) + .append(' ') + .append(claz.getName()); + log(buffer.toString()); + } + + private static void parseConstructor(Constructor c) { + // 注解 + parseAnnotation(c); + + // 构造方法 + StringBuilder buffer = new StringBuilder(32) + .append( parseMember(c) ) + .append('('); + + // 参数 + Class[] params = c.getParameterTypes(); + for (int index = 0; index < params.length; index++) { + buffer.append(params[index].getName()); + if (index!=params.length-1) { + buffer.append(", "); + } + } + buffer.append(')'); + + log(buffer.toString()); + } + + private static void parseMethod(Method method) { + // 注解 + parseAnnotation(method); + + // 方法 + StringBuilder buffer = new StringBuilder(32) + .append( parseMember(method) ) + .append('('); + + // 参数 + Class[] params = method.getParameterTypes(); + for (int index = 0; index < params.length; index++) { + buffer.append(params[index].getName()); + if (index!=params.length-1) { + buffer.append(", "); + } + } + buffer.append(')'); + + log(buffer.toString()); + } + + private static void parseField(Field field) { + // 注解 + parseAnnotation(field); + + // 字段 + StringBuilder buffer = parseMember(field); + log(buffer.toString()); + } + + /** + * 解析方法、字段或构造方法的信息。 + * @param member 方法、字段或构造方法 + * @return 修饰符和名称组成的字符串。 + */ + private static StringBuilder parseMember(Member member) { + StringBuilder buffer = new StringBuilder() + .append(Modifier.toString(member.getModifiers())) + .append(' ') + .append(member.getName()); + return buffer; + } + + /** + * 解析注解信息。 + */ + private static void parseAnnotation(AnnotatedElement ae) { + Annotation[] ans = ae.getDeclaredAnnotations(); + for (Annotation annotation : ans) { + log(annotation.toString()); + } + } + + public static void log(String msg, Object... param) { + System.out.println( String.format(msg, param) ); + } + + public static void main(String[] args) throws ClassNotFoundException { + if (args.length != 1) { + log("无效的输入参数!"); + log("示例:"); + log("java cn.aofeng.demo.java.lang.reflect.ClassAnalyze java.util.HashMap"); + } + Class claz = Class.forName(args[0]); + + log("%s类%s", PREFIX, SUFFIX); + parseClass(claz); + + log("%s构造方法%s", PREFIX, SUFFIX); + Constructor[] cs = claz.getDeclaredConstructors(); + for (Constructor constructor : cs) { + parseConstructor(constructor); + } + + log("%s字段%s", PREFIX, SUFFIX); + Field[] fields = claz.getDeclaredFields(); + for (Field field : fields) { + parseField(field); + } + + log("%s方法%s", PREFIX, SUFFIX); + Method[] methods = claz.getDeclaredMethods(); + for (Method method : methods) { + parseMethod(method); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/reflect/CreateInstance.java b/Java基础教程/Java源代码/codedemo/java/lang/reflect/CreateInstance.java new file mode 100644 index 00000000..2bf47632 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/reflect/CreateInstance.java @@ -0,0 +1,36 @@ +package cn.aofeng.demo.java.lang.reflect; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import cn.aofeng.demo.util.LogUtil; + +/** + * 通过反射使用构造方法创建对象实例。 + * + * @author 聂勇 + */ +public class CreateInstance { + + public static void main(String[] args) throws InstantiationException, IllegalAccessException, + NoSuchMethodException, SecurityException, + IllegalArgumentException, InvocationTargetException { + Class claz = Man.class; + + // 调用默认的public构造方法 + Man man = claz.newInstance(); + LogUtil.log(man.toString()); + + // 调用带参数的protected构造方法 + Constructor manC = claz.getDeclaredConstructor(String.class); + man = manC.newInstance("aofeng"); + LogUtil.log(man.toString()); + + // 调用带参数的private构造方法 + manC = claz.getDeclaredConstructor(String.class, int.class); + manC.setAccessible(true); + man = manC.newInstance("NieYong", 32); + LogUtil.log(man.toString()); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeField.java b/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeField.java new file mode 100644 index 00000000..4ec267b3 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeField.java @@ -0,0 +1,37 @@ +package cn.aofeng.demo.java.lang.reflect; + +import java.lang.reflect.Field; + +import static cn.aofeng.demo.util.LogUtil.log; + +/** + * 通过反射设置字段。 + * + * @author 聂勇 + */ +public class InvokeField { + + public static void main(String[] args) throws NoSuchFieldException, + SecurityException, IllegalArgumentException, IllegalAccessException { + Man man = new Man(); + Class claz = man.getClass(); + + log("==========设置public字段的值=========="); + log("height的值:%d", man.height); + Field field = claz.getField("height"); + field.setInt(man, 175); + log("height的值:%d", man.height); + + log("==========设置private字段的值=========="); + log("power的值:%d", man.getPower()); + field = claz.getDeclaredField("power"); + field.setAccessible(true); + field.setInt(man, 100); + log("power的值:%d", man.getPower()); + + log("==========获取private字段的值=========="); + int power = field.getInt(man); + log("power的值:%d", power); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeMethod.java b/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeMethod.java new file mode 100644 index 00000000..d11d7a9a --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/reflect/InvokeMethod.java @@ -0,0 +1,84 @@ +package cn.aofeng.demo.java.lang.reflect; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static cn.aofeng.demo.util.LogUtil.log; + +/** + * 通过反射调用方法。 + * + * @author 聂勇 + */ +public class InvokeMethod { + + /** + * 调用父类的方法。 + * + * @param claz + * 类 + * @param man + * 类对应的实例 + */ + private static void invokeParentMethod(Class claz, Man man) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + // 调用父类的public方法 + Method method = claz.getMethod("setName", String.class); + method.invoke(man, "NieYong"); + log(man.toString()); + + method = claz.getMethod("getName"); + Object result = method.invoke(man); + log("name:%s", result); + + // 调用父类的private方法 + method = claz.getSuperclass().getDeclaredMethod("reset"); + method.setAccessible(true); + result = method.invoke(man); + log(man.toString()); + } + + /** + * 调用自身的方法。 + * + * @param claz + * 类 + * @param man + * 类对应的实例 + */ + private static void invokeSelfMethod(Class claz, Man man) + throws NoSuchMethodException, IllegalAccessException, + InvocationTargetException { + man.setName("XiaoMing"); + // 调用自身的private方法 + Method method = claz.getDeclaredMethod("setPower", int.class); + method.setAccessible(true); + method.invoke(man, 99); + log("power:%d", man.getPower()); + + // 调用自身的public方法 + log("%s is marry:%s", man.getName(), (man.isMarry() ? "Yes" : "No")); + method = claz.getDeclaredMethod("setMarry", boolean.class); + method.invoke(man, true); + log("%s is marry:%s", man.getName(), (man.isMarry() ? "Yes" : "No")); + + // 调用静态方法,可将实例设置为null,因为静态方法属于类 + Man a = new Man("张三"); + Man b = new Man("李四"); + method = claz.getMethod("fight", Man.class, Man.class); + method.invoke(null, a, b); // + } + + public static void main(String[] args) throws NoSuchMethodException, + SecurityException, IllegalAccessException, + IllegalArgumentException, InvocationTargetException, + InstantiationException { + Class claz = Man.class; + Man man = claz.newInstance(); + + invokeParentMethod(claz, man); + invokeSelfMethod(claz, man); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/reflect/Man.java b/Java基础教程/Java源代码/codedemo/java/lang/reflect/Man.java new file mode 100644 index 00000000..41b4b75b --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/reflect/Man.java @@ -0,0 +1,63 @@ +package cn.aofeng.demo.java.lang.reflect; + +import cn.aofeng.demo.json.gson.Person; +import cn.aofeng.demo.util.LogUtil; + +/** + * 男人。 + * + * @author 聂勇 + */ +public class Man extends Person { + + private boolean marry; + + private int power; + + public int height; + + public Man() { + super(); + LogUtil.log("%s的默认构造方法被调用", Man.class.getName()); + } + + protected Man(String name) { + super(name, 0); + LogUtil.log("%s带name参数的构造方法被调用", Man.class.getName()); + } + + @SuppressWarnings("unused") + private Man(String name, int age) { + super(name, age); + LogUtil.log("%s带name和age参数的构造方法被调用", Man.class.getName()); + } + + public static void fight(Man a, Man b) { + String win = "unkown"; + if (a.power > b.power) { + win = a.getName(); + } else if (b.power > a.power) { + win = a.getName(); + } + + LogUtil.log("%s vs %s, fight result:%s", a.getName(), b.getName(), win); + } + + public boolean isMarry() { + return marry; + } + + public void setMarry(boolean marry) { + this.marry = marry; + } + + public int getPower() { + return power; + } + + @SuppressWarnings("unused") + private void setPower(int power) { + this.power = power; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/serialization/Man.java b/Java基础教程/Java源代码/codedemo/java/lang/serialization/Man.java new file mode 100644 index 00000000..840867bf --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/serialization/Man.java @@ -0,0 +1,74 @@ +package cn.aofeng.demo.java.lang.serialization; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +/** + * 自定义序列化和反序列化。 + * + * @author 聂勇 + */ +public class Man implements Externalizable { + + private String manName; + + private int manAge; + + private transient String password; + + public Man() { + // nothing + } + + public Man(String name, int age) { + this.manName = name; + this.manAge = age; + } + + public Man(String name, int age, String password) { + this.manName = name; + this.manAge = age; + this.password = password; + } + + public String getManName() { + return manName; + } + + public void setManName(String manName) { + this.manName = manName; + } + + public int getManAge() { + return manAge; + } + + public void setManAge(int manAge) { + this.manAge = manAge; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(manName); + out.writeInt(manAge); + out.writeObject(password); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + manName = (String) in.readObject(); + manAge = in.readInt(); + password = (String) in.readObject(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/serialization/People.java b/Java基础教程/Java源代码/codedemo/java/lang/serialization/People.java new file mode 100644 index 00000000..70fbb130 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/serialization/People.java @@ -0,0 +1,79 @@ +package cn.aofeng.demo.java.lang.serialization; + +import java.io.Serializable; + +/** + * 默认序列化和 + * + * @author 聂勇 + */ +public class People implements Serializable { + + private static final long serialVersionUID = 6235620243018494633L; + + private String name; + + private int age; + + private transient String address; + + private static String sTestNormal; + + private static transient String sTestTransient; + + public People(String name) { + this.name = name; + } + + public People(String name, int age) { + this.name = name; + this.age = age; + } + + public People(String name, int age, String address) { + this.name = name; + this.age = age; + this.address = address; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getsTestNormal() { + return sTestNormal; + } + + public void setsTestNormal(String sTestNormal) { + People.sTestNormal = sTestNormal; + } + + public String getsTestTransient() { + return sTestTransient; + } + + public void setsTestTransient(String sTestTransient) { + People.sTestTransient = sTestTransient; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/lang/serialization/TransientDemo.java b/Java基础教程/Java源代码/codedemo/java/lang/serialization/TransientDemo.java new file mode 100644 index 00000000..c6f82f5e --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/lang/serialization/TransientDemo.java @@ -0,0 +1,126 @@ +package cn.aofeng.demo.java.lang.serialization; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 关键字 transient 测试。 + * + * @author 聂勇 + */ +public class TransientDemo { + + private static Logger _logger = LoggerFactory.getLogger(TransientDemo.class); + + private String _tempFileName = "TransientDemo"; + + /** + * 将对象序列化并保存到文件。 + * + * @param obj 待序列化的对象 + */ + public void save(Object obj) { + ObjectOutputStream outs = null; + try { + outs = new ObjectOutputStream( + new FileOutputStream( getTempFile(_tempFileName) )); + outs.writeObject(obj); + } catch (IOException e) { + _logger.error("save object to file occurs error", e); + } finally { + IOUtils.closeQuietly(outs); + } + } + + /** + * 从文件读取内容并反序列化成对象。 + * + * @return {@link People}对象。如果读取文件出错 或 对象类型转换失败,返回null。 + */ + public T load() { + ObjectInputStream ins = null; + try { + ins = new ObjectInputStream( + new FileInputStream( getTempFile(_tempFileName)) ); + return ((T) ins.readObject()); + } catch (IOException e) { + _logger.error("load object from file occurs error", e); + } catch (ClassNotFoundException e) { + _logger.error("load object from file occurs error", e); + } finally { + IOUtils.closeQuietly(ins); + } + + return null; + } + + private File getTempFile(String filename) { + return new File(getTempDir(), filename); + } + + private String getTempDir() { + return System.getProperty("java.io.tmpdir"); + } + + private void displayPeople(People people) { + if (null == people) { + return; + } + String template = "People[name:%s, age:%d, address:%s, sTestNormal:%s, sTestTransient:%s]"; + System.out.println( String.format(template, people.getName(), people.getAge(), + people.getAddress(), people.getsTestNormal(), people.getsTestTransient())); + } + + private void displayMan(Man man) { + if (null == man) { + return; + } + String template = "Man[manName:%s, manAge:%d, password:%s]"; + System.out.println( String.format(template, man.getManName(), man.getManAge(), man.getPassword()) ); + } + + /** + * @param args + */ + public static void main(String[] args) { + System.out.println(">>> Serializable测试"); + TransientDemo demo = new TransientDemo(); + + People people = new People("张三", 30, "中国广州"); + people.setsTestNormal("normal-first"); + people.setsTestTransient("transient-first"); + System.out.println("序列化之前的对象信息:"); + demo.displayPeople(people); + demo.save(people); + + // 修改静态变量的值 + people.setsTestNormal("normal-second"); + people.setsTestTransient("transient-second"); + + People fromLoad = demo.load(); + System.out.println("反序列化之后的对象信息:"); + demo.displayPeople(fromLoad); + + + + System.out.println(""); + System.out.println(">>> Externalizable测试"); + Man man = new Man("李四", 10, "假密码"); + System.out.println("序列化之前的对象信息:"); + demo.displayMan(man); + demo.save(man); + + Man manFromLoadMan = demo.load(); + System.out.println("反序列化之后的对象信息:"); + demo.displayMan(manFromLoadMan); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/Gender.java b/Java基础教程/Java源代码/codedemo/java/rmi/Gender.java new file mode 100644 index 00000000..2ba3e7b1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/Gender.java @@ -0,0 +1,20 @@ +package cn.aofeng.demo.java.rmi; + +/** + * 性别。 + * + * @author 聂勇 + */ +public class Gender { + + /** + * 男。 + */ + public final static char MALE = 'M'; + + /** + * 女。 + */ + public final static char FEMALE = 'F'; + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/RmiClient.java b/Java基础教程/Java源代码/codedemo/java/rmi/RmiClient.java new file mode 100644 index 00000000..96f3c751 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/RmiClient.java @@ -0,0 +1,60 @@ +package cn.aofeng.demo.java.rmi; + +import java.net.MalformedURLException; +import java.rmi.Naming; +import java.rmi.NotBoundException; +import java.rmi.RemoteException; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * + * @author 聂勇 + */ +public class RmiClient { + + private static Logger _logger = LoggerFactory.getLogger(RmiClient.class); + + private static String assembleUrl(String hostUrl, String bindName) { + if (StringUtils.isBlank(hostUrl) || StringUtils.isBlank(bindName)) { + return null; + } + + String host = hostUrl.endsWith("/") ? hostUrl.substring(0, hostUrl.length()-1) : hostUrl; + String name = bindName.startsWith("/") ? bindName.substring(1) : bindName; + + return host + "/" + name; + } + + /** + * @param args + *
    + *
  • [0]:待连接的RMI主机。如:rmi://192.168.56.102:9999
  • + *
  • [1]:服务名称。如:UserService
  • + *
+ */ + public static void main(String[] args) throws MalformedURLException, RemoteException, NotBoundException { + String host = args[0]; + String serviceName = args[1]; + String url = assembleUrl(host, serviceName); + UserService userService = (UserService) Naming.lookup(url); + + String userId = "10000"; + User user = userService.findById(userId); + _logger.info("身份证号为{}的用户信息{}", userId, user); + userId = "10001"; + user = userService.findById(userId); + _logger.info("身份证号为{}的用户信息{}", userId, user); + + String userName = "小明"; + user = userService.findByName(userName); + _logger.info("姓名为{}的用户信息{}", userName, user); + userName = "张三"; + user = userService.findByName(userName); + _logger.info("姓名为{}的用户信息{}", userName, user); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/RmiServer.java b/Java基础教程/Java源代码/codedemo/java/rmi/RmiServer.java new file mode 100644 index 00000000..24f9e9fb --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/RmiServer.java @@ -0,0 +1,53 @@ +package cn.aofeng.demo.java.rmi; + +import java.rmi.AlreadyBoundException; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RMI服务端。 + * + * @author 聂勇 + */ +public class RmiServer { + + private static Logger _logger = LoggerFactory.getLogger(RmiServer.class); + + public static Registry createRegistry(int port) { + Registry registry = null; + try { + registry = LocateRegistry.createRegistry(port); + } catch (RemoteException e) { + _logger.info( String.format("注册端口%s失败", port), e); + } + + return registry; + } + + /** + * @param args [0]:绑定端口 + * @throws RemoteException + */ + public static void main(String[] args) throws RemoteException { + int port = Integer.parseInt(args[0]); + UserService userService = new UserServiceImpl(); + Registry registry = createRegistry(port); + if (null == registry) { + System.exit(0); + } + + String bindName = "UserService"; + try { + registry.bind(bindName, userService); + } catch (AlreadyBoundException e) { + _logger.info("服务{}已经绑定过", bindName); + } + + _logger.info("RMI Server started, listen port:{}", port); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/User.java b/Java基础教程/Java源代码/codedemo/java/rmi/User.java new file mode 100644 index 00000000..3f54cfae --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/User.java @@ -0,0 +1,170 @@ +package cn.aofeng.demo.java.rmi; + +import java.io.Serializable; + +/** + * 用户信息。 + * + * @author 聂勇 + */ +public class User implements Serializable { + + private static final long serialVersionUID = 7616705579045104892L; + + /** + * 身份证号。 + */ + private String id; + + /** + * 姓名。 + */ + private String name; + + /** + * 性别:M-男;F-女。 + */ + private char gender; + + /** + * 出生日期。以毫秒存储。 + */ + private long birthday; + + /** + * 国家。 + */ + private String country; + + /** + * 省/州。 + */ + private String province; + + /** + * 市/区。 + */ + private String city; + + /** + * 街道详细地址。 + */ + private String address; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public char getGender() { + return gender; + } + + public void setGender(char gender) { + this.gender = gender; + } + + public long getBirthday() { + return birthday; + } + + public void setBirthday(long birthday) { + this.birthday = birthday; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } + + public String getProvince() { + return province; + } + + public void setProvince(String province) { + this.province = province; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((country == null) ? 0 : country.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + User other = (User) obj; + if (country == null) { + if (other.country != null) + return false; + } else if (!country.equals(other.country)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + @Override + public String toString() { + StringBuilder buffer = new StringBuilder(256) + .append("User [id=").append(id) + .append(", name=").append(name) + .append(", gender=").append(gender) + .append(", birthday=").append(birthday) + .append(", country=").append(country) + .append(", province=").append(province) + .append(", city=").append(city) + .append(", address=").append(address) + .append("]"); + return buffer.toString(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/UserService.java b/Java基础教程/Java源代码/codedemo/java/rmi/UserService.java new file mode 100644 index 00000000..24697436 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/UserService.java @@ -0,0 +1,19 @@ +package cn.aofeng.demo.java.rmi; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +/** + * 用户信息服务。 + * + * @author 聂勇 + */ +public interface UserService extends Remote { + + public User findByName(String name) throws RemoteException; + + public User findById(String id) throws RemoteException; + + public boolean add(User user) throws RemoteException; + +} diff --git a/Java基础教程/Java源代码/codedemo/java/rmi/UserServiceImpl.java b/Java基础教程/Java源代码/codedemo/java/rmi/UserServiceImpl.java new file mode 100644 index 00000000..60e44b8f --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/rmi/UserServiceImpl.java @@ -0,0 +1,70 @@ +package cn.aofeng.demo.java.rmi; + +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; + +import org.apache.commons.lang.StringUtils; + +/** + * 用户信息服务。 + * + * @author 聂勇 + */ +public class UserServiceImpl extends UnicastRemoteObject implements UserService { + + public UserServiceImpl() throws RemoteException { + super(); + } + + private static final long serialVersionUID = -9134952963637302483L; + + @Override + public User findByName(String name) throws RemoteException { + if (StringUtils.isBlank(name)) { + return null; + } + + if ("小明".equals(name)) { + return createUser("10000", "小明", Gender.MALE); + } + + return null; + } + + @Override + public User findById(String id) throws RemoteException { + if (StringUtils.isBlank(id)) { + return null; + } + + if ("10000".equals(id)) { + return createUser("10000", "小丽", Gender.FEMALE); + } + + return null; + } + + @Override + public boolean add(User user) throws RemoteException { + if (null == user || StringUtils.isBlank(user.getId()) || StringUtils.isBlank(user.getName())) { + return false; + } + + return true; + } + + private User createUser(String id, String name, char gender) { + User user = new User(); + user.setId(id); + user.setName(name); + user.setGender(gender); + user.setBirthday(System.currentTimeMillis()); + user.setCountry("中国"); + user.setProvince("广东"); + user.setCity("广州"); + user.setAddress("xxx区xxx街道xxx号"); + + return user; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/util/concurret/ScheduledExecutorServiceDemo.java b/Java基础教程/Java源代码/codedemo/java/util/concurret/ScheduledExecutorServiceDemo.java new file mode 100644 index 00000000..fdacb077 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/concurret/ScheduledExecutorServiceDemo.java @@ -0,0 +1,78 @@ +package cn.aofeng.demo.java.util.concurret; + +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import cn.aofeng.demo.util.DateUtil; + +/** + * {@link ScheduledExecutorService}的使用示例:
+ * 定时任务1:执行过程中会抛出异常。
+ * 定时任务2:执行过程中不会抛出异常。
+ *
+ * 目的:检测java.util.concurrent.ScheduledExecutorService在执行定时任务的过程中,任务内抛出异常没有捕捉时在下一次执行时间到来时,是否可以正常执行。
+ * 测试的JDK版本:1.6.xx。
+ * 结果:通过,相比java.util.Timer,完善地解决了定时任务抛异常的问题。 + * + * @author 聂勇 + */ +public class ScheduledExecutorServiceDemo { + + public static void main(String[] args) { + ScheduledExecutorService timer = Executors.newScheduledThreadPool(2); + long delay = computeDelay(); + timer.schedule(new ThrowExceptionTask(timer), delay, TimeUnit.MILLISECONDS); + timer.schedule(new NotThrowExceptionTask(timer), delay, TimeUnit.MILLISECONDS); + System.out.println("主线程的功能执行完毕"); + } + + private static long computeDelay() { + Date now = new Date(); + Date nextMinute = DateUtil.getNextMinute(); + long delay = nextMinute.getTime() - now.getTime(); + return delay; + } + + static class ThrowExceptionTask implements Runnable { + + private ScheduledExecutorService _timer; + + public ThrowExceptionTask(ScheduledExecutorService timer) { + this._timer = timer; + } + + @Override + public void run() { + try { + System.out.println("任务名:ThrowExceptionTask, 当前时间:"+DateUtil.getCurrentTime()); + + throw new IllegalArgumentException(); + } finally { + _timer.schedule(new ThrowExceptionTask(_timer), computeDelay(), TimeUnit.MILLISECONDS); + } + } + + } // end of ThrowExceptionTask + + static class NotThrowExceptionTask implements Runnable { + + private ScheduledExecutorService _timer; + + public NotThrowExceptionTask(ScheduledExecutorService timer) { + this._timer = timer; + } + + @Override + public void run() { + try { + System.out.println("任务名:NotThrowExceptionTask, 当前时间:"+DateUtil.getCurrentTime()); + } finally { + _timer.schedule(new NotThrowExceptionTask(_timer), computeDelay(), TimeUnit.MILLISECONDS); + } + } + + } // end of NotThrowExceptionTask + +} diff --git a/Java基础教程/Java源代码/codedemo/java/util/forkjoin/HelloForkJoin.java b/Java基础教程/Java源代码/codedemo/java/util/forkjoin/HelloForkJoin.java new file mode 100644 index 00000000..a410da18 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/forkjoin/HelloForkJoin.java @@ -0,0 +1,103 @@ +package cn.aofeng.demo.java.util.forkjoin; + +import java.util.Arrays; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.RecursiveTask; +import java.util.concurrent.TimeUnit; + +/** + * Fork/Join练习。 + * + * @author 聂勇 + */ +public class HelloForkJoin extends RecursiveTask { + + private static final long serialVersionUID = -2386438994963147457L; + + private int[] _intArray; + + private int _threshold = 10; + + private long _result = 0; + + public HelloForkJoin(int[] intArray) { + this._intArray = intArray; + } + + @Override + protected Long compute() { + if (null == _intArray || _intArray.length <= 0) { + return 0L; + } + + if (_intArray.length <= _threshold) { // 如果数组长度小于等于指定的值,执行累加操作 + for (int item : _intArray) { + _result += item; + } + System.out.println( String.format("线程:%s,累加数组%s中的值,结果:%d", Thread.currentThread(), Arrays.toString(_intArray), _result) ); + } else { // 如果数组长度大于指定的值,做任务分解 + int[] temp = new int[_threshold]; + System.arraycopy(_intArray, 0, temp, 0, _threshold); + HelloForkJoin subTask1 = new HelloForkJoin(temp); + subTask1.fork(); + + if (_intArray.length - _threshold > 0) { + int remain[] = new int[_intArray.length - _threshold]; + System.arraycopy(_intArray, _threshold, remain, 0, remain.length); + HelloForkJoin task = new HelloForkJoin(remain); + task.fork(); + _result += task.join(); + } + + _result += subTask1.join(); + + return _result; + } + + return _result; + } + + public static void main(String[] args) throws InterruptedException { + int count = 10000; + + serialCompute(count); + parallelCompute(count); + } + + /** + * 使用fork/join并行计算数字累加。 + * + * @param count 数字个数(从1开始) + * @throws InterruptedException + */ + private static void parallelCompute(int count) + throws InterruptedException { + int[] numbers = new int[count]; + for (int i = 0; i < count; i++) { + numbers[i] = i+1; + } + ForkJoinPool pool = new ForkJoinPool(4); + HelloForkJoin task = new HelloForkJoin(numbers); + long startTime = System.currentTimeMillis(); + pool.submit(task); + pool.shutdown(); + pool.awaitTermination(10, TimeUnit.SECONDS); + + System.out.println( String.format("并行计算结果:%d,耗时:%d毫秒", task._result, System.currentTimeMillis()-startTime) ); + } + + /** + * 使用for循环串行计算数字累加。 + * + * @param count 数字个数(从1开始) + */ + private static void serialCompute(int count) { + long startTime = System.currentTimeMillis(); + int sum = 0; + for (int i = 0; i < count; i++) { + sum += (i+1); + } + System.out.println( String.format("串行计算结果:%d,耗时:%d毫秒", sum, System.currentTimeMillis()-startTime) ); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/util/future/Future.ucls b/Java基础教程/Java源代码/codedemo/java/util/future/Future.ucls new file mode 100644 index 00000000..c49836d6 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/future/Future.ucls @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/java/util/future/HelloFuture.java b/Java基础教程/Java源代码/codedemo/java/util/future/HelloFuture.java new file mode 100644 index 00000000..e92baba6 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/future/HelloFuture.java @@ -0,0 +1,106 @@ +package cn.aofeng.demo.java.util.future; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import org.apache.log4j.Logger; + +/** + * Future、Callable练习。 + * + * @author 聂勇 + */ +public class HelloFuture { + + private static Logger _logger = Logger.getLogger(HelloFuture.class); + + /** + * @param args + * @throws InterruptedException + */ + public static void main(String[] args) throws InterruptedException { + executeSingleTask(); + executeBatchTask(); + } + + /** + * 执行单个异步任务:
+ * 1、用submit方法向线程池提交一个任务。
+ * 2、获取结果时,指定了超时时间。

+ * 结果:超时后,调用者收到TimeoutException,但实际上任务还在继续执行。 + * @throws InterruptedException + */ + private static void executeSingleTask() throws InterruptedException { + ExecutorService threadpool = Executors.newFixedThreadPool(6); + try { + Future f = threadpool.submit(createCallable(5 * 1000)); + Object result = f.get(3, TimeUnit.SECONDS); + System.out.println("单个任务的执行结果:"+result); + } catch (Exception e) { + _logger.error("线程执行任务时出错", e); + } finally { + threadpool.shutdown(); + threadpool.awaitTermination(10, TimeUnit.SECONDS); + } + } + + /** + * 执行多个异步任务:
+ * 1、用invokeAll方法向线程池提交多个任务,并指定了执行的超时时间。

+ * 结果:超时后,未执行完成的任务被取消,在调用Future的get方法时,取消的任务会抛出CancellationException,执行完成的任务可获得结果。 + * @throws InterruptedException + */ + private static void executeBatchTask() throws InterruptedException { + ExecutorService threadpool = Executors.newFixedThreadPool(6); + List> tasks = new ArrayList>(); + tasks.add(createCallable(2000)); + tasks.add(createCallable(5000)); + tasks.add(createCallable(2500)); + + long startTime = System.currentTimeMillis(); + try { + List> fs = threadpool.invokeAll(tasks, 3, TimeUnit.SECONDS); + int result = 0; + for (Future f : fs) { + try{ + result += f.get(); + } catch(CancellationException ce) { + // nothing + } + } + System.out.println("执行三个任务共耗时:" + (System.currentTimeMillis() - startTime) + "毫秒"); + System.out.println("三个任务的执行结果汇总:"+result); + } catch (Exception e) { + _logger.error("线程执行任务时出错", e); + } finally { + threadpool.shutdown(); + threadpool.awaitTermination(10, TimeUnit.SECONDS); + } + } + + /** + * 创建需要长时间执行的任务模拟对象。 + * @param sleepTimes 线程休眠时间(单位:毫秒) + * @return {@link Callable}对象 + */ + private static Callable createCallable(final int sleepTimes) { + Callable c = new Callable() { + + @Override + public Integer call() throws Exception { + Thread.sleep(sleepTimes); + System.out.println(Thread.currentThread().getName() + ": I'm working"); + return 9; + } + }; + + return c; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/java/util/map/Map接口及其实现类.ucls b/Java基础教程/Java源代码/codedemo/java/util/map/Map接口及其实现类.ucls new file mode 100644 index 00000000..a9f38aed --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/map/Map接口及其实现类.ucls @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/java/util/timer/TimerDemo.java b/Java基础教程/Java源代码/codedemo/java/util/timer/TimerDemo.java new file mode 100644 index 00000000..7341764d --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/java/util/timer/TimerDemo.java @@ -0,0 +1,47 @@ +package cn.aofeng.demo.java.util.timer; + +import java.util.Timer; +import java.util.TimerTask; + +import cn.aofeng.demo.util.DateUtil; + +/** + * {@link Timer}的使用示例:
+ * 定时任务1:执行过程中会抛出异常。
+ * 定时任务2:执行过程中不会抛出异常。
+ *
+ * 目的:检测java.util.Timer在执行定时任务的过程中,任务内抛出异常没有捕捉时在下一次执行时间到来时,是否可以正常执行。
+ * 测试的JDK版本:1.6.xx。
+ * 结果:不通过,定时任务抛出异常时,整个Timer中止,其他定时任务也中止。 + * + * @author 聂勇 + */ +public class TimerDemo { + + public static void main(String[] args) { + Timer timer = new Timer("aofeng-timer-demo"); + timer.schedule(new ThrowExceptionTask(), DateUtil.getNextMinute(), 60*1000); + timer.schedule(new NotThrowExceptionTask(), DateUtil.getNextMinute(), 60*1000); + } + + static class ThrowExceptionTask extends TimerTask { + + @Override + public void run() { + System.out.println("任务名:ThrowExceptionTask, 当前时间:"+DateUtil.getCurrentTime()); + + throw new IllegalArgumentException(); + } + + } // end of ThrowExceptionTask + + static class NotThrowExceptionTask extends TimerTask { + + @Override + public void run() { + System.out.println("任务名:NotThrowExceptionTask, 当前时间:"+DateUtil.getCurrentTime()); + } + + } // end of NotThrowExceptionTask + +} diff --git a/Java基础教程/Java源代码/codedemo/jdbc/JDBCUtils.java b/Java基础教程/Java源代码/codedemo/jdbc/JDBCUtils.java new file mode 100644 index 00000000..57741f56 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jdbc/JDBCUtils.java @@ -0,0 +1,64 @@ +package cn.aofeng.demo.jdbc; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JDBC常用方法集。 + * + * @author 聂勇 + */ +public class JDBCUtils { + + private static Logger _logger = LoggerFactory.getLogger(JDBCUtils.class); + + public static void close(ResultSet rs) { + if (null != rs) { + try { + rs.close(); + } catch (SQLException e) { + _logger.error("close resultset occurs error", e); + } + } + } + + public static void close(Statement stmt) { + if (null != stmt) { + try { + stmt.close(); + } catch (SQLException e) { + _logger.error("close statement occurs error", e); + } + } + } + + public static void close(Connection conn) { + if (null != conn) { + try { + conn.close(); + } catch (SQLException e) { + _logger.error("close connection occurs error", e); + } + } + } + + public static void showResultSetInfo(ResultSet rs) throws SQLException { + ResultSetMetaData rsMeta = rs.getMetaData(); + int colCount = rsMeta.getColumnCount(); + _logger.info("total columns:{}", colCount); + for (int i = 1; i <= colCount; i++) { + _logger.info("column name:{}, label:{}, type:{}, typeName:{}", + rsMeta.getColumnName(i), + rsMeta.getColumnLabel(i), + rsMeta.getColumnType(i), + rsMeta.getColumnTypeName(i)); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/jdbc/MetaDataExample.java b/Java基础教程/Java源代码/codedemo/jdbc/MetaDataExample.java new file mode 100644 index 00000000..f631135a --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jdbc/MetaDataExample.java @@ -0,0 +1,82 @@ +package cn.aofeng.demo.jdbc; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Properties; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import cn.aofeng.demo.tree.PrettyTree; +import cn.aofeng.demo.tree.PrettyTree.Node; + +/** + * JDBC元数据使用示例。 + * + * @author 聂勇 + */ +public class MetaDataExample { + + private static Logger _logger = LoggerFactory.getLogger(MetaDataExample.class); + + /** + * @param args + */ + public static void main(String[] args) { + String url = "jdbc:mysql://192.168.56.102:19816/ucgc_sdk?useUnicode=true&characterEncoding=UTF8"; + Properties info = new Properties(); + info.put("user", "uzone"); + info.put("password", "uzone"); + + Connection conn = null; + try { + conn = DriverManager.getConnection(url, info); + DatabaseMetaData dbMeta = conn.getMetaData(); + showCatalogs(dbMeta); + } catch (SQLException e) { + _logger.error("get connection occurs error", e); + } finally { + JDBCUtils.close(conn); + } + } + + private static void showCatalogs(DatabaseMetaData dbMeta) throws SQLException { + ResultSet catalogsRs = null; + try { + catalogsRs = dbMeta.getCatalogs(); +// JDBCUtils.showResultSetInfo(catalogsRs); + while (catalogsRs.next()) { + String catalog = catalogsRs.getString("TABLE_CAT"); + showTables(dbMeta, catalog); + } + } finally { + JDBCUtils.close(catalogsRs); + } + } + + /** + * 采用树状结构输出Catalog和所有归属于它的表的名称。 + */ + private static void showTables(DatabaseMetaData dbMeta, String catalog) throws SQLException { + ResultSet tablesRs = null; + Node root = new Node(catalog); + try { + tablesRs = dbMeta.getTables(catalog, null, null, null); +// JDBCUtils.showResultSetInfo(tablesRs); + while (tablesRs.next()) { + root.add(new Node(tablesRs.getString("TABLE_NAME"))); + } + } finally { + JDBCUtils.close(tablesRs); + } + + StringBuilder buffer = new StringBuilder(256); + PrettyTree pt = new PrettyTree(); + pt.renderRoot(root, buffer); + System.out.print(buffer); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/jetty/HttpGet.java b/Java基础教程/Java源代码/codedemo/jetty/HttpGet.java new file mode 100644 index 00000000..00ad49d0 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jetty/HttpGet.java @@ -0,0 +1,42 @@ +package cn.aofeng.demo.jetty; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.commons.io.IOUtils; + +/** + * 抓取页面内容。 + * + * @author 聂勇 + */ +public class HttpGet { + + public String getSomeThing(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + urlConn.setConnectTimeout(3000); + urlConn.setRequestMethod("GET"); + urlConn.connect(); + + InputStream ins = null; + try { + if (200 == urlConn.getResponseCode()) { + ins = urlConn.getInputStream(); + ByteArrayOutputStream outs = new ByteArrayOutputStream(1024); + IOUtils.copy(ins, outs); + return outs.toString("UTF-8"); + } + } catch (IOException e) { + throw e; + } finally { + IOUtils.closeQuietly(ins); + } + + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/jetty/HttpGetTest.java b/Java基础教程/Java源代码/codedemo/jetty/HttpGetTest.java new file mode 100644 index 00000000..94d954d1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jetty/HttpGetTest.java @@ -0,0 +1,55 @@ +package cn.aofeng.demo.jetty; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * {@link HttpGet}的单元测试用例。 + * + * @author 聂勇 + */ +public class HttpGetTest { + + private HttpServerMock _mock; + private HttpGet _httpGet = new HttpGet(); + + @Before + public void setUp() throws Exception { + _mock = new HttpServerMock(); + } + + @After + public void tearDown() throws Exception { + if (null != _mock) { + _mock.stop(); + } + } + + /** + * 用例:响应状态码为200且有响应内容。 + */ + @Test + public void testGetSomeThing4Success() throws Exception { + String response = "Hello, The World!"; + _mock.start(response, "text/plain"); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertEquals(response, content); + } + + /** + * 用例:响应状态码为非200。 + */ + @Test + public void testGetSomeThing4Fail() throws Exception { + String response = "Hello, The World!"; + _mock.start(response, "text/plain", 500); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertNull(content); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/jetty/HttpServerMock.java b/Java基础教程/Java源代码/codedemo/jetty/HttpServerMock.java new file mode 100644 index 00000000..594ba0b1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jetty/HttpServerMock.java @@ -0,0 +1,90 @@ +package cn.aofeng.demo.jetty; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * HTTP服务器MOCK,可用于单元测试时模拟HTTP服务器的响应。 + * + * @author 聂勇 + */ +public class HttpServerMock { + + public final static int DEFAULT_PORT = 9191; + public final static String DEFAULT_CONTENT_TYPE = "application/json"; + public final static int DEFAULT_STATUS_CODE=HttpServletResponse.SC_OK; + + private Server _httpServer; + private int _port; + + public HttpServerMock() { + _port = DEFAULT_PORT; + } + + public HttpServerMock(int port) { + _port = port; + } + + /** + * 启动Jetty服务器。默认的响应status code为"200",content type为"application/json"。 + * @param content 响应内容 + */ + public void start(String content) throws Exception { + start(content, DEFAULT_CONTENT_TYPE, DEFAULT_STATUS_CODE); + } + + /** + * 启动Jetty服务器。默认的响应status code为"200"。 + * @param content 响应内容 + * @param contentType 响应内容的MIME类型 + */ + public void start(String content, String contentType) throws Exception { + start(content, contentType, DEFAULT_STATUS_CODE); + } + + /** + * 启动Jetty服务器。 + * @param content 响应内容 + * @param contentType 响应内容的MIME类型 + * @param statuCode 响应状态码 + */ + public void start(String content, String contentType, + int statuCode) throws Exception { + _httpServer = new Server(_port); + _httpServer.setHandler(createHandler(content, contentType, statuCode)); + _httpServer.start(); + } + + /** + * 停止Jetty服务器。 + */ + public void stop() throws Exception { + if (null != _httpServer) { + _httpServer.stop(); + _httpServer = null; + } + } + + private Handler createHandler(final String content, final String contentType, + final int statusCode) { + return new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + response.setContentType(contentType); + response.setStatus(statusCode); + baseRequest.setHandled(true); + response.getWriter().print(content); + } + }; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/jetty/README.md b/Java基础教程/Java源代码/codedemo/jetty/README.md new file mode 100644 index 00000000..242d8d4b --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/jetty/README.md @@ -0,0 +1,216 @@ +使用Jetty实现Http Server Mock作单元测试 +=== +在研发过程中发现许多模块对外部系统的调用并没有沉淀成专门的客户端,代码中直接调用URL发送请求和获取响应内容,导致做单元测试的时候非常麻烦。 +特别是对已有系统做改造的时候,要先写单元测试用例,保证后续的重构和修改不偏离原来的业务需求。 + +这样问题就来了:该如何模拟外部系统返回各种正常和异常的响应内容呢? +办法总是有的,这里就用Jetty实现Http Server Mock,模拟服务端返回各种响应数据。 + +预备 +--- +* [JUnit 4.11](http://junit.org/) +* [Jetty 7.6.9](http://www.eclipse.org/jetty/) + +业务示例代码 +--- +[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/jetty/HttpGet.java) +```java +package cn.aofeng.demo.jetty; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.commons.io.IOUtils; + +/** + * 抓取页面内容。 + * + * @author 聂勇 + */ +public class HttpGet { + + public String getSomeThing(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + urlConn.setConnectTimeout(3000); + urlConn.setRequestMethod("GET"); + urlConn.connect(); + + InputStream ins = null; + try { + if (200 == urlConn.getResponseCode()) { + ins = urlConn.getInputStream(); + ByteArrayOutputStream outs = new ByteArrayOutputStream(1024); + IOUtils.copy(ins, outs); + return outs.toString("UTF-8"); + } + } catch (IOException e) { + throw e; + } finally { + IOUtils.closeQuietly(ins); + } + + return null; + } + +} +``` +用Jetty实现的Http Server Mock +--- +[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/jetty/HttpServerMock.java) +```java +package cn.aofeng.demo.jetty; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.eclipse.jetty.server.Handler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; + +/** + * HTTP服务器MOCK,可用于单元测试时模拟HTTP服务器的响应。 + * + * @author 聂勇 + */ +public class HttpServerMock { + + public final static int DEFAULT_PORT = 9191; + public final static String DEFAULT_CONTENT_TYPE = "application/json"; + public final static int DEFAULT_STATUS_CODE=HttpServletResponse.SC_OK; + + private Server _httpServer; + private int _port; + + public HttpServerMock() { + _port = DEFAULT_PORT; + } + + public HttpServerMock(int port) { + _port = port; + } + + /** + * 启动Jetty服务器。默认的响应status code为"200",content type为"application/json"。 + * @param content 响应内容 + */ + public void start(String content) throws Exception { + start(content, DEFAULT_CONTENT_TYPE, DEFAULT_STATUS_CODE); + } + + /** + * 启动Jetty服务器。默认的响应status code为"200"。 + * @param content 响应内容 + * @param contentType 响应内容的MIME类型 + */ + public void start(String content, String contentType) throws Exception { + start(content, contentType, DEFAULT_STATUS_CODE); + } + + /** + * 启动Jetty服务器。 + * @param content 响应内容 + * @param contentType 响应内容的MIME类型 + * @param statuCode 响应状态码 + */ + public void start(String content, String contentType, + int statuCode) throws Exception { + _httpServer = new Server(_port); + _httpServer.setHandler(createHandler(content, contentType, statuCode)); + _httpServer.start(); + } + + /** + * 停止Jetty服务器。 + */ + public void stop() throws Exception { + if (null != _httpServer) { + _httpServer.stop(); + _httpServer = null; + } + } + + private Handler createHandler(final String content, final String contentType, + final int statusCode) { + return new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + response.setContentType(contentType); + response.setStatus(statusCode); + baseRequest.setHandled(true); + response.getWriter().print(content); + } + }; + } + +} +``` +单元测试代码 +--- +[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/jetty/HttpGetTest.java) +```java +package cn.aofeng.demo.jetty; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * {@link HttpGet}的单元测试用例。 + * + * @author 聂勇 + */ +public class HttpGetTest { + + private HttpServerMock _mock; + private HttpGet _httpGet = new HttpGet(); + + @Before + public void setUp() throws Exception { + _mock = new HttpServerMock(); + } + + @After + public void tearDown() throws Exception { + if (null != _mock) { + _mock.stop(); + } + } + + /** + * 用例:响应状态码为200且有响应内容。 + */ + @Test + public void testGetSomeThing4Success() throws Exception { + String response = "Hello, The World!"; + _mock.start(response, "text/plain"); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertEquals(response, content); + } + + /** + * 用例:响应状态码为非200。 + */ + @Test + public void testGetSomeThing4Fail() throws Exception { + String response = "Hello, The World!"; + _mock.start(response, "text/plain", 500); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertNull(content); + } + +} +``` + diff --git a/Java基础教程/Java源代码/codedemo/json/gson/ArrayDeserialize.java b/Java基础教程/Java源代码/codedemo/json/gson/ArrayDeserialize.java new file mode 100644 index 00000000..9dd5dd07 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/ArrayDeserialize.java @@ -0,0 +1,45 @@ +package cn.aofeng.demo.json.gson; + +import com.google.gson.Gson; + +/** + * 数组的反序列化。 + * + * @author 聂勇 + */ +public class ArrayDeserialize { + + public T deserialize(String json, Class claz) { + Gson gson = new Gson(); + return gson.fromJson(json, claz); + } + + public static void main(String[] args) { + ArrayDeserialize ad = new ArrayDeserialize(); + + // 整型数组 + String intArrJson = "[9,7,5]"; + int[] intArr = ad.deserialize(intArrJson, int[].class); + System.out.println("---------- 整型数组 ----------"); + for (int i : intArr) { + System.out.println(i); + } + + // 字符串数组 + String strArrJson = "[\"张三\",\"李四\",\"王五\"]"; + String[] strArr = ad.deserialize(strArrJson, String[].class); + System.out.println("---------- 字符串数组 ----------"); + for (String str : strArr) { + System.out.println(str); + } + + // 对象数组 + String objArrJson = "[{\"name\":\"小明\",\"age\":10},{\"name\":\"马丽\",\"age\":9}]"; + Person[] objArr = ad.deserialize(objArrJson, Person[].class); + System.out.println("---------- 对象数组 ----------"); + for (Person person : objArr) { + System.out.println(person); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/ArraySerialize.java b/Java基础教程/Java源代码/codedemo/json/gson/ArraySerialize.java new file mode 100644 index 00000000..3b9523c2 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/ArraySerialize.java @@ -0,0 +1,41 @@ +package cn.aofeng.demo.json.gson; + +import com.google.gson.Gson; + +/** + * 数组的序列化。 + * + * @author 聂勇 + */ +public class ArraySerialize { + + public void serialize(Object[] arr) { + Gson gson = new Gson(); + System.out.println( gson.toJson(arr) ); + } + + public static void main(String[] args) { + ArraySerialize as = new ArraySerialize(); + + // 整型对象数组 + Integer[] intArr = new Integer[3]; + intArr[0] = 9; + intArr[1] = 7; + intArr[2] = 5; + as.serialize(intArr); + + // 字符串数组 + String[] names = new String[3]; + names[0] = "张三"; + names[1] = "李四"; + names[2] = "王五"; + as.serialize(names); + + // 对象数组 + Person[] persons = new Person[2]; + persons[0] = new Person("小明", 10); + persons[1] = new Person("马丽", 9); + as.serialize(persons); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/CollectionDeserialize.java b/Java基础教程/Java源代码/codedemo/json/gson/CollectionDeserialize.java new file mode 100644 index 00000000..0b5b5fa6 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/CollectionDeserialize.java @@ -0,0 +1,55 @@ +package cn.aofeng.demo.json.gson; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +/** + * 集合的反序列化。 + * + * @author 聂勇 + */ +public class CollectionDeserialize { + + public T deserialize(String json, Type type) { + Gson gson = new Gson(); + return gson.fromJson(json, type); + } + + public static void main(String[] args) { + CollectionDeserialize cd = new CollectionDeserialize(); + + //整型List + String intListJson = "[9,8,0]"; + List intList = cd.deserialize( intListJson, + new TypeToken>(){}.getType() ); + System.out.println("---------- 整型List ----------"); + for (Integer obj : intList) { + System.out.println(obj); + } + + // 字符串Set + String strSetJson = "[\"Best\",\"World\",\"Hello\"]"; + Set strSet = cd.deserialize( strSetJson, + new TypeToken>(){}.getType() ); + System.out.println("---------- 字符串Set ----------"); + for (String str : strSet) { + System.out.println(str); + } + + // Map + String objMapJson = "{\"xiaomin\":{\"name\":\"小明\",\"age\":21},\"marry\":{\"name\":\"马丽\",\"age\":20}}"; + Map objMap = cd.deserialize( objMapJson, + new TypeToken>(){}.getType() ); + System.out.println("---------- Map ----------"); + for (Entry entry : objMap.entrySet()) { + System.out.println(entry); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/CollectionsSerialize.java b/Java基础教程/Java源代码/codedemo/json/gson/CollectionsSerialize.java new file mode 100644 index 00000000..6f505372 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/CollectionsSerialize.java @@ -0,0 +1,54 @@ +package cn.aofeng.demo.json.gson; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.gson.Gson; + +/** + * 集合的序列化。 + * + * @author 聂勇 + */ +public class CollectionsSerialize { + + public void serialize(Collection c) { + Gson gson = new Gson(); + System.out.println( gson.toJson(c) ); + } + + public void serialize(Map map) { + Gson gson = new Gson(); + System.out.println( gson.toJson(map) ); + } + + public static void main(String[] args) { + CollectionsSerialize cs = new CollectionsSerialize(); + + // 整型List + List intList = new ArrayList(); + intList.add(9); + intList.add(8); + intList.add(0); + cs.serialize(intList); + + // 字符串Set + Set strSet = new HashSet(); + strSet.add("Hello"); + strSet.add("World"); + strSet.add("Best"); + cs.serialize(strSet); + + // Map + Map objMap = new HashMap(); + objMap.put("marry", new Person("马丽", 20)); + objMap.put("xiaomin", new Person("小明", 21)); + cs.serialize(objMap); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/CustomDeserialize.java b/Java基础教程/Java源代码/codedemo/json/gson/CustomDeserialize.java new file mode 100644 index 00000000..8321f52f --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/CustomDeserialize.java @@ -0,0 +1,46 @@ +package cn.aofeng.demo.json.gson; + +import java.lang.reflect.Type; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * 自定义反序列化。 + * + * @author 聂勇 + */ +public class CustomDeserialize { + + public static void main(String[] args) { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Person.class, new PersonDeserializer()); + Gson gson = builder.create(); + + String json = "{\"PersonName\":\"aofeng\",\"PersonAge\":32}"; + Person obj = gson.fromJson(json, Person.class); + System.out.println(obj); // 输出结果:Person [name=aofeng, age=32] + } + + public static class PersonDeserializer implements JsonDeserializer { + + @Override + public Person deserialize(JsonElement jsonEle, Type type, + JsonDeserializationContext context) + throws JsonParseException { + JsonObject jo = jsonEle.getAsJsonObject(); + String name = jo.get("PersonName").getAsString(); + int age = jo.get("PersonAge").getAsInt(); + + Person obj = new Person(name, age); + return obj; + } + + } // end of PersonDeserializer + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/CustomSerialize.java b/Java基础教程/Java源代码/codedemo/json/gson/CustomSerialize.java new file mode 100644 index 00000000..4b41576d --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/CustomSerialize.java @@ -0,0 +1,42 @@ +package cn.aofeng.demo.json.gson; + +import java.lang.reflect.Type; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * 自定义序列化。 + * + * @author 聂勇 + */ +public class CustomSerialize { + + public static void main(String[] args) { + GsonBuilder builder = new GsonBuilder(); + builder.registerTypeAdapter(Person.class, new PersonSerializer()); + Gson gson = builder.create(); + + Person obj = new Person("aofeng", 32); + System.out.println( gson.toJson(obj) ); // 输出结果:{"PersonName":"aofeng","PersonAge":32} + } + + public static class PersonSerializer implements JsonSerializer { + + @Override + public JsonElement serialize(Person obj, Type type, + JsonSerializationContext context) { + JsonObject jo = new JsonObject(); + jo.addProperty("PersonName", obj.getName()); + jo.addProperty("PersonAge", obj.getAge()); + + return jo; + } + + } // end of PersonSerializer + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/Person.java b/Java基础教程/Java源代码/codedemo/json/gson/Person.java new file mode 100644 index 00000000..445adc7c --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/Person.java @@ -0,0 +1,50 @@ +package cn.aofeng.demo.json.gson; + +/** + * 简单的Java对象。 + * + * @author 聂勇 + */ +public class Person { + + private String name; + + private int age; + + public Person() { + // nothing + } + + @SuppressWarnings("unused") + private void reset() { + name = null; + age = 0; + } + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @Override + public String toString() { + return "Person [name=" + name + ", age=" + age + "]"; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/json/gson/SimpleObjectSerialize.java b/Java基础教程/Java源代码/codedemo/json/gson/SimpleObjectSerialize.java new file mode 100644 index 00000000..734692dd --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/json/gson/SimpleObjectSerialize.java @@ -0,0 +1,39 @@ +package cn.aofeng.demo.json.gson; + +import com.google.gson.Gson; + +/** + * Java简单对象的序列化与反序列化。 + * + * @author 聂勇 + */ +public class SimpleObjectSerialize { + + /** + * 序列化:将Java对象转换成JSON字符串。 + */ + public void serialize(Person person) { + Gson gson = new Gson(); + System.out.println( gson.toJson(person) ); + } + + /** + * 反序列化:将JSON字符串转换成Java对象。 + */ + public void deserialize(String json) { + Gson gson = new Gson(); + Person person = gson.fromJson(json, Person.class); + System.out.println( person ); + } + + public static void main(String[] args) { + SimpleObjectSerialize ss = new SimpleObjectSerialize(); + + Person person = new Person("NieYong", 33); + ss.serialize(person); + + String json = " {\"name\":\"AoFeng\",\"age\":32}"; + ss.deserialize(json); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/misc/GetHostInfo.java b/Java基础教程/Java源代码/codedemo/misc/GetHostInfo.java new file mode 100644 index 00000000..8c1aadd0 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/misc/GetHostInfo.java @@ -0,0 +1,63 @@ +package cn.aofeng.demo.misc; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.Enumeration; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; + +/** + * 获取本机IP和主机名以及Java环境信息。 + * + * @author 聂勇 + */ +public class GetHostInfo { + + /** + * @param args + * @throws UnknownHostException + * @throws SocketException + */ + public static void main(String[] args) throws UnknownHostException, SocketException { + InetAddress address = InetAddress.getLocalHost(); + System.out.println("计算机名:" + address.getHostName()); + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + while (nis.hasMoreElements()) { + StringBuilder buffer = new StringBuilder(); + NetworkInterface ni = nis.nextElement(); + buffer.append("网卡:").append(ni.getName()); + buffer.append(" 绑定IP:"); + Enumeration ias = ni.getInetAddresses(); + int count = 0; + while (ias.hasMoreElements()) { + InetAddress ia = ias.nextElement(); + if (count > 0) { + buffer.append(", "); + } + buffer.append(ia.getHostAddress()); + } + System.out.println(buffer.toString()); + } + + System.out.println("Java环境信息:"); + System.out.println("---------------------------------------------------:"); + Properties pros = System.getProperties(); + Set> javaEnums = pros.entrySet(); + for (Entry entry : javaEnums) { + System.out.println(entry.getKey() + " : " + entry.getValue()); + } + System.out.println(""); + System.out.println("系统环境信息:"); + System.out.println("---------------------------------------------------:"); + Map envs = System.getenv(); + Set> envSet = envs.entrySet(); + for (Entry entry : envSet) { + System.out.println(entry.getKey() + " : " + entry.getValue()); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/Commodity.java b/Java基础教程/Java源代码/codedemo/mockito/Commodity.java new file mode 100644 index 00000000..32d52a92 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/Commodity.java @@ -0,0 +1,50 @@ +package cn.aofeng.demo.mockito; + +/** + * 商品信息。 + * + * @author 聂勇 + */ +public class Commodity { + + private String id; + + private String name; + + private int type; + + public Commodity() { + // nothing + } + + public Commodity(String id, String name, int type) { + this.id = id; + this.name = name; + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/CommodityDao.java b/Java基础教程/Java源代码/codedemo/mockito/CommodityDao.java new file mode 100644 index 00000000..b6ad73d3 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/CommodityDao.java @@ -0,0 +1,20 @@ +package cn.aofeng.demo.mockito; + +import java.util.List; + +/** + * 商品存储层。 + * + * @author 聂勇 + */ +public class CommodityDao { + + public Commodity queryById(String id) { + return null; + } + + public List queryByType(int type) { + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/User.java b/Java基础教程/Java源代码/codedemo/mockito/User.java new file mode 100644 index 00000000..baade9fb --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/User.java @@ -0,0 +1,70 @@ +package cn.aofeng.demo.mockito; + +/** + * 用户信息。 + * + * @author 聂勇 + */ +public class User { + + private long id; + + private String name; + + private int sex; + + private int age; + + private String address; + + public User() { + // nothing + } + + public User(long userId, String userName, int age) { + this.id = userId; + this.name = userName; + this.age = age; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSex() { + return sex; + } + + public void setSex(int sex) { + this.sex = sex; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/UserDao.java b/Java基础教程/Java源代码/codedemo/mockito/UserDao.java new file mode 100644 index 00000000..a7861670 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/UserDao.java @@ -0,0 +1,19 @@ +package cn.aofeng.demo.mockito; + +import java.util.List; + +/** + * 用户信息存储层。 + * + * @author 聂勇 + */ +public class UserDao { + + public User queryById(long userId) { + return null; + } + + public List queryByName(String userName) { + return null; + } +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/UserService.java b/Java基础教程/Java源代码/codedemo/mockito/UserService.java new file mode 100644 index 00000000..c6ee66ba --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/UserService.java @@ -0,0 +1,54 @@ +package cn.aofeng.demo.mockito; + +/** + * 用户服务。 + * + * @author 聂勇 + */ +public class UserService { + + /** 成人年龄分界线 */ + private final static int ADULT_AGE = 18; + + private UserDao _userDao; + private CommodityDao _commodityDao; + + public boolean isAdult(long userId) { + User user = _userDao.queryById(userId); + if (null == user || user.getAge() < ADULT_AGE) { + return false; + } + + return true; + } + + public boolean buy(long userId, String commodityId) { + if (! isAdult(userId)) { + return false; + } + + Commodity commodity = _commodityDao.queryById(commodityId); + if (null == commodity) { + return false; + } + // 省略余下的处理逻辑 + return true; + } + + public UserDao getUserDao() { + return _userDao; + } + + public void setUserDao(UserDao userDao) { + this._userDao = userDao; + } + + public CommodityDao getCommodityDao() { + return _commodityDao; + } + + public void setCommodityDao(CommodityDao commodityDao) { + this._commodityDao = commodityDao; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mockito/UserServiceTest.java b/Java基础教程/Java源代码/codedemo/mockito/UserServiceTest.java new file mode 100644 index 00000000..fd7009e2 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mockito/UserServiceTest.java @@ -0,0 +1,66 @@ +package cn.aofeng.demo.mockito; + +import static org.junit.Assert.*; + +import org.junit.Test; +import static org.mockito.Mockito.*; + +/** + * {@link UserService}的单元测试用例。 + * + * @author 聂勇 + */ +public class UserServiceTest { + + private UserService _userService = new UserService(); + + @Test + public void testIsAdult4UserExist() { + long userId = 123; + User user = new User(userId, "张三", 19); + + // 大于18岁的测试用例 + UserDao daoMock = mock(UserDao.class); + when(daoMock.queryById(userId)).thenReturn(user); // 设置行为和对应的返回值 + _userService.setUserDao(daoMock); // 设置mock + assertTrue(_userService.isAdult(userId)); // 校验结果 + + // 等于18岁的测试用例 + User user2 = new User(userId, "李四", 18); + when(daoMock.queryById(userId)).thenReturn(user2); + _userService.setUserDao(daoMock); + assertTrue(_userService.isAdult(userId)); + + // 小于18岁的测试用例 + User user3 = new User(userId, "王五", 17); + when(daoMock.queryById(userId)).thenReturn(user3); + _userService.setUserDao(daoMock); + assertFalse(_userService.isAdult(userId)); + } + + @Test + public void testIsAdult4UserNotExist() { + // 用户不存在的测试用例 + long userId = 123; + UserDao daoMock = mock(UserDao.class); + when(daoMock.queryById(userId)).thenReturn(null); // 设置行为和对应的返回值 + _userService.setUserDao(daoMock); // 设置mock + assertFalse(_userService.isAdult(userId)); // 校验结果 + } + + @Test + public void testBuy() { + long userId = 12345; + UserDao daoMock = mock(UserDao.class); + when(daoMock.queryById(anyLong())).thenReturn(new User(userId, "张三", 19)); + + String commodityId = "S01A10009823"; + CommodityDao commodityDao = mock(CommodityDao.class); + when(commodityDao.queryById(anyString())).thenReturn(new Commodity(commodityId, "xxx手机", 1)); + + _userService.setUserDao(daoMock); + _userService.setCommodityDao(commodityDao); + assertTrue(_userService.buy(userId, commodityId)); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mybatis/MyBatisClient.java b/Java基础教程/Java源代码/codedemo/mybatis/MyBatisClient.java new file mode 100644 index 00000000..6b6f3f1f --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mybatis/MyBatisClient.java @@ -0,0 +1,62 @@ +package cn.aofeng.demo.mybatis; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import cn.aofeng.demo.mybatis.dao.MonitNotifyHistoryDao; +import cn.aofeng.demo.mybatis.entity.MonitNotifyHistory; + +/** + * + * + * @author 聂勇 + */ +public class MyBatisClient { + + private static Logger _logger = LoggerFactory.getLogger(MyBatisClient.class); + + public static void main(String[] args) throws IOException { + String resource = "mybatis-config.xml"; + InputStream ins = Resources.getResourceAsStream(resource); + SqlSessionFactory ssFactory = new SqlSessionFactoryBuilder().build(ins); + MonitNotifyHistoryDao dao = new MonitNotifyHistoryDao(ssFactory); + + // 插入一条记录 + long recordId = 999; + MonitNotifyHistory record = new MonitNotifyHistory(); + record.setRecordId(recordId); + record.setMonitId(9999); + record.setAppId(99); + record.setNotifyTarget(1); + record.setNotifyType(1); + record.setNotifyContent("通知内容测试"); + record.setStatus(0); + record.setCreateTime(1492061400000L); + int count = dao.insert(record); + _logger.info("insert complete, effects {} rows", count); + + // 查询记录 + MonitNotifyHistory entity = dao.selectByPrimaryKey(recordId); + _logger.info("query record id {}, result: {}", recordId, entity); + + // 更新一个字段再执行查询 + entity.setRetryTimes(99); + count = dao.updateByPrimaryKeySelective(entity); + _logger.info("update complete, record id:{}, effects {} rows", recordId, count); + entity = dao.selectByPrimaryKey(recordId); + _logger.info("query record id {}, result: {}", recordId, entity); + + // 删除记录后再执行查询 + count = dao.deleteByPrimaryKey(recordId); + _logger.info("delete complete, record id {}, effects {} rows", recordId, count); + entity = dao.selectByPrimaryKey(recordId); + _logger.info("query record id {}, result: {}", recordId, entity); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/mybatis/README.md b/Java基础教程/Java源代码/codedemo/mybatis/README.md new file mode 100644 index 00000000..e214deb3 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mybatis/README.md @@ -0,0 +1,5 @@ +* [MyBatisClien.java](MyBatisClient.java) 入口类。读取配置文件,生成SqlSessionFactory,使用DAO操作表。 +* [mybatis-config.xml](../../../../../conf/mybatis-config.xml) mybatis配置文件 +* [MonitNotifyHistoryDao](dao/MonitNotifyHistoryDao.java) DAO类 +* [MonitNotifyHisto.java](entity/MonitNotifyHistory.java) 实体类 +* [MonitNotifyHistoryMapper.xml](mapper/MonitNotifyHistoryMapper.xml) SQL模板映射配置文件 diff --git a/Java基础教程/Java源代码/codedemo/mybatis/dao/MonitNotifyHistoryDao.java b/Java基础教程/Java源代码/codedemo/mybatis/dao/MonitNotifyHistoryDao.java new file mode 100644 index 00000000..d549e879 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mybatis/dao/MonitNotifyHistoryDao.java @@ -0,0 +1,79 @@ +package cn.aofeng.demo.mybatis.dao; + +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; + +import cn.aofeng.demo.mybatis.entity.MonitNotifyHistory; + +/** + * 监控通知历史CURD。 + * + * @author 聂勇 + */ +public class MonitNotifyHistoryDao { + + private SqlSessionFactory _ssFactory = null; + + public MonitNotifyHistoryDao(SqlSessionFactory factory) { + this._ssFactory = factory; + } + + private String assembleStatement(String id) { + return "cn.aofeng.demo.mybatis.dao.MonitNotifyHistoryMapper."+id; + } + + private void close(SqlSession session) { + if (null != session) { + session.close(); + } + } + + public int deleteByPrimaryKey(Long recordId) { + SqlSession session = _ssFactory.openSession(true); + int result = 0; + try { + session.delete(assembleStatement("deleteByPrimaryKey"), recordId); + } finally { + close(session); + } + + return result; + } + + public int insert(MonitNotifyHistory record) { + SqlSession session = _ssFactory.openSession(true); + int result = 0; + try { + result = session.insert(assembleStatement("insertSelective"), record); + } finally { + close(session); + } + + return result; + } + + public MonitNotifyHistory selectByPrimaryKey(Long recordId) { + SqlSession session = _ssFactory.openSession(); + MonitNotifyHistory result = null; + try { + result = session.selectOne(assembleStatement("selectByPrimaryKey"), recordId); + } finally { + close(session); + } + + return result; + } + + public int updateByPrimaryKeySelective(MonitNotifyHistory record) { + SqlSession session = _ssFactory.openSession(true); + int result = 0; + try { + session.update(assembleStatement("updateByPrimaryKeySelective"), record); + } finally { + close(session); + } + + return result; + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/mybatis/entity/MonitNotifyHistory.java b/Java基础教程/Java源代码/codedemo/mybatis/entity/MonitNotifyHistory.java new file mode 100644 index 00000000..2bae4f51 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mybatis/entity/MonitNotifyHistory.java @@ -0,0 +1,105 @@ +package cn.aofeng.demo.mybatis.entity; + +/** + * 监控通知历史实体类。 + * + * @author 聂勇 + */ +public class MonitNotifyHistory { + private Long recordId; + + private Integer monitId; + + private Integer appId; + + private Integer notifyType; + + private Integer notifyTarget; + + private String notifyContent; + + private Integer status; + + private Integer retryTimes; + + private Long createTime; + + public Long getRecordId() { + return recordId; + } + + public void setRecordId(Long recordId) { + this.recordId = recordId; + } + + public Integer getMonitId() { + return monitId; + } + + public void setMonitId(Integer monitId) { + this.monitId = monitId; + } + + public Integer getAppId() { + return appId; + } + + public void setAppId(Integer appId) { + this.appId = appId; + } + + public Integer getNotifyType() { + return notifyType; + } + + public void setNotifyType(Integer notifyType) { + this.notifyType = notifyType; + } + + public Integer getNotifyTarget() { + return notifyTarget; + } + + public void setNotifyTarget(Integer notifyTarget) { + this.notifyTarget = notifyTarget; + } + + public String getNotifyContent() { + return notifyContent; + } + + public void setNotifyContent(String notifyContent) { + this.notifyContent = notifyContent; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public Integer getRetryTimes() { + return retryTimes; + } + + public void setRetryTimes(Integer retryTimes) { + this.retryTimes = retryTimes; + } + + public Long getCreateTime() { + return createTime; + } + + public void setCreateTime(Long createTime) { + this.createTime = createTime; + } + + @Override + public String toString() { + return "MonitNotifyHistory [recordId=" + recordId + ", monitId=" + monitId + ", appId=" + appId + ", notifyType=" + notifyType + ", notifyTarget=" + + notifyTarget + ", notifyContent=" + notifyContent + ", status=" + status + ", retryTimes=" + retryTimes + ", createTime=" + createTime + "]"; + } + +} \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/mybatis/mapper/MonitNotifyHistoryMapper.xml b/Java基础教程/Java源代码/codedemo/mybatis/mapper/MonitNotifyHistoryMapper.xml new file mode 100644 index 00000000..e6ac6884 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/mybatis/mapper/MonitNotifyHistoryMapper.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + delete from monit_notify_history + where record_id = #{recordId,jdbcType=BIGINT} + + + insert into monit_notify_history (record_id, monit_id, app_id, + notify_type, notify_target, notify_content, + status, retry_times, create_time + ) + values (#{recordId,jdbcType=BIGINT}, #{monitId,jdbcType=INTEGER}, #{appId,jdbcType=TINYINT}, + #{notifyType,jdbcType=TINYINT}, #{notifyTarget,jdbcType=INTEGER}, #{notifyContent,jdbcType=VARCHAR}, + #{status,jdbcType=TINYINT}, #{retryTimes,jdbcType=TINYINT}, #{createTime,jdbcType=BIGINT} + ) + + + insert into monit_notify_history + + + record_id, + + + monit_id, + + + app_id, + + + notify_type, + + + notify_target, + + + notify_content, + + + status, + + + retry_times, + + + create_time, + + + + + #{recordId,jdbcType=BIGINT}, + + + #{monitId,jdbcType=INTEGER}, + + + #{appId,jdbcType=TINYINT}, + + + #{notifyType,jdbcType=TINYINT}, + + + #{notifyTarget,jdbcType=INTEGER}, + + + #{notifyContent,jdbcType=VARCHAR}, + + + #{status,jdbcType=TINYINT}, + + + #{retryTimes,jdbcType=TINYINT}, + + + #{createTime,jdbcType=BIGINT}, + + + + + update monit_notify_history + + + monit_id = #{monitId,jdbcType=INTEGER}, + + + app_id = #{appId,jdbcType=TINYINT}, + + + notify_type = #{notifyType,jdbcType=TINYINT}, + + + notify_target = #{notifyTarget,jdbcType=INTEGER}, + + + notify_content = #{notifyContent,jdbcType=VARCHAR}, + + + status = #{status,jdbcType=TINYINT}, + + + retry_times = #{retryTimes,jdbcType=TINYINT}, + + + create_time = #{createTime,jdbcType=BIGINT}, + + + where record_id = #{recordId,jdbcType=BIGINT} + + + update monit_notify_history + set monit_id = #{monitId,jdbcType=INTEGER}, + app_id = #{appId,jdbcType=TINYINT}, + notify_type = #{notifyType,jdbcType=TINYINT}, + notify_target = #{notifyTarget,jdbcType=INTEGER}, + notify_content = #{notifyContent,jdbcType=VARCHAR}, + status = #{status,jdbcType=TINYINT}, + retry_times = #{retryTimes,jdbcType=TINYINT}, + create_time = #{createTime,jdbcType=BIGINT} + where record_id = #{recordId,jdbcType=BIGINT} + + \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClient.java b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClient.java new file mode 100644 index 00000000..734887e1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClient.java @@ -0,0 +1,55 @@ +package cn.aofeng.demo.netty40x.echo; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; + +import org.apache.commons.lang.math.NumberUtils; +import org.apache.log4j.Logger; + +/** + * Echo客户端。 + * + * @author 聂勇 + */ +public class EchoClient { + + private static Logger _logger = Logger.getLogger(EchoClient.class); + + public void start(String host, int port) { + EventLoopGroup worker = new NioEventLoopGroup(1); + Bootstrap bootstrap = new Bootstrap(); + try { + bootstrap.group(worker) + .channel(NioSocketChannel.class) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1000) + .handler(new EchoClientHandler()) + .remoteAddress(host, port); + ChannelFuture cf = bootstrap.connect().sync(); + cf.channel().closeFuture().sync(); + } catch (Exception e) { + _logger.error("occurs error", e); + } finally { + worker.shutdownGracefully(); + } + } + + /** + * @param args + */ + public static void main(String[] args) { + if (args.length != 2) { + _logger.error("Invalid arguments。Usage:java cn.aofeng.demo.netty40x.echo.EchoClient 192.168.56.102 8080"); + System.exit(-1); + } + + String host = args[0]; + int port = NumberUtils.toInt(args[1], 8080); + EchoClient client = new EchoClient(); + client.start(host, port); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClientHandler.java b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClientHandler.java new file mode 100644 index 00000000..f2700c41 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoClientHandler.java @@ -0,0 +1,49 @@ +package cn.aofeng.demo.netty40x.echo; + +import java.nio.charset.Charset; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.apache.log4j.Logger; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.CharsetUtil; + +/** + * 发送数据给服务端,并接收服务端的数据。 + * + * @author 聂勇 + */ +public class EchoClientHandler extends ChannelInboundHandlerAdapter { + + private static Logger _logger = Logger.getLogger(EchoClientHandler.class); + + private final Charset _utf8 = CharsetUtil.UTF_8; + + public void channelActive(ChannelHandlerContext ctx) { + DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ"); + String currTimeStr = format.format(new Date()); + String data = String.format("Hello, current time is %s", currTimeStr); + ctx.writeAndFlush( Unpooled.copiedBuffer(data, _utf8) ); + _logger.info( String.format("Client send data:%s", data) ); + } + + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf inData = (ByteBuf) msg; + _logger.info( String.format("Client receive data:%s", inData.toString(_utf8)) ); + } + + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.close(); + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + _logger.error("occurs error", cause); + ctx.close(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServer.java b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServer.java new file mode 100644 index 00000000..dec241aa --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServer.java @@ -0,0 +1,58 @@ +package cn.aofeng.demo.netty40x.echo; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +import org.apache.commons.lang.math.NumberUtils; +import org.apache.log4j.Logger; + +/** + * Echo服务端:将客户端发送的请求原样返回。 + * + * @author 聂勇 + */ +public class EchoServer { + + private static Logger _logger = Logger.getLogger(EchoServer.class); + + public void start(int port) { + EventLoopGroup boss = new NioEventLoopGroup(1); + EventLoopGroup worker = new NioEventLoopGroup(10); + ServerBootstrap bootstrap = new ServerBootstrap(); + try { + bootstrap.group(boss, worker) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 128) + .childHandler(new EchoServerHandler()) + .localAddress(port); + ChannelFuture bindFuture = bootstrap.bind().sync(); + ChannelFuture closeFuture = bindFuture.channel().closeFuture().sync(); + _logger.info( String.format("%s started and listen on %s", this.getClass().getSimpleName(), closeFuture ) ); + } catch(Exception ex) { + _logger.info( String.format("%s start failed", this.getClass().getSimpleName()), ex); + } finally { + boss.shutdownGracefully(); + worker.shutdownGracefully(); + } + + } + + /** + * @param args + */ + public static void main(String[] args) { + if (args.length != 1) { + _logger.error("Invalid arguments。Usage:java cn.aofeng.demo.netty40x.echo.EchoServer 8080"); + System.exit(-1); + } + + int port = NumberUtils.toInt(args[0], 8080); + EchoServer server = new EchoServer(); + server.start(port); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServerHandler.java b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServerHandler.java new file mode 100644 index 00000000..4b0f5968 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/netty40x/echo/EchoServerHandler.java @@ -0,0 +1,38 @@ +package cn.aofeng.demo.netty40x.echo; + +import java.nio.charset.Charset; + +import org.apache.log4j.Logger; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelHandler.Sharable; + +/** + * 将接收到的数据原样返回给发送方。 + * + * @author 聂勇 + */ +@Sharable +public class EchoServerHandler extends ChannelInboundHandlerAdapter { + + private static Logger _logger = Logger.getLogger(EchoServerHandler.class); + + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ByteBuf inData = (ByteBuf) msg; + String str = inData.toString(Charset.forName("UTF-8")); + _logger.info( String.format("Server receive data:%s", str) ); + ctx.write(inData); + } + + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush().close(); + } + + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + _logger.error("occurs error", cause); + ctx.close(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/nio/BufferIO.java b/Java基础教程/Java源代码/codedemo/nio/BufferIO.java new file mode 100644 index 00000000..29c1c8da --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/nio/BufferIO.java @@ -0,0 +1,61 @@ +package cn.aofeng.demo.nio; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * IO-缓冲区操作。 + * + * @author 聂勇 aofengblog@163.com + */ +public class BufferIO { + + private static Logger logger = Logger.getLogger("bufferio"); + + // 缓冲区大小 + private final static int BUFFER_SIZE = 4096; + + public static void close(Closeable c) { + if (null != c) { + try { + c.close(); + } catch (IOException e) { + // ingore + } + } + } + + /** + * @param args [0]:读取文件的完整路径 + */ + public static void main(String[] args) { + String filename = args[0]; + File file = new File(filename); + FileChannel channel = null; + try { + channel = new FileInputStream(file).getChannel(); + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + long startTime = System.currentTimeMillis(); + while (channel.read(buffer) > 0) { + buffer.flip(); + buffer.clear(); + } + long endTime = System.currentTimeMillis(); + logger.info("缓冲区读取文件耗时:" + (endTime-startTime)+"毫秒"); + } catch (FileNotFoundException e) { + logger.log(Level.SEVERE, "找不到文件:"+filename, e); + } catch (IOException e) { + logger.log(Level.SEVERE, "读取文件出错", e); + } finally{ + close(channel); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/nio/MemoryMapper.java b/Java基础教程/Java源代码/codedemo/nio/MemoryMapper.java new file mode 100644 index 00000000..f3de00a9 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/nio/MemoryMapper.java @@ -0,0 +1,73 @@ +package cn.aofeng.demo.nio; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * IO-内存映射。 + * + * @author 聂勇 aofengblog@163.com + */ +public class MemoryMapper { + + private static Logger logger = Logger.getLogger("bufferio"); + + // 缓冲区大小 + private final static int BUFFER_SIZE = 4096; + + public static void close(Closeable c) { + if (null != c) { + try { + c.close(); + } catch (IOException e) { + // ingore + } + } + } + + /** + * @param args [0]:读取文件的完整路径 + */ + public static void main(String[] args) { + String filename = args[0]; + File file = new File(filename); + FileChannel channel = null; + try { + channel = new FileInputStream(file).getChannel(); + MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, 0, channel.size()); + long startTime = System.currentTimeMillis(); + long total = 0; + long len = file.length(); + byte[] bs = new byte[BUFFER_SIZE]; + for (int index = 0; index < len; index+=BUFFER_SIZE) { + int readSize = BUFFER_SIZE; + if (len-index < BUFFER_SIZE) { + readSize = (int) len-index; + buffer.get(new byte[readSize]); + } else { + buffer.get(bs); + } + + total += readSize; + } + long endTime = System.currentTimeMillis(); + logger.info("内存映射读取文件耗时:" + (endTime-startTime)+"毫秒,文件长度:"+total+"字节"); + } catch (FileNotFoundException e) { + logger.log(Level.SEVERE, "找不到文件:"+filename, e); + } catch (IOException e) { + logger.log(Level.SEVERE, "读取文件出错", e); + } finally{ + close(channel); + } + + } + +} diff --git a/Java基础教程/Java源代码/codedemo/nio/NioEchoServer.java b/Java基础教程/Java源代码/codedemo/nio/NioEchoServer.java new file mode 100644 index 00000000..95e21915 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/nio/NioEchoServer.java @@ -0,0 +1,296 @@ +package cn.aofeng.demo.nio; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 用NIO实现的Echo Server。 + * @author 聂勇 aofengblog@163.com + */ +public class NioEchoServer { + + private final static Logger logger = Logger.getLogger(NioEchoServer.class.getName()); + + // 换行符 + public final static char CR = '\r'; + + // 回车符 + public final static char LF = '\n'; + + /** + * @return 当前系统的行结束符 + */ + private static String getLineEnd() { + return System.getProperty("line.separator"); + } + + /** + * 重置缓冲区状态标志位:position设置为0,limit设置为capacity的值,所有mark无效。 + * 注:缓冲区原来的内容还在,并没有清除。 + * + * @param buffer 字节缓冲区 + */ + private static void clear(ByteBuffer buffer) { + if (null != buffer) { + buffer.clear(); + } + } + + /** + * 将字节缓冲区的每一个字节转换成ASCII字符。 + * @param buffer 字节缓冲区 + * @return 转换后的字节数组字符串 + */ + private static String toDisplayChar(ByteBuffer buffer) { + if (null == buffer) { + return "null"; + } + + return Arrays.toString(buffer.array()); + } + + /** + * 将字节缓冲区用utf8编码,转换成字符串。 + * + * @param buffer 字节缓冲区 + * @return utf8编码转换的字符串 + * @throws UnsupportedEncodingException + */ + private static String convert2String(ByteBuffer buffer) throws UnsupportedEncodingException { + return new String(buffer.array(), "utf8"); + } + + /** + * 去掉尾末的行结束符(\r\n),并转换成字符串。 + * + * @param buffer 字节缓冲区 + * @return 返回去掉行结束符后的字符串。 + * @throws UnsupportedEncodingException + * @see #convert2String(ByteBuffer) + */ + private static String getLineContent(ByteBuffer buffer) throws UnsupportedEncodingException { + if (null == buffer) { + return null; + } + + byte[] result = new byte[buffer.limit()-2]; + System.arraycopy(buffer.array(), 0, result, 0, result.length); + return convert2String(ByteBuffer.wrap(result)); + } + + /** + * 顺序合并两个{@link ByteBuffer}的内容,且不改变{@link ByteBuffer}原来的标志位。即: + *
+     * 合并后的ByteBuffer = first + second
+     * 
+ * @param first 第一个待合并的{@link ByteBuffer},合并后其内容在前面 + * @param second 第二个待合并的{@link ByteBuffer},合并后其内容在后面 + * @return 合并后的内容。如果两个{@link ByteBuffer}都为null,返回null。 + */ + private static ByteBuffer merge(ByteBuffer first, ByteBuffer second) { + if (null == first && null == second) { + return null; + } + + int oneSize = null != first ? first.limit() : 0; + int twoSize = null != second ? second.limit() : 0; + ByteBuffer result = ByteBuffer.allocate(oneSize+twoSize); + if (null != first) { + result.put(Arrays.copyOfRange(first.array(), 0, oneSize)); + } + if (null != second) { + result.put(Arrays.copyOfRange(second.array(), 0, twoSize)); + } + result.rewind(); + + return result; + } + + /** + * 从字节缓冲区中获取"一行",即获取包括行结束符及其前面的内容。 + * + * @param buffer 输入缓冲区 + * @return 有遇到行结束符,返回包括行结束符在内的字节缓冲区;否则返回null。 + */ + private static ByteBuffer getLine(ByteBuffer buffer) { + int index = 0; + boolean findCR = false; + int len = buffer.limit(); + while(index < len) { + index ++; + + byte temp = buffer.get(); + if (CR == temp) { + findCR = true; + } + if (LF == temp && findCR && index > 0) { // 找到了行结束符 + byte[] copy = new byte[index]; + System.arraycopy(buffer.array(), 0, copy, 0, index); + buffer.rewind(); // 位置复原 + return ByteBuffer.wrap(copy); + } + } + buffer.rewind(); // 位置复原 + + return null; + } + + private static void readData(Selector selector, SelectionKey selectionKey) throws IOException { + SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); + + // 获取上次已经读取的数据 + ByteBuffer oldBuffer = (ByteBuffer) selectionKey.attachment(); + if (logger.isLoggable(Level.FINE)) { + logger.fine("上一次读取的数据:"+oldBuffer+getLineEnd()+toDisplayChar(oldBuffer)); + } + + // 读新的数据 + int readNum = 0; + ByteBuffer newBuffer = ByteBuffer.allocate(1024); + if ( (readNum = socketChannel.read(newBuffer)) <= 0 ) { + return; + } + if (logger.isLoggable(Level.FINE)) { + logger.fine("这次读取的数据:"+newBuffer+getLineEnd()+toDisplayChar(newBuffer)); + } + + newBuffer.flip(); + ByteBuffer lineRemain = getLine(newBuffer); + if (logger.isLoggable(Level.FINE)) { + logger.fine("解析的行数据剩余部分:"+lineRemain+getLineEnd()+toDisplayChar(lineRemain)); + } + if (null != lineRemain) { // 获取到行结束符 + ByteBuffer completeLine = merge(oldBuffer, lineRemain); + if (logger.isLoggable(Level.FINE)) { + logger.fine("准备输出的数据:"+completeLine+getLineEnd()+toDisplayChar(completeLine)); + } + while (completeLine.hasRemaining()) { // 有可能一次没有写完,需多次写 + socketChannel.write(completeLine); + } + + // 清除数据 + selectionKey.attach(null); + clear(oldBuffer); + clear(lineRemain); + + // 判断是否退出 + String lineStr = getLineContent(completeLine); + if (logger.isLoggable(Level.FINE)) { + logger.fine("判断是否退出的行数据:"+lineStr); + } + if ("exit".equalsIgnoreCase(lineStr) || "quit".equalsIgnoreCase(lineStr)) { + socketChannel.close(); + } + + // FIXME 行结束符后面是否还有数据? 此部分代码尚未测试 + if (lineRemain.limit()+2 < newBuffer.limit()) { + byte[] temp = new byte[newBuffer.limit() - lineRemain.limit()]; + newBuffer.get(temp, lineRemain.limit(), temp.length); + + selectionKey.attach(temp); + } + } else { // 没有读到一个完整的行,继续读并且带上已经读取的部分数据 + ByteBuffer temp = merge(oldBuffer, newBuffer); + socketChannel.register(selector, SelectionKey.OP_READ, temp); + + if (logger.isLoggable(Level.FINE)) { + logger.fine("暂存到SelectionKey的数据:"+temp+getLineEnd()+toDisplayChar(temp)); + } + } + } + + /** + * 接受新的Socket连接。 + * + * @param selector 选择器 + * @param selectionKey + * @return + * @throws IOException + * @throws ClosedChannelException + */ + private static SocketChannel acceptNew(Selector selector, + SelectionKey selectionKey) throws IOException, + ClosedChannelException { + ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel(); + SocketChannel socketChannel = server.accept(); + if (null != socketChannel) { + if (logger.isLoggable(Level.INFO)) { + logger.info("收到一个新的连接,客户端IP:"+socketChannel.socket().getInetAddress().getHostAddress()+",客户端Port:"+socketChannel.socket().getPort()); + } + socketChannel.configureBlocking(false); + socketChannel.register(selector, SelectionKey.OP_READ); + } + + return socketChannel; + } + + /** + * 启动服务器。 + * + * @param port 服务监听的端口 + * @param selectTimeout {@link Selector}检查通道就绪状态的超时时间(单位:毫秒) + */ + private static void startServer(int port, int selectTimeout) { + ServerSocketChannel serverChannel = null; + try { + serverChannel = ServerSocketChannel.open(); + serverChannel.configureBlocking(false); + ServerSocket serverSocket = serverChannel.socket(); + serverSocket.bind(new InetSocketAddress(port)); + if (logger.isLoggable(Level.INFO)) { + logger.info("NIO echo网络服务启动完毕,监听端口:" +port); + } + + Selector selector = Selector.open(); + serverChannel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + int selectNum = selector.select(selectTimeout); + if (0 == selectNum) { + continue; + } + + Set selectionKeys = selector.selectedKeys(); + Iterator it = selectionKeys.iterator(); + while (it.hasNext()) { + SelectionKey selectionKey = (SelectionKey) it.next(); + + // 接受新的Socket连接 + if (selectionKey.isAcceptable()) { + acceptNew(selector, selectionKey); + } + + // 读取并处理Socket的数据 + if (selectionKey.isReadable()) { + readData(selector, selectionKey); + } + + it.remove(); + } // end of while iterator + } + } catch (IOException e) { + logger.log(Level.SEVERE, "处理网络连接出错", e); + } + } + + public static void main(String[] args) { + int port = 9090; + int selectTimeout = 1000; + + startServer(port, selectTimeout); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/AccountService.java b/Java基础教程/Java源代码/codedemo/proxy/AccountService.java new file mode 100644 index 00000000..83c8f217 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/AccountService.java @@ -0,0 +1,27 @@ +package cn.aofeng.demo.proxy; + +/** + * 账号服务接口定义。 + * + * @author 聂勇 + */ +public interface AccountService { + + /** + * 注册。 + * + * @param username 账号名 + * @return 具体含义查看{@link Result}的说明。 + */ + Result register(String username); + + /** + * 登录。 + * + * @param username 账号名 + * @param password 密码 + * @return 具体含义查看{@link Result}的说明。 + */ + Result login(String username, String password); + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/AccountServiceClient.java b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceClient.java new file mode 100644 index 00000000..946409ac --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceClient.java @@ -0,0 +1,28 @@ +package cn.aofeng.demo.proxy; + +/** + * 代理调用示例。 + * + * @author 聂勇 + */ +public class AccountServiceClient { + + public static void main(String[] args) { + AccountService as = new AccountServiceImpl(); + + // 静态代理 + AccountService staticProxy = new AccountServiceStaticProxy(as); + staticProxy.register(null); + staticProxy.register("A0001"); + staticProxy.login(null, null); + staticProxy.login("A0001", "PWD0001"); + + // 动态代理 + AccountService dynamicProxy = AccountServiceDynamicProxy.newInstance(as); + dynamicProxy.register(null); + dynamicProxy.register("A0001"); + dynamicProxy.login(null, null); + dynamicProxy.login("A0001", "PWD0001"); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/AccountServiceDynamicProxy.java b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceDynamicProxy.java new file mode 100644 index 00000000..562543c4 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceDynamicProxy.java @@ -0,0 +1,42 @@ +package cn.aofeng.demo.proxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 账号服务动态代理。 + * + * @author 聂勇 + */ +public class AccountServiceDynamicProxy implements InvocationHandler { + + private final static Logger _LOGGER = LoggerFactory.getLogger(AccountServiceDynamicProxy.class); + + private AccountService _accountService; + + public AccountServiceDynamicProxy(AccountService accountService) { + this._accountService = accountService; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + _LOGGER.debug("execute method {}, arguments: {}", method.getName(), args); + Object result = method.invoke(_accountService, args); + _LOGGER.debug("execute method {}, result:{}", method.getName(), result); + + return result; + } + + public static AccountService newInstance(AccountService accountService) { + ClassLoader loader = accountService.getClass().getClassLoader(); + Class[] interfaces = accountService.getClass().getInterfaces(); + + return (AccountService) Proxy.newProxyInstance(loader, + interfaces, + new AccountServiceDynamicProxy(accountService)); + } +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/AccountServiceImpl.java b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceImpl.java new file mode 100644 index 00000000..f527efc7 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceImpl.java @@ -0,0 +1,53 @@ +package cn.aofeng.demo.proxy; + +import java.util.Random; + +import wiremock.org.apache.commons.lang.StringUtils; + +/** + * + * + * @author 聂勇 + */ +public class AccountServiceImpl implements AccountService { + + private final static Random NUM = new Random(System.currentTimeMillis()); + + @Override + public Result register(String username) { + if (StringUtils.isBlank(username)) { + return createResult(4000001, "注册失败"); + } + + User user = new User(); + user.setUid(NUM.nextInt()); + user.setNickname("用户"+user.getUid()); + return createResult(2000001, "注册成功", user); + } + + @Override + public Result login(String username, String password) { + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return createResult(4000001, "登录失败"); + } + + User user = new User(); + user.setUid(NUM.nextInt()); + user.setNickname("用户"+user.getUid()); + return createResult(2000001, "登录成功", user); + } + + private Result createResult(int code, String msg) { + Result result = new Result(code, msg); + + return result; + } + + private Result createResult(int code, String msg, User user) { + Result result = new Result(code, msg); + result.setData(user); + + return result; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/AccountServiceStaticProxy.java b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceStaticProxy.java new file mode 100644 index 00000000..9705868b --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/AccountServiceStaticProxy.java @@ -0,0 +1,39 @@ +package cn.aofeng.demo.proxy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link AccountServiceImpl}的代理类,增加了输出入参和响应的日志。 + * + * @author 聂勇 + */ +public class AccountServiceStaticProxy implements AccountService { + + private AccountService _delegate; + + private final static Logger _LOGGER = LoggerFactory.getLogger(AccountServiceStaticProxy.class); + + public AccountServiceStaticProxy(AccountService accountService) { + this._delegate = accountService; + } + + @Override + public Result register(String username) { + _LOGGER.debug("execute method register, arguments: username={}", username); + Result result = _delegate.register(username); + _LOGGER.debug("execute method register, result:{}", result); + + return result; + } + + @Override + public Result login(String username, String password) { + _LOGGER.debug("execute method login, arguments: username={}, password={}", username, password); + Result result = _delegate.login(username, password); + _LOGGER.debug("execute method login, result:{}", result); + + return result; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/Result.java b/Java基础教程/Java源代码/codedemo/proxy/Result.java new file mode 100644 index 00000000..66ee0384 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/Result.java @@ -0,0 +1,65 @@ +package cn.aofeng.demo.proxy; + +/** + * 账号服务操作结果。 + * + * @author 聂勇 + */ +public class Result { + + /** + * 200xxxx 表示成功。
+ * 400xxxx 表示参数错误。
+ * 500xxxx 表示服务内部错误。 + */ + private int code; + + /** + * code对应的描述信息。 + */ + private String msg; + + /** + * 只有code为200xxxx时才有值,其他情况下为null。 + */ + private User data; + + public Result() { + // nothing + } + + public Result(int code, String msg) { + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMsg() { + return msg; + } + + public void setMsg(String msg) { + this.msg = msg; + } + + public User getData() { + return data; + } + + public void setData(User data) { + this.data = data; + } + + @Override + public String toString() { + return "Result [code=" + code + ", msg=" + msg + ", data=" + data + "]"; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/proxy/User.java b/Java基础教程/Java源代码/codedemo/proxy/User.java new file mode 100644 index 00000000..a9cbd692 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/proxy/User.java @@ -0,0 +1,38 @@ +/** + * + */ +package cn.aofeng.demo.proxy; + +/** + * 用户信息。 + * + * @author 聂勇 + */ +public class User { + + private long uid; + + private String nickname; + + public long getUid() { + return uid; + } + + public void setUid(long uid) { + this.uid = uid; + } + + public String getNickname() { + return nickname; + } + + public void setNickname(String nickname) { + this.nickname = nickname; + } + + @Override + public String toString() { + return "User [uid=" + uid + ", nickname=" + nickname + "]"; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Acceptor.java b/Java基础教程/Java源代码/codedemo/reactor/Acceptor.java new file mode 100644 index 00000000..b1104ec9 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Acceptor.java @@ -0,0 +1,48 @@ +package cn.aofeng.demo.reactor; + +import java.io.IOException; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 负责处理新连入的客户端Socket连接。 + * + * @author NieYong + */ +public class Acceptor { + + private final static Logger _logger = Logger.getLogger(Acceptor.class.getName()); + + protected Selector _selector; + + protected ServerSocketChannel _serverChannel; + + public Acceptor(Selector selector, ServerSocketChannel serverChannel) { + this._selector = selector; + this._serverChannel = serverChannel; + } + + /** + * 接收一个新连入的客户端Socket连接,交给{@link Reader}处理:{@link Reader}向{@link Selector}注册并关注READ事件。 + * + * @throws IOException + */ + public void accept() throws IOException { + SocketChannel clientChannel = _serverChannel.accept(); + if (null != clientChannel) { + if (_logger.isLoggable(Level.INFO)) { + _logger.info("收到一个新的连接,客户端IP:"+clientChannel.socket().getInetAddress().getHostAddress() + +",客户端Port:"+clientChannel.socket().getPort()); + } + clientChannel.configureBlocking(false); + Reader reader = new Reader(_selector, clientChannel); + reader.setDecoder(new LineDecoder()); + clientChannel.register(_selector, SelectionKey.OP_READ, reader); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Constant.java b/Java基础教程/Java源代码/codedemo/reactor/Constant.java new file mode 100644 index 00000000..da5df983 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Constant.java @@ -0,0 +1,25 @@ +package cn.aofeng.demo.reactor; + +/** + * 常量定义。 + * + * @author NieYong + */ +public class Constant { + + /** + * UTF-8字符集。 + */ + public final static String CHARSET_UTF8 = "UTF-8"; + + /** + * 换行符。 + */ + public final static char CR = '\r'; + + /** + * 回车符。 + */ + public final static char LF = '\n'; + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Decoder.java b/Java基础教程/Java源代码/codedemo/reactor/Decoder.java new file mode 100644 index 00000000..c1977b26 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Decoder.java @@ -0,0 +1,20 @@ +package cn.aofeng.demo.reactor; + +import java.nio.ByteBuffer; + +/** + * 请求数据解析器接口定义。 + * + * @author NieYong + */ +public interface Decoder { + + /** + * 解析请求数据,不影响源数据的状态和内容。 + * + * @param source {@link Reader}读取到的源数据字节数组 + * @return 如果解析到符合要求的数据,则返回解析到的数据;否则返回null。 + */ + public Object decode(ByteBuffer source); + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Encoder.java b/Java基础教程/Java源代码/codedemo/reactor/Encoder.java new file mode 100644 index 00000000..ed5b626f --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Encoder.java @@ -0,0 +1,20 @@ +package cn.aofeng.demo.reactor; + +import java.nio.ByteBuffer; + +/** + * 响应数据封装接口定义。 + * + * @author NieYong + */ +public interface Encoder { + + /** + * 将源数据转换成{@link ByteBuffer}。 + * + * @param source 源数据 + * @return {@link ByteBuffer}对象。 + */ + public ByteBuffer encode(Object source); + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/LineDecoder.java b/Java基础教程/Java源代码/codedemo/reactor/LineDecoder.java new file mode 100644 index 00000000..fb043eb2 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/LineDecoder.java @@ -0,0 +1,50 @@ +package cn.aofeng.demo.reactor; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 行数据解析器。 + * + * @author NieYong + */ +public class LineDecoder implements Decoder { + + private final static Logger _logger = Logger.getLogger(LineDecoder.class.getName()); + + /** + * 从字节缓冲区中获取"一行"。 + * + * @param buffer 输入缓冲区 + * @return 有遇到行结束符,返回不包括行结束符的字符串;否则返回null。 + */ + @Override + public String decode(ByteBuffer source) { + int index = 0; + boolean findCR = false; + int len = source.limit(); + byte[] bytes = source.array(); + while(index < len) { + index ++; + + byte temp = bytes[index-1]; + if (Constant.CR == temp) { + findCR = true; + } + if (Constant.LF == temp && findCR) { // 找到了行结束符 + byte[] copy = new byte[index]; + System.arraycopy(bytes, 0, copy, 0, index); + try { + return new String(copy, Constant.CHARSET_UTF8); + } catch (UnsupportedEncodingException e) { + _logger.log(Level.SEVERE, "将解析完成的请求数据转换成字符串出错", e); + } + } + } + + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/LineEncoder.java b/Java基础教程/Java源代码/codedemo/reactor/LineEncoder.java new file mode 100644 index 00000000..c6819164 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/LineEncoder.java @@ -0,0 +1,31 @@ +package cn.aofeng.demo.reactor; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 将字符串转换成{@link ByteBuffer}并加上行结束符。 + * + * @author NieYong + */ +public class LineEncoder implements Encoder { + + private final static Logger logger = Logger.getLogger(LineEncoder.class.getName()); + + @Override + public ByteBuffer encode(Object source) { + String line = (String) source; + try { + ByteBuffer buffer = ByteBuffer.wrap(line.getBytes(Constant.CHARSET_UTF8)); + + return buffer; + } catch (UnsupportedEncodingException e) { + logger.log(Level.SEVERE, "将响应数据转换成ByteBuffer出错", e); + } + + return null; + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/ProcessService.java b/Java基础教程/Java源代码/codedemo/reactor/ProcessService.java new file mode 100644 index 00000000..07178f11 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/ProcessService.java @@ -0,0 +1,46 @@ +package cn.aofeng.demo.reactor; + +import java.io.IOException; +import java.nio.channels.SocketChannel; + +/** + * 业务逻辑处理。 + * + * @author NieYong + */ +public class ProcessService { + + private SocketChannel _clientChannel; + + private String _line; + + public ProcessService(SocketChannel clientChannel, String line) { + this._clientChannel = clientChannel; + this._line = line; + } + + public String execute() { + // 判断客户端是否发送了退出指令 + String content = _line.substring(0, _line.length()-2); + if (isCloseClient(content)) { + try { + _clientChannel.close(); + } catch (IOException e) { + // nothing + } + } + + return _line; + } + + /** + * 客户端是否发送了退出指令("quit" | "exit")。 + * + * @param str 收到的客户端数据 + * @return 返回true表示收到了退出指令;否则返回false。 + */ + private boolean isCloseClient(String str) { + return "exit".equalsIgnoreCase(str) || "quit".equalsIgnoreCase(str); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Reactor.java b/Java基础教程/Java源代码/codedemo/reactor/Reactor.java new file mode 100644 index 00000000..700d8da0 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Reactor.java @@ -0,0 +1,113 @@ +package cn.aofeng.demo.reactor; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.util.Iterator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 负责Echo Server启动和停止 ,ACCEPT和READ事件的分派。 + * + * @author NieYong + */ +public class Reactor { + + private final static Logger logger = Logger.getLogger(Reactor.class.getName()); + + // 监听端口 + private int _port; + + // {@link Selector}检查通道就绪状态的超时时间(单位:毫秒) + private int _selectTimeout = 3000; + + // 服务运行状态 + private volatile boolean _isRun = true; + + /** + * @param port 服务监听端口。 + */ + public Reactor(int port) { + this._port = port; + } + + public void setSelectTimeout(int selectTimeout) { + this._selectTimeout = selectTimeout; + } + + /** + * 启动服务。 + */ + public void start() { + ServerSocketChannel serverChannel = null; + try { + serverChannel = ServerSocketChannel.open(); + serverChannel.configureBlocking(false); + ServerSocket serverSocket = serverChannel.socket(); + serverSocket.bind(new InetSocketAddress(_port)); + _isRun = true; + if (logger.isLoggable(Level.INFO)) { + logger.info("NIO echo网络服务启动完毕,监听端口:" +_port); + } + + Selector selector = Selector.open(); + serverChannel.register(selector, SelectionKey.OP_ACCEPT, new Acceptor(selector, serverChannel)); + + while (_isRun) { + int selectNum = selector.select(_selectTimeout); + if (0 == selectNum) { + continue; + } + + Set selectionKeys = selector.selectedKeys(); + Iterator it = selectionKeys.iterator(); + while (it.hasNext()) { + SelectionKey selectionKey = (SelectionKey) it.next(); + + // 接受新的Socket连接 + if (selectionKey.isValid() && selectionKey.isAcceptable()) { + Acceptor acceptor = (Acceptor) selectionKey.attachment(); + acceptor.accept(); + } + + // 读取并处理Socket的数据 + if (selectionKey.isValid() && selectionKey.isReadable()) { + Reader reader = (Reader) selectionKey.attachment(); + reader.read(); + } + + // 移除已经处理过的Key + it.remove(); + } // end of while iterator + } + } catch (IOException e) { + logger.log(Level.SEVERE, "处理网络连接出错", e); + } + } + + /** + * 停止服务。 + */ + public void stop() { + _isRun = false; + } + + public static void main(String[] args) { + if (1 != args.length) { + logger.severe("无效参数。使用示例:\n java cn.aofeng.demo.reactor.Reactor 9090"); + System.exit(-1); + } + int port = Integer.parseInt(args[0]); + int selectTimeout = 1000; + + Reactor reactor = new Reactor(port); + reactor.setSelectTimeout(selectTimeout); + reactor.start(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Reader.java b/Java基础教程/Java源代码/codedemo/reactor/Reader.java new file mode 100644 index 00000000..a163b53d --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Reader.java @@ -0,0 +1,94 @@ +package cn.aofeng.demo.reactor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 负责读取客户端的请求数据并解析。 + * + * @author NieYong + */ +public class Reader { + + private final static Logger _logger = Logger.getLogger(Reader.class.getName()); + + private SocketChannel _clientChannel; + + private Decoder _decoder; + + private final static int BUFFER_SIZE = 512; + + private ByteBuffer _buffer = ByteBuffer.allocate(BUFFER_SIZE); + + public Reader(Selector selector, SocketChannel clientChannel) { + this._clientChannel = clientChannel; + } + + public void setDecoder(Decoder decoder) { + this._decoder = decoder; + } + + public void read() throws IOException { + int readCount = _clientChannel.read(_buffer); + if (-1 == readCount) { + _clientChannel.close(); + } + + _buffer.flip(); + int oldLimit = _buffer.limit(); + String line = null; + while( (line = (String) _decoder.decode(_buffer)) != null ) { // 处理一次多行发送过来的情况 + if (_logger.isLoggable(Level.FINE)) { + _logger.fine("收到的数据:"+line); + } + + // 处理业务逻辑 + ProcessService service= new ProcessService(_clientChannel, line); + String result = service.execute(); + + // 发送响应 + Writer writer = new Writer(_clientChannel, result); + writer.setEncoder(new LineEncoder()); + writer.write(); + + // 重建临时数据缓冲区 + rebuildBuffer(line.length()); + } + + // 缓冲区数据还没有符合一个decode数据的条件,重置数据缓冲区的状态方便append数据 + if (oldLimit == _buffer.limit()) { + resetBuffer(); + } + } + + private void resetBuffer() { + _buffer.position(_buffer.limit()); + _buffer.limit(_buffer.capacity()); + } + + /** + * 重建临时数据缓冲区。 + * + * @param lineSize 收到的一行数据(不包括行结束符)的长度 + */ + private void rebuildBuffer(int lineSize) { + if (_buffer.limit() == lineSize) { + // 数据刚好是一行 + _buffer = ByteBuffer.allocate(BUFFER_SIZE); + } else if (_buffer.limit() > lineSize) { + // 数据多于一行 + byte[] temp = new byte[_buffer.limit() - lineSize]; + System.arraycopy(_buffer.array(), lineSize, temp, 0, temp.length); + _buffer = ByteBuffer.allocate(BUFFER_SIZE); + _buffer.put(temp); + _buffer.flip(); + } else { + // nothing + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/reactor/Writer.java b/Java基础教程/Java源代码/codedemo/reactor/Writer.java new file mode 100644 index 00000000..e6299fb8 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/reactor/Writer.java @@ -0,0 +1,41 @@ +package cn.aofeng.demo.reactor; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +/** + * 负责向客户端发送响应数据。 + * + * @author NieYong + */ +public class Writer { + + private SocketChannel _clientChannel; + + private Object _data; + + private Encoder _encoder; + + public Writer(SocketChannel clientChannel, Object data) { + this._clientChannel = clientChannel; + this._data = data; + } + + public void setEncoder(Encoder encoder) { + this._encoder = encoder; + } + + public void write() throws IOException { + if (null == _data || !_clientChannel.isOpen()) { + return; + } + + ByteBuffer buffer = _encoder.encode(_data); + if (null == buffer) { + return; + } + _clientChannel.write(buffer); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/redis/JedisDemo.java b/Java基础教程/Java源代码/codedemo/redis/JedisDemo.java new file mode 100644 index 00000000..46395dfe --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/redis/JedisDemo.java @@ -0,0 +1,133 @@ +package cn.aofeng.demo.redis; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.ShardedJedis; +import redis.clients.jedis.ShardedJedisPool; +import sun.security.krb5.internal.HostAddress; + +/** + * Redis客户端Jedis使用示例。 + * + * @author 聂勇 + */ +public class JedisDemo { + + /** + * @param args + */ + public static void main(String[] args) { + single(); + pool(); + shardPool(); + } + + /** + * 单机单连接方式。 + */ + private static void single() { + Jedis client = new Jedis("192.168.56.102", 6379); + String result = client.set("key-string", "Hello, Redis!"); + System.out.println( String.format("set指令执行结果:%s", result) ); + String value = client.get("key-string"); + System.out.println( String.format("get指令执行结果:%s", value) ); + } + + /** + * 集群方式(尚未实现)。 + */ + private static void cluster() { + // 生成集群节点列表 + Set clusterNodes = new HashSet(); + clusterNodes.add(new HostAndPort("127.0.0.1", 6379)); + clusterNodes.add(new HostAndPort("192168.56.102", 6379)); + + // 执行指令 + JedisCluster client = new JedisCluster(clusterNodes); + String result = client.set("key-string", "Hello, Redis!"); + System.out.println( String.format("set指令执行结果:%s", result) ); + String value = client.get("key-string"); + System.out.println( String.format("get指令执行结果:%s", value) ); + } + + /** + * 单机连接池方式。 + */ + private static void pool() { + // 在应用初始化的时候生成连接池 + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxIdle(10); + config.setMaxTotal(30); + config.setMaxWaitMillis(3*1000); + JedisPool pool = new JedisPool(config, "192.168.56.102", 6379); + + // 在业务操作时,从连接池获取连接 + Jedis client = pool.getResource(); + try { + // 执行指令 + String result = client.set("key-string", "Hello, Redis!"); + System.out.println( String.format("set指令执行结果:%s", result) ); + String value = client.get("key-string"); + System.out.println( String.format("get指令执行结果:%s", value) ); + } catch (Exception e) { + // TODO: handle exception + } finally { + // 业务操作完成,将连接返回给连接池 + if (null != client) { + pool.returnResource(client); + } + } // end of try block + + // 应用关闭时,释放连接池资源 + pool.destroy(); + } + + /** + * 多机分布式+连接池。 + */ + private static void shardPool() { + // 生成多机连接信息列表 + List shards = new ArrayList(); + shards.add( new JedisShardInfo("127.0.0.1", 6379) ); + shards.add( new JedisShardInfo("192.168.56.102", 6379) ); + + // 生成连接池配置信息 + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxIdle(10); + config.setMaxTotal(30); + config.setMaxWaitMillis(3*1000); + + // 在应用初始化的时候生成连接池 + ShardedJedisPool pool = new ShardedJedisPool(config, shards); + + // 在业务操作时,从连接池获取连接 + ShardedJedis client = pool.getResource(); + try { + // 执行指令 + String result = client.set("key-string", "Hello, Redis!"); + System.out.println( String.format("set指令执行结果:%s", result) ); + String value = client.get("key-string"); + System.out.println( String.format("get指令执行结果:%s", value) ); + } catch (Exception e) { + // TODO: handle exception + } finally { + // 业务操作完成,将连接返回给连接池 + if (null != client) { + pool.returnResource(client); + } + } // end of try block + + // 应用关闭时,释放连接池资源 + pool.destroy(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/script/MultiScriptEngineCompare.java b/Java基础教程/Java源代码/codedemo/script/MultiScriptEngineCompare.java new file mode 100644 index 00000000..5e1d61e8 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/script/MultiScriptEngineCompare.java @@ -0,0 +1,250 @@ +package cn.aofeng.demo.script; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * 多个脚本引擎执行JavaScript的性能比较。 + * + * @author 聂勇 + */ +public class MultiScriptEngineCompare { + + public final static String PARSE = "parse"; + public final static String COMPILE = "compile"; + + /** + * 获取指定的脚本引擎执行指定的脚本(解释执行)。 + * + * @param scriptEngineName 脚本引擎名称 + * @param script 脚本 + * @param count 脚本的执行次数 + * @param vars 绑定到脚本的变量集合 + * @throws ScriptException 执行脚本出错 + */ + public ExecuteResult parse(String scriptEngineName, String script, int count, + Map vars) throws ScriptException { + ScriptEngine scriptEngine = getScriptEngine(scriptEngineName); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + runSingleScript(script, vars, scriptEngine); + } + long usedTime = System.currentTimeMillis() - startTime; + + ExecuteResult result = new ExecuteResult(); + result.setEngine(scriptEngine.getFactory().getEngineName()); + result.setScript(script); + result.setBindParam(vars.toString()); + result.setExecuteCount(count); + result.setExecuteType(PARSE); + result.setUsedTime(usedTime); + + return result; + } + + private void runSingleScript(String script, Map vars, + ScriptEngine scriptEngine) throws ScriptException { + if (null == vars || vars.isEmpty()) { + scriptEngine.eval(script); + } else { + Bindings binds = createBinding(scriptEngine, vars); + scriptEngine.eval(script, binds); + } + } + + public ExecuteResult compile(String scriptEngineName, String script, int count, + Map vars) throws ScriptException { + ScriptEngine scriptEngine = getScriptEngine(scriptEngineName); + Compilable compileEngine = (Compilable) scriptEngine; + CompiledScript compileScript = compileEngine.compile(script); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < count; i++) { + runSingleScript(compileScript, vars, scriptEngine); + } + long usedTime = System.currentTimeMillis() - startTime; + + ExecuteResult result = new ExecuteResult(); + result.setEngine(scriptEngine.getFactory().getEngineName()); + result.setScript(script); + result.setBindParam(vars.toString()); + result.setExecuteCount(count); + result.setExecuteType(COMPILE); + result.setUsedTime(usedTime); + + return result; + } + + private void runSingleScript(CompiledScript compileScript, Map vars, + ScriptEngine scriptEngine) throws ScriptException { + if (null == vars || vars.isEmpty()) { + compileScript.eval(); + } else { + Bindings binds = createBinding(scriptEngine, vars); + compileScript.eval(binds); + } + } + + protected void log(String msg) { + System.out.println(msg); + } + + protected void log(String msg, Object... args) { + log( String.format(msg, args) ); + } + + /** + * 根据名称获取脚本引擎。 + * + * @param name 脚本引擎名称 + * @return 实现了{@link ScriptEngine}的脚本引擎。如果没有对应的脚本引擎,返回null。 + */ + public ScriptEngine getScriptEngine(String name) { + ScriptEngineManager sem = new ScriptEngineManager(); + return sem.getEngineByName(name); + } + + private Bindings createBinding(ScriptEngine scriptEngine, Map vars) { + Bindings binds = scriptEngine.createBindings(); + if (null != vars && !vars.isEmpty()) { + binds.putAll(vars); + } + + return binds; + } + + /** + * @param args 执行次数 + * @throws ScriptException + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static void main(String[] args) throws ScriptException { + if ( args.length != 2) { + System.err.println("参数错误。\n语法格式:\n java cn.aofeng.demo.script.MultiScriptEngineCompare 脚本执行次数 脚本执行方式(parse|compile)\n使用示例:\n java cn.aofeng.demo.script.MultiScriptEngineCompare 100000 parse"); + System.exit(-1); + } + int count = Integer.parseInt(args[0]); + String executeType = args[1]; + + String[] scriptEngineList = {"JavaScript", "JEXL"}; + + String script1 = "var c = a + b;" + + "var d = a * b;" + + "var e = a / b;" + + "var f = a % b;" + + "var g = a - b;" + + "var result = ((a * 5) > b || b * 10 >= 100) && (a * b > 99);"; + Map vars1 = new HashMap(2); + vars1.put("a", 20); + vars1.put("b", 9); + + String script2 = "var result = src.indexOf(b);"; + Map vars2 = new HashMap(2); + vars2.put("src", "compare performance javascript and jexl"); + vars2.put("b", "script"); + + String[] scriptList = {script1, script2}; + Map[] varsList = {vars1, vars2}; + + MultiScriptEngineCompare msec = new MultiScriptEngineCompare(); + List resultList = new ArrayList(); + for (int i = 0; i < scriptEngineList.length; i++) { + for (int j = 0; j < scriptList.length; j++) { + if (PARSE.equalsIgnoreCase(executeType)) { + resultList.add( msec.parse(scriptEngineList[i], scriptList[j], count, varsList[j]) ); + } else if (COMPILE.equalsIgnoreCase(executeType)) { + resultList.add( msec.compile(scriptEngineList[i], scriptList[j], count, varsList[j]) ); + } else { + msec.log("错误的执行方式:%s", executeType); + } + } + } + + List arrayList = new ArrayList(); + arrayList.add(new String[]{"脚本引擎", "脚本", "脚本绑定参数", "脚本执行次数", "脚本执行类型", "消耗时间(毫秒)", "JDK版本"}); + for (Iterator iterator = resultList.iterator(); iterator.hasNext();) { + ExecuteResult er = (ExecuteResult) iterator.next(); + arrayList.add(new String[]{er.getEngine(), + er.getScript(), + er.getBindParam(), + String.valueOf(er.getExecuteCount()), + er.getExecuteType(), + String.valueOf(er.getUsedTime()), + er.getJdkVersion()}); + } + + String[][] table = new String[arrayList.size()][7]; + arrayList.toArray(table); + + PrettyTable prettyTable = new PrettyTable(System.out); + prettyTable.print(table); + } + + static class ExecuteResult { + + private String engine; + private String script; + private String bindParam; + private int executeCount; + private String executeType; + private long usedTime; + private String jdkVersion; + + public ExecuteResult() { + this.jdkVersion = System.getProperty("java.version"); + } + + public String getEngine() { + return engine; + } + public void setEngine(String engine) { + this.engine = engine; + } + public String getScript() { + return script; + } + public void setScript(String script) { + this.script = script; + } + public String getBindParam() { + return bindParam; + } + public void setBindParam(String bindParam) { + this.bindParam = bindParam; + } + public int getExecuteCount() { + return executeCount; + } + public void setExecuteCount(int executeCount) { + this.executeCount = executeCount; + } + public String getExecuteType() { + return executeType; + } + public void setExecuteType(String executeType) { + this.executeType = executeType; + } + public long getUsedTime() { + return usedTime; + } + public void setUsedTime(long usedTime) { + this.usedTime = usedTime; + } + public String getJdkVersion() { + return jdkVersion; + } + public void setJdkVersion(String jdkVersion) { + this.jdkVersion = jdkVersion; + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/script/PrettyTable.java b/Java基础教程/Java源代码/codedemo/script/PrettyTable.java new file mode 100644 index 00000000..b45b60cf --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/script/PrettyTable.java @@ -0,0 +1,125 @@ +package cn.aofeng.demo.script; + +import java.io.PrintStream; +import static java.lang.String.format; + +/** + * 此代码来自于网络。使用示例: + *
+ * PrettyTable printer = new PrettyTable(out);
+ * printer.print(new String[][] {
+ *      new String[] {"FIRST NAME", "LAST NAME", "DATE OF BIRTH", "NOTES"},
+ *      new String[] {"Joe", "Smith", "November 2, 1972"},
+ *      null,
+ *      new String[] {"John", "Doe", "April 29, 1970", "Big Brother"},
+ *      new String[] {"Jack", null, null, "(yes, no last name)"}
+ * });
+
+ */ +public final class PrettyTable { + + private static final char BORDER_KNOT = '+'; + private static final char HORIZONTAL_BORDER = '-'; + private static final char VERTICAL_BORDER = '|'; + + private static final String DEFAULT_AS_NULL = "(NULL)"; + + private final PrintStream out; + private final String asNull; + + public PrettyTable(PrintStream out) { + this(out, DEFAULT_AS_NULL); + } + + public PrettyTable(PrintStream out, String asNull) { + if ( out == null ) { + throw new IllegalArgumentException("No print stream provided"); + } + if ( asNull == null ) { + throw new IllegalArgumentException("No NULL-value placeholder provided"); + } + this.out = out; + this.asNull = asNull; + } + + public void print(String[][] table) { + if ( table == null ) { + throw new IllegalArgumentException("No tabular data provided"); + } + if ( table.length == 0 ) { + return; + } + final int[] widths = new int[getMaxColumns(table)]; + adjustColumnWidths(table, widths); + printPreparedTable(table, widths, getHorizontalBorder(widths)); + } + + private void printPreparedTable(String[][] table, int widths[], String horizontalBorder) { + final int lineLength = horizontalBorder.length(); + out.println(horizontalBorder); + for ( final String[] row : table ) { + if ( row != null ) { + out.println(getRow(row, widths, lineLength)); + out.println(horizontalBorder); + } + } + } + + private String getRow(String[] row, int[] widths, int lineLength) { + final StringBuilder builder = new StringBuilder(lineLength).append(VERTICAL_BORDER); + final int maxWidths = widths.length; + for ( int i = 0; i < maxWidths; i++ ) { + builder.append(padRight(getCellValue(safeGet(row, i, null)), widths[i])).append(VERTICAL_BORDER); + } + return builder.toString(); + } + + private String getHorizontalBorder(int[] widths) { + final StringBuilder builder = new StringBuilder(256); + builder.append(BORDER_KNOT); + for ( final int w : widths ) { + for ( int i = 0; i < w; i++ ) { + builder.append(HORIZONTAL_BORDER); + } + builder.append(BORDER_KNOT); + } + return builder.toString(); + } + + private int getMaxColumns(String[][] rows) { + int max = 0; + for ( final String[] row : rows ) { + if ( row != null && row.length > max ) { + max = row.length; + } + } + return max; + } + + private void adjustColumnWidths(String[][] rows, int[] widths) { + for ( final String[] row : rows ) { + if ( row != null ) { + for ( int c = 0; c < widths.length; c++ ) { + final String cv = getCellValue(safeGet(row, c, asNull)); + final int l = cv.length(); + if ( widths[c] < l ) { + widths[c] = l; + } + } + } + } + } + + private static String padRight(String s, int n) { + return format("%1$-" + n + "s", s); + } + + private static String safeGet(String[] array, int index, String defaultValue) { + return index < array.length ? array[index] : defaultValue; + } + + private String getCellValue(Object value) { + return value == null ? asNull : value.toString(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/script/ScriptRunPerformence.java b/Java基础教程/Java源代码/codedemo/script/ScriptRunPerformence.java new file mode 100644 index 00000000..6deb86ad --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/script/ScriptRunPerformence.java @@ -0,0 +1,112 @@ +package cn.aofeng.demo.script; + +import java.util.HashMap; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.Compilable; +import javax.script.CompiledScript; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; + +/** + * 脚本语言运行性能测试: + *
+ * 1、每次运行脚本都解释的执行性能。
+ * 2、编译脚本后再运行的性能。
+ * 
+ * + * @author 聂勇 + */ +public class ScriptRunPerformence { + + private final static int COUNT = 100000; + + /** + * 每次解释脚本并运行COUNT次。 + * + * @param script 脚本表达式 + * @param vars 用于替换script中变量的Key=>Value集合 + * @throws ScriptException 如果解释或运行脚本出错 + */ + public void parse(String script, Map vars) throws ScriptException { + ScriptEngine scriptEngine = getScriptEngine("javascript"); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < COUNT; i++) { + Bindings binds = createBinding(vars, scriptEngine); + scriptEngine.eval(script, binds); + } + long usedTime = System.currentTimeMillis() - startTime; + System.out.println( String.format("每次都解释脚本执行%d次消耗%d毫秒", COUNT, usedTime) ); + } + + /** + * 编译脚本后运行COUNT次。 + * + * @param script 脚本表达式 + * @param vars 用于替换script中变量的Key=>Value集合 + * @throws ScriptException 如果编译或运行脚本出错 + */ + public void compile(String script, Map vars) throws ScriptException { + ScriptEngine scriptEngine = getScriptEngine("javascript"); + Compilable compileEngine = (Compilable) scriptEngine; + CompiledScript compileScript = compileEngine.compile(script); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < COUNT; i++) { + Bindings binds = createBinding(vars, scriptEngine); + compileScript.eval(binds); + } + long usedTime = System.currentTimeMillis() - startTime; + System.out.println( String.format("编译脚本后执行%d次消耗%d毫秒", COUNT, usedTime) ); + } + + private ScriptEngine getScriptEngine(String name) { + ScriptEngineManager sem = new ScriptEngineManager(); + return sem.getEngineByName(name); + } + + private Bindings createBinding(Map vars, + ScriptEngine scriptEngine) { + Bindings binds = scriptEngine.createBindings(); + if (null != vars && !vars.isEmpty()) { + binds.putAll(vars); + } + + return binds; + } + + public static void main(String[] args) throws ScriptException { + if ( args.length != 1) { + System.err.println("参数错误。使用示例:\n java cn.aofeng.demo.script.ScriptRunPerformence [parse|compile]"); + System.exit(-1); + } + String runType = args[0]; + if ( !"parse".equals(runType) && !"compile".equals(runType) ) { + System.err.println("参数错误。使用示例:\n java cn.aofeng.demo.script.ScriptRunPerformence [parse|compile]"); + System.exit(-1); + } + + String script = "function run(a, b) {" + + "var c = a + b;" + + "var d = a * b;" + + "var e = a / b;" + + "var f = a % b;" + + "var g = a - b;" + + "var express = ((a * 5) > b || b * 10 >= 100) && (a * b > 99);" + + "}" + + "" + + "run(x, y);"; + Map vars = new HashMap(2); + vars.put("x", 20); + vars.put("y", 9); + + ScriptRunPerformence srp = new ScriptRunPerformence(); + if ("parse".equals(runType)) { + srp.parse(script, vars); + } else { + srp.compile(script, vars); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/script/SupportScriptEngine.java b/Java基础教程/Java源代码/codedemo/script/SupportScriptEngine.java new file mode 100644 index 00000000..a2b042b1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/script/SupportScriptEngine.java @@ -0,0 +1,36 @@ +package cn.aofeng.demo.script; + +import java.util.List; + +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; + +/** + * 支持的脚本引擎列表。 + * + * @author 聂勇 + */ +public class SupportScriptEngine { + + public void listScriptEngine() { + ScriptEngineManager sem = new ScriptEngineManager(); + List sefList = sem.getEngineFactories(); + for (ScriptEngineFactory factory : sefList) { + printScriptEngineInfo(factory); + } + } + + private void printScriptEngineInfo(ScriptEngineFactory factory) { + System.out.println("ScriptEngineName:" + factory.getEngineName() + + ", Names:" + factory.getNames() + + ", ScriptEngineVersion:" + factory.getEngineVersion() + + ", LanguageName:" + factory.getLanguageName() + + ", LanguageVersion:" + factory.getLanguageVersion() ); + } + + public static void main(String[] args) { + SupportScriptEngine msep = new SupportScriptEngine(); + msep.listScriptEngine(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/slf4j/HelloSlf4j.java b/Java基础教程/Java源代码/codedemo/slf4j/HelloSlf4j.java new file mode 100644 index 00000000..dce7ffdc --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/slf4j/HelloSlf4j.java @@ -0,0 +1,31 @@ +package cn.aofeng.demo.slf4j; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * slf4j使用示例。 + * + * @author 聂勇 + */ +public class HelloSlf4j { + + private static Logger _logger = LoggerFactory.getLogger(HelloSlf4j.class); + + public static void main(String[] args) { + // 直接输出字符串 + _logger.info("Hello, slf4j logger."); + + // 输出格式字符串(携带多个填充参数) + _logger.info("{} + {} = {}", 1, 2, (1+2)); + + // 输出错误信息和异常堆栈 + _logger.error("错误信息", new IOException("测试抛出IO异常信息")); + + // 输出错误信息(携带多个填充参数)和异常堆栈 + _logger.error("两个参数。agrs1:{};agrs2:{}的info级别日志", "args1", "args2", new IOException("测试抛出IO异常信息")); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/thread/DaemonThreadDemo.java b/Java基础教程/Java源代码/codedemo/thread/DaemonThreadDemo.java new file mode 100644 index 00000000..541d1334 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/thread/DaemonThreadDemo.java @@ -0,0 +1,45 @@ +/** + * 公司:阿里游戏 + * 创建时间:2018年11月2日下午5:56:43 + */ +package cn.aofeng.demo.thread; + +import java.util.Date; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 守护线程DEMO。 + * + * @author 聂勇 + */ +public class DaemonThreadDemo extends Thread { + + private static Logger logger = LoggerFactory.getLogger(DaemonThreadDemo.class); + + @Override + public void run() { + while (true) { + System.out.println("守护线程运行, 时间:" + new Date()); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.error("守护线程运行出错", e); + } + } + } + + public static void main(String[] args) { + DaemonThreadDemo thread = new DaemonThreadDemo(); + thread.setDaemon(true); + thread.start(); + + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + logger.error("主线程运行出错", e); + } + } + +} diff --git a/Java基础教程/Java源代码/codedemo/tree/PrettyTree.java b/Java基础教程/Java源代码/codedemo/tree/PrettyTree.java new file mode 100644 index 00000000..76aa3335 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/tree/PrettyTree.java @@ -0,0 +1,118 @@ +package cn.aofeng.demo.tree; + +import java.util.LinkedList; +import java.util.List; + +/** + * 输出类似目录树的结构。 + * + * @author 聂勇 + */ +public class PrettyTree { + + private final static String NODE_INDENT_PREFIX = "| "; + private final static String NODE_PARENT_PREFIX = "├── "; + private final static String NODE_LEAF_PREFIX = "└── "; + private final static String LINE_SEPARATOR = System.getProperty("line.separator", "\n"); + + public void renderRoot(Node root, StringBuilder buffer) { + renderParentNode(root, 0, buffer); + } + + private void renderParentNode(Node node, int indent, StringBuilder buffer) { + addIndent(indent, buffer); + buffer.append(NODE_PARENT_PREFIX) + .append(node.getName()) + .append(LINE_SEPARATOR); + List childList = node.getChild(); + if (null == childList) { + return; + } + for (Node child : childList) { + if (child.isLeaf()) { + renderLeafNode(child, indent+1, buffer); + } else { + renderParentNode(child, indent+1, buffer); + } + } + } + + private void renderLeafNode(Node node, int indent, StringBuilder buffer) { + addIndent(indent, buffer); + buffer.append(NODE_LEAF_PREFIX) + .append(node.getName()) + .append(LINE_SEPARATOR); + } + + private void addIndent(int indent, StringBuilder buffer) { + for (int i = 0; i < indent; i++) { + buffer.append(NODE_INDENT_PREFIX); + } + } + + public static class Node { + + /** 节点名称。 */ + private String name; + + /** 子节点列表 */ + private List child = new LinkedList(); + + public Node(String name) { + this.name = name; + } + + public Node(String name, List child) { + this.name = name; + this.child = child; + } + + /** + * 判断当前节点是否为叶子节点。 + * + * @return 如果是叶子节点返回true;否则返回false。 + */ + public boolean isLeaf() { + if (null == child || child.isEmpty()) { + return true; + } + + return false; + } + + /** + * 批量添加子节点。 + * + * @param child 子节点列表。 + */ + public void addAll(List nodeList) { + this.child.addAll(nodeList); + } + + /** + * 添加单个子节点。 + * + * @param node 子节点。 + */ + public void add(Node node) { + this.child.add(node); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChild() { + return child; + } + + public void setChild(List child) { + this.child = child; + } + } // end of Node + +} diff --git a/Java基础教程/Java源代码/codedemo/tree/PrettyTreeTest.java b/Java基础教程/Java源代码/codedemo/tree/PrettyTreeTest.java new file mode 100644 index 00000000..6a030752 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/tree/PrettyTreeTest.java @@ -0,0 +1,41 @@ +package cn.aofeng.demo.tree; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import cn.aofeng.demo.tree.PrettyTree.Node; + +public class PrettyTreeTest { + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testPrintRoot() { + Node p11 = new Node("p1-1"); + Node p12 = new Node("p1-2"); + Node p1 = new Node("p1"); + p1.add(p11); + p1.add(p12); + + Node p21 = new Node("p21"); + Node p2 = new Node("p2"); + p12.add(p21); + + Node root = new Node("Root"); + root.add(p1); + root.add(p2); + + StringBuilder buffer = new StringBuilder(256); + PrettyTree pt = new PrettyTree(); + pt.renderRoot(root, buffer); + System.out.print(buffer.toString()); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/util/DateUtil.java b/Java基础教程/Java源代码/codedemo/util/DateUtil.java new file mode 100644 index 00000000..473d5376 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/util/DateUtil.java @@ -0,0 +1,32 @@ +package cn.aofeng.demo.util; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +/** + * @author 聂勇 + */ +public class DateUtil { + + /** + * @return 返回当前时间的字符串(格式:"yyyy-MM-dd HH:mm:ss") + */ + public static String getCurrentTime() { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return dateFormat.format(new Date()); + } + + /** + * @return 下一分钟秒数为0的时间 + */ + public static Date getNextMinute() { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.MINUTE, 1); + cal.set(Calendar.SECOND, 0); + + return cal.getTime(); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/util/LogUtil.java b/Java基础教程/Java源代码/codedemo/util/LogUtil.java new file mode 100644 index 00000000..44558c91 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/util/LogUtil.java @@ -0,0 +1,18 @@ +package cn.aofeng.demo.util; + +/** + * 日志工具类。 + * + * @author 聂勇 + */ +public class LogUtil { + + public static void log(String msg) { + System.out.println(msg); + } + + public static void log(String msg, Object... param) { + log( String.format(msg, param) ); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/wiremock/HttpGetTest.java b/Java基础教程/Java源代码/codedemo/wiremock/HttpGetTest.java new file mode 100644 index 00000000..1629597c --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/wiremock/HttpGetTest.java @@ -0,0 +1,66 @@ +package cn.aofeng.demo.wiremock; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import cn.aofeng.demo.jetty.HttpGet; + +/** + * {@link HttpGet}的单元测试用例。 + * + * @author 聂勇 + */ +public class HttpGetTest { + + private HttpGet _httpGet = new HttpGet(); + + @Rule + public WireMockRule _wireMockRule = new WireMockRule(9191); + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + /** + * 用例:响应状态码为200且有响应内容。 + */ + @Test + public void testGetSomeThing4Success() throws Exception { + // 设置Mock + String response = "Hello, The World!"; + stubFor(get(urlEqualTo("/hello")) + .willReturn(aResponse() + .withStatus(200) + .withBody(response))); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertEquals(response, content); + } + + /** + * 用例:响应状态码为非200。 + */ + @Test + public void testGetSomeThing4Fail() throws Exception { + // 设置Mock + stubFor(get(urlEqualTo("/hello")) + .willReturn(aResponse() + .withStatus(500) + .withBody("Hello, The World!"))); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertNull(content); + } + +} diff --git a/Java基础教程/Java源代码/codedemo/wiremock/README.md b/Java基础教程/Java源代码/codedemo/wiremock/README.md new file mode 100644 index 00000000..e429c9e1 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/wiremock/README.md @@ -0,0 +1,131 @@ +使用WireMock实现Http Server Mock作单元测试 +=== +前一篇文章"[使用jetty实现Http Server Mock作单元测试](http://aofengblog.com/2014/05/06/WireMock-%E5%AE%9E%E7%8E%B0Http-Server-Mock%E7%94%A8%E4%BA%8E%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/)" +讲了用Jetty实现Http Server Mock来模拟依赖的外部HTTP服务系统,但如果需要更多的功能,如:区分GET和POST;匹配请求的URL;匹配Http Header;匹配请求内容等等。 +实际研发的过程中这些功能都有可能会用到,如果还是用Jetty来实现,需要自己不停地动手去添砖加瓦,虽然有成就感,但在快速迭代的节奏下不一定有足够的时间去做这些。 +这时我们需要一个现成的类库来满足我们这些需求,WireMock和MockServer都可以做到,这里只讲WireMock。 + +预备 +--- +* [JUnit 4.11](http://junit.org/) +* [wiremock 1.46](http://wiremock.org/) + +业务示例代码 +--- +[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/jetty/HttpGet.java) +```java +package cn.aofeng.demo.jetty; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.apache.commons.io.IOUtils; + +/** + * 抓取页面内容。 + * + * @author 聂勇 + */ +public class HttpGet { + + public String getSomeThing(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection urlConn = (HttpURLConnection) url.openConnection(); + urlConn.setConnectTimeout(3000); + urlConn.setRequestMethod("GET"); + urlConn.connect(); + + InputStream ins = null; + try { + if (200 == urlConn.getResponseCode()) { + ins = urlConn.getInputStream(); + ByteArrayOutputStream outs = new ByteArrayOutputStream(1024); + IOUtils.copy(ins, outs); + return outs.toString("UTF-8"); + } + } catch (IOException e) { + throw e; + } finally { + IOUtils.closeQuietly(ins); + } + + return null; + } + +} +``` + +单元测试代码 +--- +[源码下载](https://raw.githubusercontent.com/aofeng/JavaDemo/master/src/cn/aofeng/demo/wiremock/HttpGetTest.java) +```java +package cn.aofeng.demo.wiremock; + +import static org.junit.Assert.*; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import com.github.tomakehurst.wiremock.junit.WireMockRule; + +import cn.aofeng.demo.jetty.HttpGet; + +/** + * {@link HttpGet}的单元测试用例。 + * + * @author 聂勇 + */ +public class HttpGetTest { + + private HttpGet _httpGet = new HttpGet(); + + @Rule + public WireMockRule _wireMockRule = new WireMockRule(9191); + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + /** + * 用例:响应状态码为200且有响应内容。 + */ + @Test + public void testGetSomeThing4Success() throws Exception { + // 设置Mock + String response = "Hello, The World!"; + stubFor(get(urlEqualTo("/hello")) + .willReturn(aResponse() + .withStatus(200) + .withBody(response))); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertEquals(response, content); + } + + /** + * 用例:响应状态码为非200。 + */ + @Test + public void testGetSomeThing4Fail() throws Exception { + // 设置Mock + stubFor(get(urlEqualTo("/hello")) + .willReturn(aResponse() + .withStatus(500) + .withBody("Hello, The World!"))); + + String content = _httpGet.getSomeThing("http://localhost:9191/hello"); + assertNull(content); + } + +} +``` \ No newline at end of file diff --git a/Java基础教程/Java源代码/codedemo/xml/BookStore.xml b/Java基础教程/Java源代码/codedemo/xml/BookStore.xml new file mode 100644 index 00000000..9c956ade --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/xml/BookStore.xml @@ -0,0 +1,33 @@ + + + + + + Java并发编程实战 + Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea + 2012 + 69 + + + + + Java Concurrency in Practice + Brian Goetz / Tim Peierls / Joshua Bloch / Joseph Bowbeer / David Holmes / Doug Lea + 2006 + 59.99 + + + + 德文书 + 德国 + 2014 + 50 + + + + 没有lang属性 + 试试 + 2016 + + + diff --git a/Java基础教程/Java源代码/codedemo/xml/XPathDemo.java b/Java基础教程/Java源代码/codedemo/xml/XPathDemo.java new file mode 100644 index 00000000..39d07301 --- /dev/null +++ b/Java基础教程/Java源代码/codedemo/xml/XPathDemo.java @@ -0,0 +1,34 @@ +package cn.aofeng.demo.xml; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +/** + * XPath语法实践。 + * + * @author 聂勇 + */ +public class XPathDemo { + + public static void main(String[] args) throws XPathExpressionException { + InputSource ins = new InputSource(XPathDemo.class.getResourceAsStream("/cn/aofeng/demo/xml/BookStore.xml")); + XPathFactory factory = XPathFactory.newInstance(); + XPath xpath = factory.newXPath(); + NodeList result = (NodeList) xpath.evaluate("//title[@lang='de']", ins, XPathConstants.NODESET); + for (int i = 0; i < result.getLength(); i++) { + Node node = result.item(i); + StringBuilder buffer = new StringBuilder() + .append("NodeName=").append(node.getNodeName()).append(", ") + .append("NodeValue=").append(node.getNodeValue()).append(", ") + .append("Text=").append(node.getTextContent()); + System.out.println(buffer.toString()); + } + } + +} diff --git a/Java基础教程/Java标准库/41 Rest接口.md b/Java基础教程/Java网站开发/41 Rest接口.md similarity index 100% rename from Java基础教程/Java标准库/41 Rest接口.md rename to Java基础教程/Java网站开发/41 Rest接口.md diff --git a/Java基础教程/Java语言基础/04 Java数组.md b/Java基础教程/Java语言基础/04 Java数组.md index 298725ee..e6dd5168 100644 --- a/Java基础教程/Java语言基础/04 Java数组.md +++ b/Java基础教程/Java语言基础/04 Java数组.md @@ -7,7 +7,9 @@ - [数组参数](#数组参数) - [数组返回值](#数组返回值) - [多维数组](#多维数组) - - [Arrays类](#arrays类) + - [2 Arrays类](#2-arrays类) + - [方法概述](#方法概述) + - [具体方法](#具体方法) # Java 数组 ## 1 概述 @@ -120,7 +122,9 @@ s[1][2] = new String("!"); ``` -### Arrays类 +## 2 Arrays类 + +### 方法概述 * 给数组赋值:通过 fill 方法。 * 对数组排序:通过 sort 方法,按升序。 @@ -128,6 +132,8 @@ s[1][2] = new String("!"); * 查找数组元素:通过 binarySearch 方法能对排序好的数组进行二分查找法操作。 +### 具体方法 + * public static int binarySearch(Object[] a, Object key) 用二分查找算法在给定数组中搜索给定值的对象(Byte,Int,double等)。数组在调用前必须排序好的。如果查找值包含在数组中,则返回搜索键的索引;否则返回 (-(插入点) - 1)。 * public static boolean equals(long[] a, long[] a2) diff --git a/Java基础教程/Java语言基础/05 面向对象的类与对象.md b/Java基础教程/Java语言基础/05 面向对象的类与对象.md index c7278ccc..8c9c52e4 100644 --- a/Java基础教程/Java语言基础/05 面向对象的类与对象.md +++ b/Java基础教程/Java语言基础/05 面向对象的类与对象.md @@ -6,6 +6,7 @@ - [2 成员变量Field](#2-成员变量field) - [变量类型](#变量类型) - [成员变量](#成员变量) + - [初始化顺序](#初始化顺序) - [3 成员方法Method](#3-成员方法method) - [4 构造方法Constructor](#4-构造方法constructor) - [构造方法](#构造方法) @@ -19,7 +20,6 @@ - [final](#final) - [this](#this) - [super](#super) - - [static](#static) - [instanceof](#instanceof) # 类与对象 ## 1 基本内容 @@ -131,6 +131,26 @@ public class Book{ 成员变量可以设置初始值,也可以不设置,如果不设置初始值,则会有默认值。 +### 初始化顺序 + +Java类中各元素的初始化顺序 初始化的原则是: + +先初始化静态部分,再初始化动态部分;(先静再动) +先初始化父类部分,后初始化子类部分;(先父再子) +先初始化变量,次初始化代码块,再初始化构造器;(先变量,次代码块,再构造器) +所以依照这个规则可以得出总体顺序是: +``` +1.父类的静态成员变量(第一次加载类时):父静成 +2.父类的静态代码块(第一次加载类时):父静块 +3.子类的静态成员变量(第一次加载类时):子静成 +4.子类的静态代码块(第一次加载类时):子静块 +5.父类的普通成员变量:父成 +6.父类的动态代码块:父块 +7.父类的构造器方法:父构 +8.子类的普通成员变量:子成 +9.子类的动态代码块:子块 +10.子类的构造器方法:子构 +``` ## 3 成员方法Method @@ -336,9 +356,7 @@ public class SuperExtendExample extends SuperExample { } ``` -### static -static方法和static变量和类加载器同时加载到内存中,存储在静态方法区。 ### instanceof diff --git a/Java基础教程/Java语言基础/10 Java泛型.md b/Java基础教程/Java语言基础/10 Java泛型.md index 2cc029a5..4c5907da 100644 --- a/Java基础教程/Java语言基础/10 Java泛型.md +++ b/Java基础教程/Java语言基础/10 Java泛型.md @@ -12,12 +12,13 @@ - [泛型接口](#泛型接口) - [泛型方法](#泛型方法) - [4 泛型通配符](#4-泛型通配符) - - [5 泛型中的KTVE](#5-泛型中的ktve) + - [5 泛型中的 KTVE](#5-泛型中的-ktve) - [6 泛型的实现原理](#6-泛型的实现原理) - # 泛型机制 + > [Java 泛型详解](https://blog.csdn.net/ChenRui_yz/article/details/122935621) + ## 1 泛型概述 ### 基本概念 @@ -28,7 +29,8 @@ Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了 ![](image/2022-07-11-22-24-04.png) -与C#中的泛型相比,Java的泛型可以算是“伪泛型”了。在C#中,不论是在程序源码中、在编译后的中间语言,还是在运行期泛型都是真实存在的。Java则不同,Java的泛型只在源代码存在,只供编辑器检查使用,编译后的字节码文件已擦除了泛型类型,同时在必要的地方插入了强制转型的代码。 +与 C#中的泛型相比,Java 的泛型可以算是“伪泛型”了。在 C#中,不论是在程序源码中、在编译后的中间语言,还是在运行期泛型都是真实存在的。Java 则不同,Java 的泛型只在源代码存在,只供编辑器检查使用,编译后的字节码文件已擦除了泛型类型,同时在必要的地方插入了强制转型的代码。 + ### 泛型的基本用法 ```java @@ -41,15 +43,18 @@ public class Box { ``` ## 2 优势 + 1. 安全性? 2. 消除转换? 3. 提升性能? 4. 重用行。不同类型不需要重载。泛型本质上也是一种编译时多态。 ### 安全性 + 在没有泛型之前,从集合中读取到的每一个对象都必须进行类型转换,如果不小心插入了错误的类型对象,在运行时的转换处理就会出错。 -* 没有泛型的情况下使用集合: +- 没有泛型的情况下使用集合: + ```java public static void noGeneric() { ArrayList names = new ArrayList(); @@ -57,7 +62,9 @@ names.add("mikechen的互联网架构"); names.add(123); //编译正常 } ``` -* 有泛型的情况下使用集合: + +- 有泛型的情况下使用集合: + ```java public static void useGeneric() { ArrayList names = new ArrayList<>(); @@ -65,20 +72,23 @@ names.add("mikechen的互联网架构"); names.add(123); //编译不通过 } ``` + 相当于告诉编译器每个集合接收的对象类型是什么,编译器在编译期就会做类型检查,告知是否插入了错误类型的对象,使得程序更加安全,增强了程序的健壮性。 - ### 消除强制转换 + 泛型的一个附带好处是,消除源代码中的许多强制类型转换,这使得代码更加可读,并且减少了出错机会。 +- 以下没有泛型的代码段需要强制转换: -* 以下没有泛型的代码段需要强制转换: ```java List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); ``` -* 当重写为使用泛型时,代码不需要强制转换: + +- 当重写为使用泛型时,代码不需要强制转换: + ```java List list = new ArrayList(); list.add("hello"); @@ -90,48 +100,56 @@ String s = list.get(0); // no cast ### 避免了不必要的装箱、拆箱操作,提高程序的性能 -在非泛型编程中,将简单类型作为Object传递时会引起Boxing(装箱)和Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行Boxing和Unboxing操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。 +在非泛型编程中,将简单类型作为 Object 传递时会引起 Boxing(装箱)和 Unboxing(拆箱)操作,这两个过程都是具有很大开销的。引入泛型后,就不必进行 Boxing 和 Unboxing 操作了,所以运行效率相对较高,特别在对集合操作非常频繁的系统中,这个特点带来的性能提升更加明显。 + +- 泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。 -* 泛型变量固定了类型,使用的时候就已经知道是值类型还是引用类型,避免了不必要的装箱、拆箱操作。 ```java object a=1;//由于是object类型,会自动进行装箱操作。 - + int b=(int)a;//强制转换,拆箱操作。这样一去一来,当次数多了以后会影响程序的运行效率。 ``` -* 使用泛型之后 + +- 使用泛型之后 + ```java public static T GetValue(T a) - + {   return a; } - + public static void Main() - + {   int b=GetValue(1);//使用这个方法的时候已经指定了类型是int,所以不会有装箱和拆箱的操作。 } ``` ### 提高了代码的重用行 -> 省略 + +- 显而易见,能够通过泛型代替部分重载,大大提升了可用性。 ## 3 泛型的使用 +都是通过尖括号定义的 + ### 泛型类 -* 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔: + +- 定义泛型类,在类名后添加一对尖括号,并在尖括号中填写类型参数,参数可以有多个,多个参数使用逗号分隔: + ```java public class 类名 <泛型类型1,...> {} public class GenericClass {} ``` -* 实例代码 +- 实例代码 ```java public class GenericClass { private T value; - - + + public GenericClass(T value) { this.value = value; } @@ -143,26 +161,132 @@ public class GenericClass { } } ``` -### 泛型接口 + +- **在创建对象的时候确定泛型** + +例如,`ArrayList list = new ArrayList();` + +此时,变量 E 的值就是 String 类型,那么我们的类型就可以理解为: ```java -public interface GenericInterface { -void show(T value); -} +class ArrayList{ + public boolean add(String e){ } -public class StringShowImpl implements GenericInterface { -@Override -public void show(String value) { -System.out.println(value); -}} - -public class NumberShowImpl implements GenericInterface { -@Override -public void show(Integer value) { -System.out.println(value); -}} + public String get(int index){ } + ... +} ``` +- 实例 + +举例自定义泛型类 + +```java +public class MyGenericClass { + //没有MVP类型,在这里代表 未知的一种数据类型 未来传递什么就是什么类型 + private MVP mvp; + + public void setMVP(MVP mvp) { + this.mvp = mvp; + } + + public MVP getMVP() { + return mvp; + } +} +``` + +使用: + +```java +public class GenericClassDemo { + public static void main(String[] args) { + // 创建一个泛型为String的类 + MyGenericClass my = new MyGenericClass(); + // 调用setMVP + my.setMVP("大胡子哈登"); + // 调用getMVP + String mvp = my.getMVP(); + System.out.println(mvp); + //创建一个泛型为Integer的类 + MyGenericClass my2 = new MyGenericClass(); + my2.setMVP(123); + Integer mvp2 = my2.getMVP(); + } +} +``` + +### 泛型接口 + +定义格式: + +``` +修饰符 interface接口名<代表泛型的变量> { } +``` + +例如, + +```java +public interface MyGenericInterface{ + public abstract void add(E e); + + public abstract E getE(); +} +``` + +使用格式: + +**1、定义类时确定泛型的类型** + +例如 + +```java +public class MyImp1 implements MyGenericInterface { + @Override + public void add(String e) { + // 省略... + } + + @Override + public String getE() { + return null; + } +} +``` + +此时,泛型 E 的值就是 String 类型。 + +**2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型** + +例如 + +```java +public class MyImp2 implements MyGenericInterface { + @Override + public void add(E e) { + // 省略... + } + + @Override + public E getE() { + return null; + } +} +``` + +确定泛型: + +```java +/* + * 使用 + */ +public class GenericInterface { + public static void main(String[] args) { + MyImp2 my = new MyImp2(); + my.add("aa"); + } +} +``` ### 泛型方法 @@ -187,40 +311,57 @@ return t; } ``` +- 使用格式:**调用方法时,确定泛型的类型** + +```java +public class GenericMethodDemo { + public static void main(String[] args) { + // 创建对象 + MyGenericMethod mm = new MyGenericMethod(); + // 演示看方法提示 + mm.show("aaa"); + mm.show(123); + mm.show(12.45); + } +} +``` + ## 4 泛型通配符 -Java泛型的通配符是用于解决泛型之间引用传递问题的特殊语法。泛型与继承之间的关系 +Java 泛型的通配符是用于解决泛型之间引用传递问题的特殊语法。泛型与继承之间的关系 + 1. 无边界通配符 1. 无边界的通配符的主要作用就是让泛型能够接受未知类型的数据. 2. 固定上边界的通配符 1. 使用固定上边界的通配符的泛型, 就能够接受指定类及其子类类型的数据。 - 2. 要声明使用该类通配符, 采用的形式, 这里的E就是该泛型的上边界。 - 3. 注意: 这里虽然用的是extends关键字, 却不仅限于继承了父类E的子类, 也可以代指显现了接口E的类 + 2. 要声明使用该类通配符, 采用的形式, 这里的 E 就是该泛型的上边界。 + 3. 注意: 这里虽然用的是 extends 关键字, 却不仅限于继承了父类 E 的子类, 也可以代指显现了接口 E 的类 3. 固定下边界的通配符 1. 使用固定下边界的通配符的泛型, 就能够接受指定类及其父类类型的数据.。 - 2. 要声明使用该类通配符, 采用的形式, 这里的E就是该泛型的下边界.。 + 2. 要声明使用该类通配符, 采用的形式, 这里的 E 就是该泛型的下边界.。 3. 注意: 你可以为一个泛型指定上边界或下边界, 但是不能同时指定上下边界。 + ```java //表示类型参数可以是任何类型 public class Apple{} - + //表示类型参数必须是A或者是A的子类 public class Apple{} - + //表示类型参数必须是A或者是A的超类型 public class Apple{} ``` -## 5 泛型中的KTVE +## 5 泛型中的 KTVE 泛型中的规范 -* T:任意类型 type -* E:集合中元素的类型 element -* K:key-value形式 key -* V: key-value形式 value -* N: Number(数值类型) -* ?: 表示不确定的java类型 +- T:任意类型 type +- E:集合中元素的类型 element +- K:key-value 形式 key +- V: key-value 形式 value +- N: Number(数值类型) +- ?: 表示不确定的 java 类型 ## 6 泛型的实现原理 @@ -228,4 +369,4 @@ public class Apple{} 实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。 -实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。 \ No newline at end of file +实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换,这一个过程就叫做『泛型翻译』。 diff --git a/Java基础教程/Java语言基础/18 ClassLoader机制.md b/Java基础教程/Java语言基础/18 ClassLoader机制.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Java基础教程/Java语言基础/19 staic关键字.md b/Java基础教程/Java语言基础/19 staic关键字.md new file mode 100644 index 00000000..d05efd93 --- /dev/null +++ b/Java基础教程/Java语言基础/19 staic关键字.md @@ -0,0 +1,274 @@ +# static + +## 1 静态变量 + +- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 +- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 +- static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。所以我们一般在这两种情况下使用静态变量:对象之间共享数据、访问方便。 + + +```java +public class A { + + private int x; // 实例变量 + private static int y; // 静态变量 + + public static void main(String[] args) { + // int x = A.x; // Non-static field 'x' cannot be referenced from a static context + A a = new A(); + int x = a.x; + int y = A.y; + } +} +``` + +## 2 静态方法 + +静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 + + +```java +public abstract class A { + public static void func1(){ + } + // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' +} +``` + +只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因为这两个关键字与具体对象关联。 + +```java +public class A { + + private static int x; + private int y; + + public static void func1(){ + int a = x; + // int b = y; // Non-static field 'y' cannot be referenced from a static context + // int b = this.y; // 'A.this' cannot be referenced from a static context + } +} +``` + +## 3 静态语句块 + +静态语句块在类初始化时运行一次。 + +```java +public class A { + static { + System.out.println("123"); + } + + public static void main(String[] args) { + A a1 = new A(); + A a2 = new A(); + } +} +``` + +```html +123 +``` + +## 4 静态内部类 + +非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。 + +```java +public class OuterClass { + + class InnerClass { + } + + static class StaticInnerClass { + } + + public static void main(String[] args) { + // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context + OuterClass outerClass = new OuterClass(); + InnerClass innerClass = outerClass.new InnerClass(); + StaticInnerClass staticInnerClass = new StaticInnerClass(); + } +} +``` + +静态内部类不能访问外部类的非静态的变量和方法。 + +  内部类一般情况下使用不是特别多,如果需要在外部类里面定义一个内部类,通常是基于外部类和内部类有很强关联的前提下才去这么使用。 + +   在说静态内部类的使用场景之前,我们先来看一下静态内部类和非静态内部类的区别: + +   **非静态内部类对象持有外部类对象的引用(编译器会隐式地将外部类对象的引用作为内部类的构造器参数);而静态内部类对象不会持有外部类对象的引用** + +   由于非静态内部类的实例创建需要有外部类对象的引用,所以非静态内部类对象的创建必须依托于外部类的实例;而静态内部类的实例创建只需依托外部类; + +   并且由于非静态内部类对象持有了外部类对象的引用,因此非静态内部类可以访问外部类的非静态成员;而静态内部类只能访问外部类的静态成员; + + + +   两者的根本性区别其实也决定了用static去修饰内部类的真正意图: + + - 内部类需要脱离外部类对象来创建实例 + - 避免内部类使用过程中出现内存溢出 + +    + + 第一种是目前静态内部类使用比较多的场景,比如JDK集合中的Entry、builder设计模式。 + +  HashMap Entry: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200518164800.png) + +  builder设计模式: + +```java +public class Person { + private String name; + private int age; + + private Person(Builder builder) { + this.name = builder.name; + this.age = builder.age; + } + + public static class Builder { + + private String name; + private int age; + + public Builder() { + } + + public Builder name(String name) { + this.name = name; + return this; + } + public Builder age(int age) { + this.age=age; + return this; + } + + public Person build() { + return new Person(this); + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} + +// 在需要创建Person对象的时候 +Person person = new Person.Builder().name("张三").age(17).build(); +``` + +第二种情况一般出现在多线程场景下,非静态内部类可能会引发内存溢出的问题,比如下面的例子: + +```java +public class Task { + + public void onCreate() { + // 匿名内部类, 会持有Task实例的引用 + new Thread() { + public void run() { + //...耗时操作 + }; + }.start(); + } +} +``` + + 上面这段代码中的: + +```java +new Thread() { + public void run() { + //...耗时操作 + }; +}.start(); +``` + +  声明并创建了一个匿名内部类对象,该对象持有外部类Task实例的引用,如果在在run方法中做的是耗时操作,将会导致外部类Task的实例迟迟不能被回收,如果Task对象创建过多,会引发内存溢出。 + +​ 优化方式: + +```java +public class Task { + + public void onCreate() { + SubTask subTask = new SubTask(); + subTask.start(); + } + + static class SubTask extends Thread { + @Override + public void run() { + //...耗时操作 + } + + } +} +``` + +## 5 静态导包 + +在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 + +```java +import static com.xxx.ClassName.* +``` + +## 6 初始化顺序 + +静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。 + +```java +public static String staticField = "静态变量"; +``` + +```java +static { + System.out.println("静态语句块"); +} +``` + +```java +public String field = "实例变量"; +``` + +```java +{ + System.out.println("普通语句块"); +} +``` + +最后才是构造函数的初始化。 + +```java +public InitialOrderTest() { + System.out.println("构造函数"); +} +``` + +存在继承的情况下,初始化顺序为: + +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) \ No newline at end of file diff --git a/Java基础教程/Java语言基础/20 final关键字.md b/Java基础教程/Java语言基础/20 final关键字.md new file mode 100644 index 00000000..38c40483 --- /dev/null +++ b/Java基础教程/Java语言基础/20 final关键字.md @@ -0,0 +1,27 @@ + +# final + +## 1. 数据 + +声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 + +- 对于基本类型,final 使数值不变; +- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 +- 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。 +```java +final int x = 1; +// x = 2; // cannot assign value to final variable 'x' +final A y = new A(); +y.a = 1; +``` + +## 2. 方法 + +声明方法不能被子类重写。当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。 + +private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 + +## 3. 类 + +声明类不允许被继承。 + diff --git a/Java基础教程/Java语言基础/21 Java类加载机制.md b/Java基础教程/Java语言基础/21 Java类加载机制.md new file mode 100644 index 00000000..c8fd4860 --- /dev/null +++ b/Java基础教程/Java语言基础/21 Java类加载机制.md @@ -0,0 +1,89 @@ +# Java类加载机制 +> https://blog.csdn.net/qq_29167297/article/details/124800850 +## 0 JVM简介 +### JVM空间 +JVM内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。 + +![](image/2022-12-04-13-14-56.png) + +### ClassLoader + +![](image/2022-12-04-13-15-46.png) +## 1 加载 +加载阶段是将class文件从磁盘或者jar等读到JVM内存中,并为其创建一个Class对象。任何一个类被使用时候系统都会为其创建一个Class对象的。加载的同时将加载的这些数据转换成方法区中运行时数据(运行时候数据区:静态变量、静态代码块、常量池等),作为方法区数据的访问入口。 + +加载是类加载的第一个过程,在这个阶段,将完成以下三件事情: + +1. 通过一个类的全限定名获取该类的二进制流。 +2. 将该二进制流中的静态存储结构转化为方法去运行时数据结构。 +3. 在内存中生成该类的Class对象,作为该类的数据访问入口。 + + +事实上,这三条限定都不是很严格,比如第一条,并没有明确指出通过全限定名从哪里得到二进制流,由此就有很多不同的实现: +* 在ZIP包中读取(JAR,EAR,WAR) +* 从网络中获取(APPLET) +* 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口申城$Proxy的代理类的二进制流 +* 由其它文件生成(jsp) +* 从数据库中读取,有些中间件服务器(SAP NETWEAVER) + +加载阶段完成后,虚拟机外部的二进制流就按照虚拟机所需的格式存储在方法区中,方法区中的数据存储格式由虚拟机实现自行定义。然后在JAVA堆中实例化一个java.lang.Class类对象(比如我们new A()对象,在加载过程中,会在堆区生成一个代表A类的java.lang.Class类的对象,而new所产生的对象,是依靠A类的java.lang.class对象为模板产生的新的对象到堆区),这个对象将作为程序访问方法区中的这些类型数据的外部接口。加载阶段与连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。 + +## 2 验证 + +验证目的是为了确保Class文件的字节流中的信息不会危害到虚拟机,在该阶段主要完成以下四种验证: + +1. 文件格式验证:验证字节流是否符合Class文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型。 +2. 元数据验证:对字节码描述的信息进行语义分析,如这个类中是否有父类,是否集成了不被继承的类等。 +3. 字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。如:方法中的类型转换是否正确,跳转指令是否正确等。 +4. 符号引用验证:这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。 + +## 3 准备 + +准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在Java堆中。如果该变量被final修饰,将在编译时生成ConstantValue,这样在准备阶段将直接设置成该初值。 +```java +public static int value=123;//在准备阶段value初始值为0,初始化阶段才变为123。 +``` + +### 4 解析 + +该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。 + +* 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。其实就是将堆内存空间里的静态变量符号修改为已经申请了空间的静态变量地址的过程 + * 符号引用:(Symbolic References)符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,引用的目标并不一定已经加载到内存中,与虚拟机内存布局无关。 + * 直接引用:(Direct References)直接引用可以是直接指向目标的指针,相对偏移量,或是一个能间接定位到目标的句柄。与虚拟机内存布局相关。 + + + +### 5 初始化 + +初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。 + +初始化阶段是执行类构造器()方法的过程 + +1. ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并而成。编译器收集的顺序和语句在源文件中出现的顺序一致,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,只能赋值,不能访问 +2. ()方法与类的构造函数()不同,不需要显式的调用父类构造器,虚拟机会保证父类的()在子类的之前完成。因此,虚拟机执行的第一个()方法肯定是java.lang.Object. +3. 由于父类()方法先执行,也就意味着父类中定义的静态语句要优先于子类的变量赋值操作。 +4. ()方法并不是必须的,如果一个类没有静态语句块也没有对变量赋值操作,就不会生成 +5. 接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成()方法,但与类不同的是,接口的()方法不需要执行父接口的()方法。只有当父几口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的()方法。 +6. 虚拟机会保证一个类的()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。 + +### 触发初始化 + +虚拟机规范严格规定了有且只有四种情况必须对类进行初始化(加载,验证,准备自动在之前开始) + +1. 遇到new,getstatic,putstatic,invokestatic这4条字节码指令时,如果类没有进行初始化,则先初始化。这4个字节码常见的出现场景是:使用new关键字实例化对象的时候,读取或设置静态字段(被final修饰,已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。 +2. 反射调用时 +3. 初始化一个类时,如果其父类还未初始化,则先出发父类初始化。 +4. 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类 + + +这4种情况称为对类的主动引用,其他情况称为被动引用。一下四种情况不会触发初始化 + +1. 对于访问静态字段,只有直接定义这个字段的类才被初始化,因此通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。但是对于HOTSPOT,会触发子类的加载。 +2. 通过数组定义引用类,不会触发此类的初始化。 +3. 常量在编译阶段会存入调用类的常量池,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。 +4. 接口的加载和类加载过程稍有不同,接口不能有static代码段,但接口中还是会生成()类构造器,用于初始化接口中所定义的成员变量。 一个接口在初始化时,并不要求其父类也初始化了。 + +### 补充说明 +* 只有当某个类初始化之后,才会调用类的静态代码块。 +* 初始化过程一方面是唯一的,另一方面是线程安全的。所以通过静态语句块的单例模式非常合理。 \ No newline at end of file diff --git a/Java基础教程/Java语言基础/22 Java包机制.md b/Java基础教程/Java语言基础/22 Java包机制.md new file mode 100644 index 00000000..66d592f5 --- /dev/null +++ b/Java基础教程/Java语言基础/22 Java包机制.md @@ -0,0 +1,43 @@ +## 包 +包我们每天建的项目就是在一个目录下,我们每次都会建立一个包,这个包在磁盘下其实就是一个目录。**包是用来分门别类的管理技术,不同的技术类放在不同的包下**,方便管理和维护。 + +**包名的命名规范**: + +``` +路径名.路径名.xxx.xxx +// 例如:com.github.krislinzhao +``` + +- 包名一般是公司域名的倒写。例如:黑马是www.github.com,包名就可以定义成com.github.技术名称。 +- 包名必须用''."连接。 +- 包名的每个路径名必须是一个合法的标识符,而且不能是Java的关键字。 + +## 2 权限修饰符 + +在Java中提供了四种访问权限,使用不同的访问权限修饰符修饰时,被修饰的内容会有不同的访问权限,我们之前已经学习过了public 和 private,接下来我们研究一下protected和缺省(default默认)修饰符的作用。 + +- public:公共的,所有地方都可以访问。 +- protected:当前类 ,当前包,当前类的子类可以访问。 +- 缺省(没有修饰符):当前类 ,当前包可以访问。 +- private:私有的,当前类可以访问。 + `public > protected > 缺省 > private` + +## 3 不同权限的访问能力 + + +| | public | protected | 缺省(空的) | private | +| ---------------- | ------ | --------- | ------------ | ------- | +| 同一类中 | √ | √ | √ | √ | +| 同一包中的类 | √ | √ | √ | | +| 不同包的子类 | √ | √ | | | +| 不同包中的无关类 | √ | | | | + +可见,public具有最大权限。private则是最小权限。 + +编写代码时,如果没有特殊的考虑,建议这样使用权限: + +- 成员变量使用`private` ,隐藏细节。 +- 构造方法使用` public` ,方便创建对象。 +- 成员方法使用`public` ,方便调用方法。 + +> 小贴士:不加权限修饰符,就是default权限 \ No newline at end of file diff --git a/Java基础教程/Java语言基础/image/2022-12-04-13-14-56.png b/Java基础教程/Java语言基础/image/2022-12-04-13-14-56.png new file mode 100644 index 00000000..672eb1d8 Binary files /dev/null and b/Java基础教程/Java语言基础/image/2022-12-04-13-14-56.png differ diff --git a/Java基础教程/Java语言基础/image/2022-12-04-13-15-46.png b/Java基础教程/Java语言基础/image/2022-12-04-13-15-46.png new file mode 100644 index 00000000..3bdcaa19 Binary files /dev/null and b/Java基础教程/Java语言基础/image/2022-12-04-13-15-46.png differ diff --git a/Java基础教程/Java集合类/01 Java数据结构.md b/Java基础教程/Java集合类/01 Java数据结构.md new file mode 100644 index 00000000..7f056f60 --- /dev/null +++ b/Java基础教程/Java集合类/01 Java数据结构.md @@ -0,0 +1,267 @@ + +# 数据结构 + +## 1 线性数据结构 +数据存储的常用结构有:栈、队列、数组、链表和红黑树。我们分别来了解一下: + +### 栈 + +- **栈**:**stack**,又称堆栈,它是运算受限的线性表,其限制是仅允许在标的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。 + +简单的说:采用该结构的集合,对元素的存取有如下的特点 + +- 先进后出(即,存进去的元素,要在后它后面的元素依次取出后,才能取出该元素)。例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。 + +- 栈的入口、出口的都是栈的顶端位置。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613144909.png) + +这里两个名词需要注意: + +- **压栈**:就是存元素。即,把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。 +- **弹栈**:就是取元素。即,把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。 + +### 队列 + +- **队列**:**queue**,简称队,它同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。 + +简单的说,采用该结构的集合,对元素的存取有如下的特点: + +- 先进先出(即,存进去的元素,要在后它前面的元素依次取出后,才能取出该元素)。例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。 +- 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613145012.bmp) + +### 数组 + +- **数组**:**Array**,是有序的元素序列,数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。 + +简单的说,采用该结构的集合,对元素的存取有如下的特点: + +- 查找元素快:通过索引,可以快速访问指定位置的元素 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613144058.png) + +- 增删元素慢 + +- **指定索引位置增加元素**:需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613144218.png) + +- **指定索引位置删除元素:**需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置元素不复制到新数组中。如下图 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613144337.png) + +### 链表 + +- **链表**:**linked list**,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。我们常说的链表结构有单向链表与双向链表,那么这里给大家介绍的是**单向链表**。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613144444.png) + +简单的说,采用该结构的集合,对元素的存取有如下的特点: + +- 多个结点之间,通过地址进行连接。例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613145131.png) + +- 查找元素慢:想查找某个元素,需要通过连接的节点,依次向后查找指定元素 + +- 增删元素快: + + * 增加元素:只需要修改连接下个元素的地址即可。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613145412.png) + + * 删除元素:只需要修改连接下个元素的地址即可。 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613145458.bmp) + + +## 2 树型数据结构 + +树具有的特点: + +1. 每一个节点有零个或者多个子节点 + +2. 没有父节点的节点称之为根节点,**一个树最多有一个根节点。** + +3. 每一个非根节点有且只有一个父节点 + + ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613145927.png) + +| 名词 | 含义 | +| -------- | ------------------------------------------------------------ | +| 节点 | 指树中的一个元素 | +| 节点的度 | 节点拥有的子树的个数,二叉树的度不大于2 | +| 叶子节点 | 度为0的节点,也称之为终端结点 | +| 高度 | 叶子结点的高度为1,叶子结点的父节点高度为2,以此类推,根节点的高度最高 | +| 层 | 根节点在第一层,以此类推 | +| 父节点 | 若一个节点含有子节点,则这个节点称之为其子节点的父节点 | +| 子节点 | 子节点是父节点的下一层节点 | +| 兄弟节点 | 拥有共同父节点的节点互称为兄弟节点 | + +### 二叉树 + +如果树中的每个节点的子节点的个数不超过2,那么该树就是一个二叉树。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150057.png) + +### 二叉查找树/二叉排序树 + +二叉查找树的特点: + +1. 左子树上所有的节点的值均小于等于他的根节点的值 +2. 右子树上所有的节点值均大于或者等于他的根节点的值 +3. 每一个子节点最多有两个子树 + +案例演示(20,18,23,22,17,24,19)数据的存储过程; + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150149.png) + +**增删改查的性能都很高!!!** + +遍历获取元素的时候可以按照"左中右"的顺序进行遍历; + +注意:二叉查找树存在的问题:会出现"瘸子"的现象,影响查询效率。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150220.png) + +### 平衡二叉树 + +(基于查找二叉树,但是让树不要太高,尽量让树的元素均衡分布。这样综合性能就高了) + +#### 概述 + +为了避免出现"瘸子"的现象,减少树的高度,提高我们的搜素效率,又存在一种树的结构:"平衡二叉树" + +规则:**它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树** + +如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150348.png) + +如下图所示,左图是一棵平衡二叉树,根节点10,左右两子树的高度差是1,而右图,虽然根节点左右两子树高度差是0,但是右子树15的左右子树高度差为2,不符合定义, + +所以右图不是一棵平衡二叉树。 + +#### 旋转 + +在构建一棵平衡二叉树的过程中,当有新的节点要插入时,检查是否因插入后而破坏了树的平衡,如果是,则需要做旋转去改变树的结构。 + +左旋: + +**左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点;** + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150534.png) + +右旋: + +**将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点** + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150600.png) + +举个例子,像上图是否平衡二叉树的图里面,左图在没插入前"19"节点前,该树还是平衡二叉树,但是在插入"19"后,导致了"15"的左右子树失去了"平衡", + +所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右子树平衡,而"15"节点没有子树,左右也平衡了。如下图, + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150700.png) + +由于在构建平衡二叉树的时候,当有**新节点插入**时,都会判断插入后时候平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后, + +有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作**左左,左右,右左,右右**。 + +##### 左左 + +左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7"的左子树"4",插入了节点"5"或"3"导致失衡。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150825.png) + + + +左左调整其实比较简单,只需要对节点进行右旋即可,如下图,对节点"10"进行右旋, + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613150922.png) + + + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151000.png) + + + +##### 左右 + +左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的左子树"7"的右子树"9", + +插入了节点"10"或"8"导致失衡。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151051.png) + +左右的调整就不能像左左一样,进行一次旋转就完成调整。我们不妨先试着让左右像左左一样对"11"节点进行右旋,结果图如下,右图的二叉树依然不平衡,而右图就是接下来要 + +讲的右左,即左右跟右左互为镜像,左左跟右右也互为镜像。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151113.png) + + + +左右这种情况,进行一次旋转是不能满足我们的条件的,正确的调整方式是,将左右进行第一次旋转,将左右先调整成左左,然后再对左左进行调整,从而使得二叉树平衡。 + +即先对上图的节点"7"进行左旋,使得二叉树变成了左左,之后再对"11"节点进行右旋,此时二叉树就调整完成,如下图: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151248.png) + + + +##### 右左 + +右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如上即为"11"节点的右子树"15"的左子树"13", + +插入了节点"12"或"14"导致失衡。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151443.png) + + + +前面也说了,右左跟左右其实互为镜像,所以调整过程就反过来,先对节点"15"进行右旋,使得二叉树变成右右,之后再对"11"节点进行左旋,此时二叉树就调整完成,如下图: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151525.png) + + + +##### 右右 + +右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点 + +"14"或"19"导致失衡。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151608.png) + + + +右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151638.png) + + + +### 红黑树 + +就是平衡的二叉查找树!! + +红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构,它是在1972年由Rudolf Bayer发明的,当时被称之为平衡二叉B树,后来,在1978年被Leoj.Guibas和Robert Sedgewick修改为如今的"红黑树"。它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,可以是红或者黑; + +红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的; + +红黑树的特性: + +1. 每一个节点或是红色的,或者是黑色的。 +2. 根节点必须是黑色 +3. 每个叶节点(Nil)是黑色的;(如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点) +4. 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况) +5. 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点; + +如下图所示就是一个 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613151901.png) + +在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则; \ No newline at end of file diff --git a/Java基础教程/Java集合类/02 JavaCollection.md b/Java基础教程/Java集合类/02 JavaCollection.md new file mode 100644 index 00000000..1483cde1 --- /dev/null +++ b/Java基础教程/Java集合类/02 JavaCollection.md @@ -0,0 +1,64 @@ +## 准备知识 +数据结构分为 + +* 线性数据结构 +* 树型数据结构 +* 图型数据结构 + + +C++中的容器分为(都是线性的) +* 顺序容器 + * array 数组 + * vector向量 + * list 链表 +* 关联容器 + * map 映射 + * set 集合 +* 容器适配器 + * stack 栈 + * queue 队列 + + +Java中的容器分为(都是线性的) +* 集合collection + * List + * Queue + * Set + * Map + +![](image/2022-11-08-10-51-54.png) + +![](image/2022-11-08-10-54-19.png) + +![](image/2022-12-04-22-53-11.png) +## 体系 + ++ [Java 集合 - `List`](2.md) + + [`ArrayList`](3.md) + + [链表](47.md) + + [`Vector`](81.md) ++ [Java 集合 - `Set`](102.md) + + [`HashSet`](103.md) + + [`LinkedHashSet`](111.md) + + [`TreeSet`](114.md) ++ [Java 集合 - `Map`](117.md) + + [`HashMap`](118.md) + + [`TreeMap`](142.md) + + [`LinkedHashMap`](148.md) ++ [Java 集合 - `Iterator`/`ListIterator`](152.md) ++ [`Comparable`和`Comparator`接口](155.md) ++ [集合面试问题](158.md) + + +## 集合框架总览 + + +1. 集合框架提供了两个遍历接口:`Iterator`和`ListIterator`,其中后者是前者的`优化版`,支持在任意一个位置进行**前后双向遍历**。注意图中的`Collection`应当继承的是`Iterable`而不是`Iterator`,后面会解释`Iterable`和`Iterator`的区别 +2. 整个集合框架分为两个门派(类型):`Collection`和`Map`,前者是一个容器,存储一系列的**对象**;后者是键值对``,存储一系列的**键值对** +3. 在集合框架体系下,衍生出四种具体的集合类型:`Map`、`Set`、`List`、`Queue` +4. `Map`存储``键值对,查找元素时通过`key`查找`value` +5. `Set`内部存储一系列**不可重复**的对象,且是一个**无序**集合,对象排列顺序不一 +6. `List`内部存储一系列**可重复**的对象,是一个**有序**集合,对象按插入顺序排列 +7. `Queue`是一个**队列**容器,其特性与`List`相同,但只能从`队头`和`队尾`操作元素 +8. JDK 为集合的各种操作提供了两个工具类`Collections`和`Arrays`,之后会讲解工具类的常用方法 +9. 四种抽象集合类型内部也会衍生出许多具有不同特性的集合类,**不同场景下择优使用,没有最佳的集合** \ No newline at end of file diff --git a/Java基础教程/Java集合类/03 List-ArrayList.md b/Java基础教程/Java集合类/03 List-ArrayList.md new file mode 100644 index 00000000..452c55cb --- /dev/null +++ b/Java基础教程/Java集合类/03 List-ArrayList.md @@ -0,0 +1,616 @@ +# List 接口 + +## 1 概述 + +### 简介 +List 接口和 Set 接口齐头并进,是我们日常开发中接触的很多的一种集合类型了。整个 List 集合的组成部分如下图 + +![](image/2022-12-04-22-09-27.png) + +`List` 接口直接继承 Collection 接口,它定义为可以存储**重复**元素的集合,并且元素按照插入顺序**有序排列**,且可以通过**索引**访问指定位置的元素。常见的实现有:ArrayList、LinkedList、Vector和Stack + +### AbstractList 和 AbstractSequentialList + +AbstractList 抽象类实现了 List 接口,其内部实现了所有的 List 都需具备的功能,子类可以专注于实现自己具体的操作逻辑。 + +```java +// 查找元素 o 第一次出现的索引位置 +public int indexOf(Object o) +// 查找元素 o 最后一次出现的索引位置 +public int lastIndexOf(Object o) +//··· +``` + +AbstractSequentialList 抽象类继承了 AbstractList,在原基础上限制了访问元素的顺序**只能够按照顺序访问**,而**不支持随机访问**,如果需要满足随机访问的特性,则继承 AbstractList。子类 LinkedList 使用链表实现,所以仅能支持**顺序访问**,顾继承了 `AbstractSequentialList`而不是 AbstractList。 + + + + + +## 2 ArrayList + +### 概述 +ArrayList 以**数组**作为存储结构,它是**线程不安全**的集合;具有**查询快、在数组中间或头部增删慢**的特点,所以它除了线程不安全这一点,其余可以替代`Vector`,而且线程安全的 ArrayList 可以使用 `CopyOnWriteArrayList`代替 Vector。 + +![](image/2022-12-04-22-17-09.png) + +关于 ArrayList 有几个重要的点需要注意的: + +- 具备**随机访问**特点,**访问元素的效率**较高,ArrayList 在**频繁插入、删除**集合元素的场景下效率较`低`。 + +- 底层数据结构:ArrayList 底层使用数组作为存储结构,具备**查找快、增删慢**的特点 + +- 线程安全性:ArrayList 是**线程不安全**的集合 + +- ArrayList **首次扩容**后的长度为 `10`,调用 `add()` 时需要计算容器的最小容量。可以看到如果数组`elementData`为空数组,会将最小容量设置为`10`,之后会将数组长度完成首次扩容到 10。 + +另外一篇文章 +* Ordered – arraylist中的元素保留其排序,默认情况下是其添加到列表的顺序。 +* Index based –可以使用索引位置随机访问元素。 索引以'0'开头。 +* Dynamic resizing –当需要添加的元素数量超过当前大小时,ArrayList会动态增长。 +* Non synchronized –默认情况下,ArrayList不同步。 程序员需要适当地使用synchronized关键字,或者仅使用Vector类。 +* Duplicates allowed -我们可以在arraylist中添加重复元素。 不能成组放置。 + +```Java +// new ArrayList 时的默认空数组 +private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; +// 默认容量 +private static final int DEFAULT_CAPACITY = 10; +// 计算该容器应该满足的最小容量 +private static int calculateCapacity(Object[] elementData, int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + return Math.max(DEFAULT_CAPACITY, minCapacity); + } + return minCapacity; +} +``` + +- 集合从**第二次扩容**开始,数组长度将扩容为原来的 `1.5` 倍,即:`newLength = oldLength * 1.5` + +![](image/2022-12-04-22-20-35.png) + +### 构造函数 + +* 没有逐个元素初始化的方法,使用Arrays.asList能够添加对象初始化。 +```java +//Empty arraylist +List names = new ArrayList<>(); + +//Arraylist initialized with another collection +List numbers = new ArrayList<>(Arrays.asList(1,2,3,4,5)); +``` + + +### 常用方法 + +* add()添加单个元素 +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; +} +``` +* addAll()方法将给定集合的所有元素添加到arraylist中。 始终使用泛型来确保您仅在给定列表中添加某种类型的元素。 + +```java +import java.util.ArrayList; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list1 = new ArrayList<>(); //list 1 + + list1.add("A"); + list1.add("B"); + list1.add("C"); + list1.add("D"); + + ArrayList list2 = new ArrayList<>(); //list 2 + + list2.add("E"); + + list1.addAll(list2); + + System.out.println(list1); //combined list + } +} +``` +* clear()方法将arraylist clear + +```java +import java.util.ArrayList; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList arrayList = new ArrayList<>(); + + arrayList.add("A"); + arrayList.add("B"); + arrayList.add("C"); + arrayList.add("D"); + + System.out.println(arrayList); + + arrayList.clear(); + + System.out.println(arrayList); + } +} +``` + +* clone()方法创建arraylist的浅表副本 。 +```java +import java.util.ArrayList; + +public class ArrayListExample +{ + @SuppressWarnings("unchecked") + public static void main(String[] args) + { + ArrayList arrayListObject = new ArrayList<>(); + + arrayListObject.add("A"); + arrayListObject.add("B"); + arrayListObject.add("C"); + arrayListObject.add("D"); + + System.out.println(arrayListObject); + + ArrayList arrayListClone = (ArrayList) arrayListObject.clone(); + + System.out.println(arrayListClone); + } +} +``` +* clone深拷贝。创建集合的深层副本非常容易。 我们需要创建一个新的collection实例,并将给定collection中的所有元素一一复制到克隆的collection中。 请注意,我们将在克隆集合中复制元素的克隆。 +```java +ArrayList employeeList = new ArrayList<>(); +ArrayList employeeListClone = new ArrayList<>(); + +Iterator iterator = employeeList.iterator(); + +while(iterator.hasNext()) +{ + //Add the object clones + employeeListClone.add((Employee) iterator.next().clone()); +} +``` +* cantains()数组列表中存储了几个字母。 我们将尝试找出列表中是否包含字母“ A”和“ Z”。 +```java +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(2); + + list.add("A"); + list.add("B"); + list.add("C"); + list.add("D"); + + System.out.println( list.contains("A") ); //true + + System.out.println( list.contains("Z") ); //false + } +} +``` + +* indexOf()判断是否存在.返回此列表中指定元素的首次出现的索引。 如果列表不包含元素,它将返回'-1' 。 + +```java +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(2); + + list.add("A"); + list.add("B"); + list.add("C"); + list.add("D"); + + System.out.println( list.indexOf("A") > 0 ); //true + + System.out.println( list.indexOf("Z") > 0); //false + } +} +``` +* lastIndexOf()返回此列表中最后一次出现的指定元素的索引。 如果列表不包含元素,它将返回'-1' 。 + +```java +public int lastIndexOf(Object object) { + if (o == null) { + for (int i = size-1; i >= 0; i--) + if (elementData[i]==null) + return i; + } else { + for (int i = size-1; i >= 0; i--) + if (o.equals(elementData[i])) + return i; + } + return -1; +} + +``` +* get(int index)方法返回列表中指定位置'index'处的元素。 +```java +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(Arrays.asList("alex", "brian", "charles", "dough")); + + String firstName = list.get(0); //alex + String secondName = list.get(1); //brian + + System.out.println(firstName); + System.out.println(secondName); + } +} +``` + +* boolean remove(Object o) –从列表中删除第一次出现的指定元素。 true从列表中删除了任何元素,则返回true ,否则返回false 。对象remove(int index)引发IndexOutOfBoundsException-移除此列表中指定位置的元素。 将所有后续元素向左移动。 返回从列表中移除的元素。 如果参数索引无效,则引发异常。 + +```java +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList alphabets = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); + + System.out.println(alphabets); + + alphabets.remove("C"); //Element is present + + System.out.println(alphabets); + + alphabets.remove("Z"); //Element is NOT present + + System.out.println(alphabets); + } +} +``` +* removeAll()方法遍历arraylist的所有元素。 对于每个元素,它将元素传递给参数集合的contains()方法。如果在参数集合中找到element,它将重新排列索引。 如果未找到element,则将其保留在后备数组中。 + +```java +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList alphabets = new ArrayList<>(Arrays.asList("A", "B", "A", "D", "A")); + + System.out.println(alphabets); + + alphabets.removeAll(Collections.singleton("A")); + + System.out.println(alphabets); + } +} +``` + +* removeIf()方法采用Predicate类型的单个参数。 谓词接口是一种功能接口,表示一个参数的条件(布尔值函数)。 它检查给定参数是否满足条件。 + +```java +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList numbers = new ArrayList<>(Arrays.asList(1,2,3,4,5,6,7,8,9,10)); + + numbers.removeIf( number -> number%2 == 0 ); + + System.out.println(numbers); + } +} + +``` + +* retainAll()方法来保留列表中存在于指定参数集合中的所有元素。 + +```java +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList alphabets = new ArrayList<>(Arrays.asList("A", "B", "A", "D", "A")); + + System.out.println(alphabets); + + alphabets.retainAll(Collections.singleton("A")); + + System.out.println(alphabets); + } +} +``` + +* sort()方法使用Arrays.sort()方法对列表中的元素进行比较和排序。sort()方法接受Comparator实现类的实例,该实例必须能够比较arraylist中包含的元素 + +```java +import java.util.Comparator; + +public class NameSorter implements Comparator +{ + @Override + public int compare(Employee o1, Employee o2) { + return o1.getName().compareToIgnoreCase(o1.getName()); + } +} + + +import java.time.LocalDate; +import java.time.Month; +import java.util.ArrayList; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList employees = new ArrayList<>(); + + employees.add(new Employee(1l, "Alex", LocalDate.of(2018, Month.APRIL, 21))); + employees.add(new Employee(4l, "Brian", LocalDate.of(2018, Month.APRIL, 22))); + employees.add(new Employee(3l, "David", LocalDate.of(2018, Month.APRIL, 25))); + employees.add(new Employee(5l, "Charles", LocalDate.of(2018, Month.APRIL, 23))); + employees.add(new Employee(2l, "Edwin", LocalDate.of(2018, Month.APRIL, 24))); + + employees.sort(new NameSorter()); + System.out.println(employees); + } +} + +``` + +* toArray()将arraylist转换为对象数组并遍历数组内容 + +```java +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(2); + + list.add("A"); + list.add("B"); + list.add("C"); + list.add("D"); + + //Convert to object array + Object[] array = list.toArray(); + + System.out.println( Arrays.toString(array) ); + + //Iterate and convert to desired type + for(Object o : array) { + String s = (String) o; + + System.out.println(s); + } + } +} +// 可以控制转换完成的结果。 +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(2); + + list.add("A"); + list.add("B"); + list.add("C"); + list.add("D"); + + //Convert to string array + String[] array = list.toArray(new String[list.size()]); + + System.out.println(Arrays.toString(array)); + } +} +``` + +* sublist获取子列表 + +```java +import java.util.ArrayList; +import java.util.Arrays; + +public class ArrayListExample +{ + public static void main(String[] args) + { + ArrayList list = new ArrayList<>(Arrays.asList(0,1,2,3,4,5,6,7,8,9)); + + ArrayList sublist = new ArrayList( list.subList(2, 6) ); + + System.out.println(sublist); + } +} +``` + + +### 遍历方法 +五种loop ArrayList + +* Java程序使用standard for loop遍历对象的数组列表。(能够控制遍历计数,方便进行排序等算法操作) + +```java +ArrayList namesList = new ArrayList(Arrays.asList( "alex", "brian", "charles") ); + +for(int i = 0; i < namesList.size(); i++) +{ + System.out.println(namesList.get(i)); +} +``` +* 使用foreach loop遍历对象的数组列表。(最方便的遍历。) + +```java +ArrayList namesList = new ArrayList(Arrays.asList( "alex", "brian", "charles") ); + +for(String name : namesList) +{ + System.out.println(name); +} +``` +* 使用列表迭代器对象迭代对象的数组列表。(能够在遍历的时候删除。) +```java +ArrayList namesList = new ArrayList(Arrays.asList( "alex", "brian", "charles") ); + +ListIterator listItr = namesList.listIterator(); + +while(listItr.hasNext()) +{ + System.out.println(listItr.next()); +} +``` + +* while循环。(可能在某些算法中比较方便,除非你想让循环体控制遍历计数) + +```java +ArrayList namesList = new ArrayList(Arrays.asList( "alex", "brian", "charles") ); + +int index = 0; +while (namesList.size() > index) +{ + System.out.println(namesList.get(index++)); +} +``` +* 使用Java 8流API遍历对象的数组列表。 使用stream.foreach()方法从arraylist对象创建元素流,并stream.foreach()获取元素。 +```java +Iterate arraylist with stream api +ArrayList namesList = new ArrayList(Arrays.asList( "alex", "brian", "charles") ); +namesList.forEach(name -> System.out.println(name)); +``` + +* listIterator()方法获得的列表迭代器来迭代arraylist。ListIterator支持在迭代列表时添加和删除列表中的元素。 + * listIterator.add(Element e) –将该元素立即插入将由next()返回的元素之前或将要返回的previous()方法的元素之后。 + * listIterator.remove() –从列表中删除next()或previous()方法返回的最后一个元素。 + +```java +import java.util.ArrayList; +import java.util.Arrays; +import java.util.ListIterator; + +public class ArrayListExample +{ + public static void main(String[] args) throws CloneNotSupportedException + { + ArrayList alphabets = new ArrayList<>(Arrays.asList("A", "B", "C", "D")); + + ListIterator listItr = alphabets.listIterator(); + + System.out.println("===========Forward========="); + + while(listItr.hasNext()) { + System.out.println(listItr.next()); + } + + System.out.println("===========Backward========="); + + while(listItr.hasPrevious()) { + System.out.println(listItr.previous()); + } + } +} +``` + +## 3 LinkedList + +LinkedList 底层采用`双向链表`数据结构存储元素,由于链表的内存地址`非连续`,所以它不具备随机访问的特点,但由于它利用指针连接各个元素,所以插入、删除元素只需要`操作指针`,不需要`移动元素`,故具有**增删快、查询慢**的特点。它也是一个非线程安全的集合。 + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595393003456-d37281f4-8332-46b4-9d81-e1f0c24dc060.png) + +由于以双向链表作为数据结构,它是**线程不安全**的集合;存储的每个节点称为一个`Node`,下图可以看到 Node 中保存了`next`和`prev`指针,`item`是该节点的值。在插入和删除时,时间复杂度都保持为 `O(1)` + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595725358023-1f64f780-9dd0-47ff-a84c-d4101d16c1e1.png) + +关于 LinkedList,除了它是以链表实现的集合外,还有一些特殊的特性需要注意的。 + +- 优势:LinkedList 底层没有`扩容机制`,使用`双向链表`存储元素,所以插入和删除元素效率较高,适用于频繁操作元素的场景 +- 劣势:LinkedList 不具备`随机访问`的特点,查找某个元素只能从 `head` 或 `tail` 指针一个一个比较,所以**查找中间的元素时效率很低** +- 查找优化:LinkedList 查找某个下标 `index` 的元素时**做了优化**,若 `index > (size / 2)`,则从 `head` 往后查找,否则从 `tail` 开始往前查找,代码如下所示: + +```Java +LinkedList.Node node(int index) { + LinkedList.Node x; + int i; + if (index < this.size >> 1) { // 查找的下标处于链表前半部分则从头找 + x = this.first; + for(i = 0; i < index; ++i) { x = x.next; } + return x; + } else { // 查找的下标处于数组的后半部分则从尾开始找 + x = this.last; + for(i = this.size - 1; i > index; --i) { x = x.prev; } + return x; + } +} +``` + +- 双端队列:使用双端链表实现,并且实现了 `Deque` 接口,使得 LinkedList 可以用作**双端队列**。下图可以看到 Node 是集合中的元素,提供了前驱指针和后继指针,还提供了一系列操作`头结点`和`尾结点`的方法,具有双端队列的特性。 + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595693779116-a8156f03-36fa-4557-892e-ea5103b06136.png) + +LinkedList 集合最让人树枝的是它的链表结构,但是我们同时也要注意它是一个双端队列型的集合。 + +```java +Deque deque = new LinkedList<>(); +``` + +### Vector + +![image.png](https://cdn.nlark.com/yuque/0/2020/png/1694029/1595375364068-c2c49168-a2f4-4a06-97c0-eb3446d60a68.png) + +`Vector` 在现在已经是一种过时的集合了,包括继承它的 `Stack` 集合也如此,它们被淘汰的原因都是因为**性能**低下。 + +> JDK 1.0 时代,ArrayList 还没诞生,大家都是使用 Vector 集合,但由于 Vector 的**每个操作**都被 **synchronized** 关键字修饰,即使在线程安全的情况下,仍然**进行无意义的加锁与释放锁**,造成额外的性能开销,做了无用功。 + +```java +public synchronized boolean add(E e); +public synchronized E get(int index); +``` + +在 JDK 1.2 时,Collection 家族出现了,它提供了大量**高性能、适用於不同场合**的集合,而 Vector 也是其中一员,但由于 Vector 在每个方法上都加了锁,由于需要兼容许多老的项目,很难在此基础上优化`Vector`了,所以渐渐地也就被历史淘汰了。 + +现在,在**线程安全**的情况下,不需要选用 Vector 集合,取而代之的是 **ArrayList** 集合;在并发环境下,出现了 `CopyOnWriteArrayList`,Vector 完全被弃用了。 + +### Stack + +![img](https://cdn.nlark.com/yuque/0/2020/png/1694029/1596126551356-dc1af780-2fe9-4d04-8351-e70637ecdab5.png) + +`Stack`是一种`后入先出(LIFO)`型的集合容器,如图中所示,`大雄`是最后一个进入容器的,top指针指向大雄,那么弹出元素时,大雄也是第一个被弹出去的。 + +Stack 继承了 Vector 类,提供了栈顶的压入元素操作(push)和弹出元素操作(pop),以及查看栈顶元素的方法(peek)等等,但由于继承了 Vector,正所谓跟错老大没福报,Stack 也渐渐被淘汰了。 + +取而代之的是后起之秀 `Deque`接口,其实现有 `ArrayDeque`,该数据结构更加完善、可靠性更好,依靠队列也可以实现`LIFO`的栈操作,所以优先选择 ArrayDeque 实现栈。 + +```java +Deque stack = new ArrayDeque(); +``` + +ArrayDeque 的数据结构是:`数组`,并提供**头尾指针下标**对数组元素进行操作。本文也会讲到哦,客官请继续往下看,莫着急!:smile: + \ No newline at end of file diff --git a/Java基础教程/Java容器/12 JavaQueue.md b/Java基础教程/Java集合类/12 JavaQueue.md similarity index 100% rename from Java基础教程/Java容器/12 JavaQueue.md rename to Java基础教程/Java集合类/12 JavaQueue.md diff --git a/Java基础教程/Java容器/13 JavaSet.md b/Java基础教程/Java集合类/13 JavaSet.md similarity index 100% rename from Java基础教程/Java容器/13 JavaSet.md rename to Java基础教程/Java集合类/13 JavaSet.md diff --git a/Java基础教程/Java容器/14 JavaMap.md b/Java基础教程/Java集合类/14 JavaMap.md similarity index 100% rename from Java基础教程/Java容器/14 JavaMap.md rename to Java基础教程/Java集合类/14 JavaMap.md diff --git a/Java基础教程/Java容器/15 Java容器底层结构.md b/Java基础教程/Java集合类/15 Java容器底层结构.md similarity index 100% rename from Java基础教程/Java容器/15 Java容器底层结构.md rename to Java基础教程/Java集合类/15 Java容器底层结构.md diff --git a/Java基础教程/Java标准库/image/2022-11-08-10-51-54.png b/Java基础教程/Java集合类/image/2022-11-08-10-51-54.png similarity index 100% rename from Java基础教程/Java标准库/image/2022-11-08-10-51-54.png rename to Java基础教程/Java集合类/image/2022-11-08-10-51-54.png diff --git a/Java基础教程/Java标准库/image/2022-11-08-10-54-19.png b/Java基础教程/Java集合类/image/2022-11-08-10-54-19.png similarity index 100% rename from Java基础教程/Java标准库/image/2022-11-08-10-54-19.png rename to Java基础教程/Java集合类/image/2022-11-08-10-54-19.png diff --git a/Java基础教程/Java集合类/image/2022-12-04-22-09-27.png b/Java基础教程/Java集合类/image/2022-12-04-22-09-27.png new file mode 100644 index 00000000..d5497efa Binary files /dev/null and b/Java基础教程/Java集合类/image/2022-12-04-22-09-27.png differ diff --git a/Java基础教程/Java集合类/image/2022-12-04-22-17-09.png b/Java基础教程/Java集合类/image/2022-12-04-22-17-09.png new file mode 100644 index 00000000..9782dbd0 Binary files /dev/null and b/Java基础教程/Java集合类/image/2022-12-04-22-17-09.png differ diff --git a/Java基础教程/Java集合类/image/2022-12-04-22-20-35.png b/Java基础教程/Java集合类/image/2022-12-04-22-20-35.png new file mode 100644 index 00000000..0e6bce25 Binary files /dev/null and b/Java基础教程/Java集合类/image/2022-12-04-22-20-35.png differ diff --git a/Java基础教程/Java集合类/image/2022-12-04-22-53-11.png b/Java基础教程/Java集合类/image/2022-12-04-22-53-11.png new file mode 100644 index 00000000..dddf8679 Binary files /dev/null and b/Java基础教程/Java集合类/image/2022-12-04-22-53-11.png differ diff --git a/Java基础教程/Java面试原理/01.String 是如何实现的?它有哪些重要的方法?.md b/Java基础教程/Java面试原理/01.String 是如何实现的?它有哪些重要的方法?.md new file mode 100644 index 00000000..8c202988 --- /dev/null +++ b/Java基础教程/Java面试原理/01.String 是如何实现的?它有哪些重要的方法?.md @@ -0,0 +1,306 @@ +# String 是如何实现的?它有哪些重要的方法? + +几乎所有的 Java 面试都是以 String 开始的,如果第一个问题没有回答好,则会给面试官留下非常不好的第一印象,而糟糕的第一印象则会直接影响到自己的面试结果,就好像刚破壳的小鹅一样,会把第一眼看到的动物当成自己的母亲,即使它第一眼看到的是一只小狗或小猫,也会默认跟随其后,心理学把这种现象叫做印刻效应。印刻效应不仅存在于低等动物之中,同样也适用于人类,所以对于 String 的知识,我们必须深入的掌握才能为自己赢得更多的筹码。 + +本课时的问题是:String 是如何实现的?它有哪些重要的方法? + +## 典型回答 + +以主流的 JDK 版本 1.8 来说,String 内部实际存储结构为 char 数组,源码如下: + +```java +public final class String implements java.io.Serializable,Comparable,CharSequence{ + //用于存储字符串的值 + private final charvalue[]; + //缓存字符串的 hash code + private int hash; //Default to 0 + //......其他内容 +} +``` + +## Stirng中的几个重要方法 + +### 1.多构造方法 + +String字符串中4个重要的构造方法: + +```java +// String 为参数的构造方法 +public String(String original) { + this.value = original.value; + this.hash = original.hash; + } +// char[] 为参数的构造方法 +public String(char value[]) { + this.value = Arrays.copyOf(value, value.length); + } +// StringBuffer 为参数的构造方法 + public String(StringBuffer buffer) { + synchronized(buffer) { + this.value = Arrays.copyOf(buffer.getValue(), buffer.length()); + } + } +// StringBuilder 为参数的构造方法 +public String(StringBuilder builder) { + this.value = Arrays.copyOf(builder.getValue(), builder.length()); + } +``` + +### 2.equals()比较两个字符串是否相等 + +源码如下: + +```java +public boolean equals(Object anObject) { + // 对象引用相同直接返回true + if (this == anObject) { + return true; + } + // 判断需要对比的值是否为 String 类型,如果不是则直接返回 false + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + // 把两个字符串转化为 char 数组对比 + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + // 循环比对两个字符串的每一字符 + while (n-- != 0) { + // 如果其中一个字符不相等就返回false 若相等就继续比对 + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + + `String`类型重写了`Object`中的`equals()`方法,`equals()`方法需要传递一个`Object`类型的参数值,在比较时会先通过`instanceof`判断是否为`String`类型,如果不是则会直接返回`false`,`instanceof`的使用如下: + +```java +Object oString ="123"; +Object oInt =123; +System.out.println(oString instanceof String);//返回 true +System.out.println(oInt instanceof String);//返回false +``` + +当判断参数为`String`类型之后,会循环对比两个字符串中的每一个字符,当所有字符都相等时返回`true`,否则则返回`false`。 +还有一个和`equals()`比较类似的方法`equalslgnoreCase()`,它是用于忽略字符串的大小写之后进行字符串对比。 + +### 3.compareTo()比较两个字符串 + +compareTo() 方法用于比较两个字符串,返回的结果为 int 类型的值,源码如下: + +```java +public int compareTo(String anotherString) { + int len1 = value.length; + int len2 = anotherString.value.length; + // 获取两个字符串长度最短的那个 int 值 + int lim = Math.min(len1, len2); + char v1[] = value; + char v2[] = anotherString.value; + + int k = 0; + // 对比每个字符串 + while (k < lim) { + char c1 = v1[k]; + char c2 = v2[k]; + if (c1 != c2) { + // 有字符不相等就返回差值 + return c1 - c2; + } + k++; + } + return len1 - len2; + } +``` + +从源码中可以看出,`compareTo()`方法会循环对比所有的字符,当两个字符串中有任意一个字符不相同时,则`return char1-char2`。比如,两个字符串分别存储的是1和2,返回的值是-1;如果存储的是1和1,则返回的值是0,如果存储的是2和1,则返回的值是1。 +还有一个和`compareTo()`比较类似的方法 `compareTolgnoreCase()`,用于忽略大小写后比较两个字符串。 +可以看出`compareTo()`方法和`equals()`方法都是用于比较两个字符串的,但它们有两点不同: + +* `equals()`可以接收一个`Object` 类型的参数,而`compareTo()`只能接收一个`String`类型的参数; +* `equals()`返回值为`Boolean`,而`compareTo()`的返回值则为`int`。 + +它们都可以用于两个字符串的比较,当 `equals() `方法返回 `true `时,或者是` compareTo() `方法返回` 0` 时,则表示两个字符串完全相同。 + +### 4.其他重要方法 + +* indexOf():查询字符串首次出现的下标的位置 +* lastIndexOf():查询字符串最后出现的下表的位置 +* contains():查询字符串中是否含有另一个字符串 +* toLowerCase():把字符串全部转化为小写 +* toUpperCase():把字符串全部转化为大写 +* length():查询字符串的长度 +* trim():去除字符串首尾的空格 +* replace():替换字符串中的某些字符 +* split():把字符串分割并返回字符串数组 +* jion():把字符串数组转化为字符串 + +## 考点分析 +这道题目考察的重点是,你对 Java 源码的理解,这也从侧面反应了你是否热爱和喜欢专研程序,而这正是一个优秀程序员所必备的特质。 + +String 源码属于所有源码中最基础、最简单的一个,对 String 源码的理解也反应了你的 Java 基础功底。 + +String 问题如果再延伸一下,会问到一些更多的知识细节,这也是大厂一贯使用的面试策略,从一个知识点入手然后扩充更多的知识细节,对于 String 也不例外,通常还会关联的询问以下问题: + +* 为什么 String 类型要用 final 修饰? +* == 和 equals 的区别是什么? +* String 和 StringBuilder、StringBuffer 有什么区别? +* String 的 intern() 方法有什么含义? +* String 类型在 JVM(Java 虚拟机)中是如何存储的?编译器对 String 做了哪些优化? + +# String的扩展问题 + +## 1.==和equals的区别 + +`==`对于基本数据类型来说,是用于比较“值"是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的。 +查看源码我们可以知道`Object`中也有`equals()`方法,源码如下: + +```java +public boolean equals(Object obj) { +    return (this == obj); +} +``` + +可以看出,Object 中的 equals() 方法其实就是 ==,而 String 重写了 equals() 方法把它修改成比较两个字符串的值是否相等。源码如下: + +```java +public boolean equals(Object anObject) { + // 对象引用相同直接返回true + if (this == anObject) { + return true; + } + // 判断需要对比的值是否为 String 类型,如果不是则直接返回 false + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + // 把两个字符串转化为 char 数组对比 + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + // 循环比对两个字符串的每一字符 + while (n-- != 0) { + // 如果其中一个字符不相等就返回false 若相等就继续比对 + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +## 2.final修饰的好处 + +从 String 类的源码我们可以看出 String 是被 final 修饰的不可继承类,源码如下: + +```java +public final class String implements java.io.Serializable,Comparable,CharSequence{ + ... +} +``` + +那这样设计有什么好处呢? + +Java 语言之父James Gosling的回答是,他会更倾向于使用final,因为它能够缓存结果,当你在传参时不需要考虑谁会修 + +改它的值;如果是可变类的话,则有可能需要重新拷贝出来一个新值进行传参,这样在性能上就会有一定的损失。 + +James Gosling 还说迫使String类设计成不可变的另一个原因是安全,当你在调用其他方法时,比如调用一些系统级操作 + +指令之前,可能会有一系列校验,如果是可变类的话,可能在你校验过后,它的内部的值又被改变了,这样有可能会引起 + +严重的系统崩溃问题,这是迫使String类设计成不可变类的一个重要原因。 + +总结来说,使用final修饰的第一个好处是**安全**;第二个好处是**高效**,以JVM中的字符串常量池来举例,如下两个变量: + +```java +String s1 = "java"; +String s2 = "java"; +``` + +只有字符串是不可变时,我们才能实现字符串常量池,字符串常量池可以为我们缓存字符串,提高程序的运行效率,如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200313211609.png) + +试想一下如果`String`是可变的,那当 s1 的值修改之后,s2 的值也跟着改变了,这样就和我们预期的结果不相符了,因此也就没有办法实现字符串常量池的功能了。 + +## 3. String 和 StringBuilder、StringBuffer 的区别 + +因为`String`类型是不可变的,所以在字符串拼接的时候如果使用`String`的话性能会很低,因此我们就需要使用另一个数据类型`StringBuffer`,它提供了`append`和`insert`方法可用于字符串的拼接,它使用`synchronized`来保证线程安全,如下源码所示: + +```java + @Override + public synchronized StringBuffer append(Object obj) { + toStringCache = null; + super.append(String.valueOf(obj)); + return this; + } + + @Override + public synchronized StringBuffer append(String str) { + toStringCache = null; + super.append(str); + return this; + } +``` + +因为它使用了`synchronized`来保证线程安全,所以性能不是很高,于是在JDK1.5就有了`StringBuilder`,它同样提供了`append`和`insert`的拼接方法,但它没有使用`synchronized`来修饰,因此在性能上要优于`StringBuffer`,所以在非并发操作的环境下可使用`StringBuilder`来进行字符串拼接。 + +## 4.String和JVM + +`String`常见的创建方式有两种,直接赋值的方式`"String s1="Java";"`和`"String s2=new String("Java");"`的方式,但两者在JVM的存储区域却截然不同,在JDK1.8中,变量s1会先去字符串常量池中找字符串`"Java”`,如果有相同的字符则直接返回常量句柄,如果没有此字符串则会先在常量池中创建此字符串,然后再返回常量句柄;而变量s2是直接在堆上创建一个变量,如果调用`intern`方法才会把此字符串保存到常量池中,如下代码所示: + +```java +String s1 = new String("Java"); +String s2 = s1.intern(); +String s3 = "Java"; +System.out.println(s1 == s2); // false +System.out.println(s2 == s3); // true +``` + +它们在 JVM 存储的位置,如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200313214234.png) + +> 小贴士:JDK 1.7 之后把永生代换成的元空间,把字符串常量池从方法区移到了 Java 堆上。 +> +> 除此之外编译器还会对 String 字符串做一些优化,例如以下代码: +> +> ```java +> String s1 = "Ja" + "va"; +> String s2 = "Java"; +> System.out.println(s1 == s2); +> ``` +> +> 虽然 s1 拼接了多个字符串,但对比的结果却是 true,我们使用反编译工具,看到的结果如下: +> +> ```java +> 0 ldc #2 +> 2 astore_1 +> 3 ldc #2 +> 5 astore_2 +> 6 getstatic #3 +> 9 aload_1 +> 10 aload_2 +> 11 if_acmpne 18 (+7) +> 14 iconst_1 +> 15 goto 19 (+4) +> 18 iconst_0 +> 19 invokevirtual #4 +> 22 return +> ``` +> +> 从编译代码 #2 可以看出,代码 "Ja"+"va" 被直接编译成了 "Java" ,因此 s1==s2 的结果才是 true,这就是编译器对字符串优化的结果。 +> + +# 小结 +本课时从 String 的源码入手,重点讲了 String 的构造方法、equals() 方法和 compareTo() 方法,其中 equals() 重写了 Object 的 equals() 方法,把引用对比改成了字符串值对比,也介绍了 final 修饰 String 的好处,可以提高效率和增强安全性,同时我们还介绍了 String 和 JVM 的一些执行细节。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/02.HashMap 底层实现原理是什么?JDK8 做了哪些优化?.md b/Java基础教程/Java面试原理/02.HashMap 底层实现原理是什么?JDK8 做了哪些优化?.md new file mode 100644 index 00000000..e79ac6c9 --- /dev/null +++ b/Java基础教程/Java面试原理/02.HashMap 底层实现原理是什么?JDK8 做了哪些优化?.md @@ -0,0 +1,355 @@ +# `HashMap` 底层实现原理是什么?JDK8 做了哪些优化? + +HashMap 是使用频率最高的类型之一,同时也是面试经常被问到的问题之一,这是因为 HashMap 的知识点有很多,同时它又属于 Java 基础知识的一部分,因此在面试中经常被问到。 + +本课时的面试题是,HashMap 底层是如何实现的?在 JDK 1.8 中它都做了哪些优化? + +## 典型回答 + +在JDK1.7中`HashMap`是以数组加链表的形式组成的,JDK1.8之后新增了红黑树的组成结构,当链表大于8时,链表结构会转换成红黑树结构,它的组成结构如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200314141314.png) + +数组中的元素我们称之为哈希桶,它的定义如下: + +```java +static class Node implements Map.Entry { + final int hash; + final K key; + V value; + Node next; + + Node(int hash, K key, V value, Node next) { + this.hash = hash; + this.key = key; + this.value = value; + this.next = next; + } + + public final K getKey() { return key; } + public final V getValue() { return value; } + public final String toString() { return key + "=" + value; } + + public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); + } + + public final V setValue(V newValue) { + V oldValue = value; + value = newValue; + return oldValue; + } + + public final boolean equals(Object o) { + if (o == this) + return true; + if (o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + if (Objects.equals(key, e.getKey()) && + Objects.equals(value, e.getValue())) + return true; + } + return false; + } + } +``` + +可以看出每个哈希桶中包含了四个字段:hash、key、value、next,其中next 表示链表的下一个节点。 + +JDK 1.8之所以添加红黑树是因为一旦链表过长,会严重影响`HashMap`的性能,而红黑树具有快速增删改查的特点,这样就可以有效的解决链表过长时操作比较慢的问题。 + +## 考点分析 +上面大体介绍了 HashMap 的组成结构,但面试官想要知道的远远不止这些,和 HashMap 相关的面试题还有以下几个: + +* JDK 1.8 HashMap 扩容时做了哪些优化? +* 加载因子为什么是 0.75? +* 当有哈希冲突时,HashMap 是如何查找并确认元素的? +* HashMap 源码中有哪些重要的方法? +* HashMap 是如何导致死循环的? + +# 知识扩展 + +## 1.HashMap源码分析 + +`HashMap`源码中包含一下几个属性: + +```java +// HashMap初始化长度 +static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 +// HashMap最大长度 +static final int MAXIMUM_CAPACITY = 1 << 30; //1073741824 +// 默认加载因子(扩容因子) +static final float DEFAULT_LOAD_FACTOR = 0.75f; +//转换红黑树的临界值,当链表长度大于此值时,会把链表结构转换为红黑树结构 +static final int TREEIFY_THRESHOLD = 8; +//转换链表的临界值,当元素小于此值时,会将红黑树结构转换成链表结构 +static final int UNTREEIFY_THRESHOLD = 6; +//最小树容量 +static final int MIN TREEIFY CAPACITY = 64; +``` + +### 1.1.什么时加载因子?为什么加载因子时0.75? + +加载因子也叫扩容因子或负载因子,用来判断什么时候进行扩容的,假如加载因子是0.5,`HashMap`的初始化容量是16,那么当`HashMap`中有16*0.5=8个元素时,`HashMap`就会进行扩容。 + +那加载因子为什么是0.75而不是0.5或者1.0呢? + +这其实是出于容量和性能之间平衡的结果: + +* 当加载因子设置比较大的时候,扩容的门槛就被提高了,扩容发生的频率比较低,占用的空间会比较小,但此时发生Hash冲突的几率就会提升,因此需要更复杂的数据结构来存储元素,这样对元素的操作时间就会增加,运行效率也会因此降低; +* 而当加载因子值比较小的时候,扩容的门槛会比较低,因此会占用更多的空间,此时元素的存储就比较稀疏,发生哈希冲突的可能性就比较小,因此操作性能会比较高。 + +所以综合了以上情况就取了一个 0.5 到 1.0 的平均数 0.75 作为加载因子。 + +### 1.2.HashMap源码中三个重要方法:**查询**、**新增**和**数据扩容**。 + +#### 1.2.1.查询 + +```java +public V get(Object key) { + Node e; + // 对key进行哈希操作 + return (e = getNode(hash(key), key)) == null ? null : e.value; + } +final Node getNode(int hash, Object key) { + Node[] tab; Node first, e; int n; K k; + // 非空判断 + if ((tab = table) != null && (n = tab.length) > 0 && + (first = tab[(n - 1) & hash]) != null) { + // 判断第一个是否是要查询的元素 + if (first.hash == hash && // always check first node + ((k = first.key) == key || (key != null && key.equals(k)))) + return first; + // 判断下一个节点是否非空 + if ((e = first.next) != null) { + // 如果第一节点时树结构,则使用getTreeNode获取相应的数据 + if (first instanceof TreeNode) + return ((TreeNode)first).getTreeNode(hash, key); + do { + // 非树结构,循环判断 + // hash相等并且key相等,返回此节点 + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + return e; + } while ((e = e.next) != null); + } + } + return null; + } +``` + +从以上源码可以看出,当哈希冲突时我们需要通过判断 key 值是否相等,才能确认此元素是不是我们想要的元素。 + +#### 1.2.2.新增 + +```java +public V put(K key, V value) { + // 对key进行哈希操作 + return putVal(hash(key), key, value, false, true); + } +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + Node[] tab; Node p; int n, i; + // 哈希表为空则创建表 + if ((tab = table) == null || (n = tab.length) == 0) + n = (tab = resize()).length; + // 根据 key 的哈希值计算出要插入的数组索引 i + if ((p = tab[i = (n - 1) & hash]) == null) + // 如果table[i]等于nul1,则直接插入 + tab[i] = newNode(hash, key, value, null); + else { + Node e; K k; + // 如果key已经存在了,直接覆盖 value + if (p.hash == hash && + ((k = p.key) == key || (key != null && key.equals(k)))) + e = p; + // 如果key不存在,判断是否为红黑树 + else if (p instanceof TreeNode) + // 红黑树直接插入键值对 + e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); + else { + // 为链表结构,循环准备插入 + for (int binCount = 0; ; ++binCount) { + // 下一个元素为空时 + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + //链表长度大于8转换为红黑树进行处理 + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + treeifyBin(tab, hash); + break; + } + // key已经存在直接覆盖 value + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; + } + } + if (e != null) { // existing mapping for key + V oldValue = e.value; + if (!onlyIfAbsent || oldValue == null) + e.value = value; + afterNodeAccess(e); + return oldValue; + } + } + ++modCount; + // 超过最大容量,扩容 + if (++size > threshold) + resize(); + afterNodeInsertion(evict); + return null; + } + +``` + +新增方法的执行流程,如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200314151422.png) + +#### 1.2.3.扩容 + +```java +final Node[] resize() { + // 扩容前的数组 + Node[] oldTab = table; + // 扩容前的数组的大小和阈值 + int oldCap = (oldTab == null) ? 0 : oldTab.length; + int oldThr = threshold; + //预定义新数组的大小和阈值 + int newCap, newThr = 0; + if (oldCap > 0) { + //超过最大值就不再扩容了 + if (oldCap >= MAXIMUM_CAPACITY) { + threshold = Integer.MAX_VALUE; + return oldTab; + } + //扩大容量为当前容量的两倍,但不能超过MAXIMUM_CAPACITY + else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && + oldCap >= DEFAULT_INITIAL_CAPACITY) + newThr = oldThr << 1; // double threshold + } + //当前数组没有数据,使用初始化的值 + else if (oldThr > 0) // initial capacity was placed in threshold + newCap = oldThr; + else { // zero initial threshold signifies using defaults + //如果初始化的值为0,则使用默认的初始化容量 + newCap = DEFAULT_INITIAL_CAPACITY; + newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); + } + //如果新的容量等于0 + if (newThr == 0) { + float ft = (float)newCap * loadFactor; + newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? + (int)ft : Integer.MAX_VALUE); + } + threshold = newThr; + @SuppressWarnings({"rawtypes","unchecked"}) + Node[] newTab = (Node[])new Node[newCap]; + //开始扩容,将新的容量赋值给 table + table = newTab; + //原数据不为空,将原数据复制到新 table中 + if (oldTab != null) { + //根据容量循环数组,复制非空元素到新 table + for (int j = 0; j < oldCap; ++j) { + Node e; + if ((e = oldTab[j]) != null) { + oldTab[j] = null; + //如果链表只有一个,则进行直接赋值 + if (e.next == null) + newTab[e.hash & (newCap - 1)] = e; + else if (e instanceof TreeNode) + // 红黑树相关操作 + ((TreeNode)e).split(this, newTab, j, oldCap); + else { // preserve order + //链表复制,JDK1.8扩容优化部分 + Node loHead = null, loTail = null; + Node hiHead = null, hiTail = null; + Node next; + do { + next = e.next; + // 原索引 + if ((e.hash & oldCap) == 0) { + if (loTail == null) + loHead = e; + else + loTail.next = e; + loTail = e; + } + // 原索引 + oldCap + else { + if (hiTail == null) + hiHead = e; + else + hiTail.next = e; + hiTail = e; + } + } while ((e = next) != null); + // 将原索引放到哈希桶中 + if (loTail != null) { + loTail.next = null; + newTab[j] = loHead; + } + // 将 原索引+oldCap 放到哈希桶中 + if (hiTail != null) { + hiTail.next = null; + newTab[j + oldCap] = hiHead; + } + } + } + } + } + return newTab; + } +``` + +从以上源码可以看出,JDK1.8在扩容时并没有像JDK1.7那样,重新计算每个元素的哈希值,而是通过高位运算(`e.hash&oldCap`)来确定元素是否需要移动,比如key1的信息如下: + +* key1.hash = 10 0000 1010 +* oldCap = 16 0001 0000 + +使用`e.hash&oldCap`得到的结果高一位为0,当结果为0时表示元素在扩容时位置不会发生任何变化,而key 2信息如下: + +* key2.hash = 10 0001 0001 +* oldCap = 16 0001 0000 + +这时候得到的结果高一位为 1,当结果为 1 时,表示元素在扩容时位置发生了变化,新的下标位置等于原下标位置 + 原数组长度,如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200314160052.png) + +其中红色的虚线图代表了扩容时元素移动的位置。 + +## 2.HashMap死循环分析 + +以JDK1.7为例,假设`HashMap`默认大小为2,原本`HashMap`中有一个元素key(5),我们再使用两个线程:t1添加元素key(3),t2添加元素key(7),当元素key(3)和key(7)都添加到`HashMap`中之后,线程t1在执行到`Entrynext=e.next`;时,交出了CPU的使用权,源码如下: + +```java +void transfer(Entry[] newTable,boolean rehash){ + int newCapacity = newTable.length; + for(Entry e : table){ + while(null != e){ + Entry next = e.next; //线程一执行此处 + if(rehash){ + e.hash = null==e.key ? 0 : hash(e.key); + } + int i = indexFor(e.hash,newCapacity); + e.next = newTable[i]; + newTable[i] = e; + e = next; + } + } +} +``` + +那么此时线程t1中的e指向了key(3),而next指向了key(7);之后线程t2重新rehash之后链表的顺序被反转,链表的位置变成了key(5)→key(7)→ key(3),其中 “→” 用来表示下一个元素。 + +当 t1 重新获得执行权之后,先执行`newTalbe[i] = e` 把 key(3) 的 next 设置为 key(7),而下次循环时查询到 key(7) 的 next 元素为 key(3),于是就形成了 key(3) 和 key(7) 的循环引用,因此就导致了死循环的发生,如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200314161713.png) + +当然发生死循环的原因是JDK1.7链表插入方式为首部倒序插入,这个问题在JDK1.8得到了改善,变成了尾部正序插入。 + +有人曾经把这个问题反馈给了Sun公司,但Sun公司认为这不是一个问题,因为`HashMap`本身就是非线程安全的,如果要在多线程下,建议使用`ConcurrentHashMap`替代,但这个问题在面试中被问到的几率依然很大,所以在这里需要特别说明一下。 + +# 小结 +本课时介绍了 HashMap 的底层数据结构,在 JDK 1.7 时 HashMap 是由数组和链表组成的,而 JDK 1.8 则新增了红黑树结构,当链表的长度大于 8 并且容量大于 64 时会转换为红黑树存储,以提升元素的操作性能。同时还介绍了 HashMap 的三个重要方法,查询、添加和扩容,以及 JDK 1.7 resize() 在并发环境下导致死循环的原因。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/03 关键字.md b/Java基础教程/Java面试原理/03 关键字.md index 866d3ea9..0860f99c 100644 --- a/Java基础教程/Java面试原理/03 关键字.md +++ b/Java基础教程/Java面试原理/03 关键字.md @@ -216,173 +216,3 @@ - -### final - -**1. 数据** - -声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。 - -- 对于基本类型,final 使数值不变; -- 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。 -- 如果引用时类的成员变量,则必须当场赋值,否则编译会报错。 -```java -final int x = 1; -// x = 2; // cannot assign value to final variable 'x' -final A y = new A(); -y.a = 1; -``` - -**2. 方法** - -声明方法不能被子类重写。当使用final修饰方法时,这个方法将成为最终方法,无法被子类重写。但是,该方法仍然可以被继承。 - -private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方法不是重写基类方法,而是在子类中定义了一个新的方法。 - -**3. 类** - -声明类不允许被继承。 - -### static - -**1. 静态变量** - -- 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份。 -- 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。 - -```java -public class A { - - private int x; // 实例变量 - private static int y; // 静态变量 - - public static void main(String[] args) { - // int x = A.x; // Non-static field 'x' cannot be referenced from a static context - A a = new A(); - int x = a.x; - int y = A.y; - } -} -``` - -**2. 静态方法** - -静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。 - -```java -public abstract class A { - public static void func1(){ - } - // public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static' -} -``` - -只能访问所属类的静态字段和静态方法,方法中不能有 this 和 super 关键字,因为这两个关键字与具体对象关联。 - -```java -public class A { - - private static int x; - private int y; - - public static void func1(){ - int a = x; - // int b = y; // Non-static field 'y' cannot be referenced from a static context - // int b = this.y; // 'A.this' cannot be referenced from a static context - } -} -``` - -**3. 静态语句块** - -静态语句块在类初始化时运行一次。 - -```java -public class A { - static { - System.out.println("123"); - } - - public static void main(String[] args) { - A a1 = new A(); - A a2 = new A(); - } -} -``` - -```html -123 -``` - -**4. 静态内部类** - -非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。 - -```java -public class OuterClass { - - class InnerClass { - } - - static class StaticInnerClass { - } - - public static void main(String[] args) { - // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context - OuterClass outerClass = new OuterClass(); - InnerClass innerClass = outerClass.new InnerClass(); - StaticInnerClass staticInnerClass = new StaticInnerClass(); - } -} -``` - -静态内部类不能访问外部类的非静态的变量和方法。 - -**5. 静态导包** - -在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。 - -```java -import static com.xxx.ClassName.* -``` - -**6. 初始化顺序** - -静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。 - -```java -public static String staticField = "静态变量"; -``` - -```java -static { - System.out.println("静态语句块"); -} -``` - -```java -public String field = "实例变量"; -``` - -```java -{ - System.out.println("普通语句块"); -} -``` - -最后才是构造函数的初始化。 - -```java -public InitialOrderTest() { - System.out.println("构造函数"); -} -``` - -存在继承的情况下,初始化顺序为: - -- 父类(静态变量、静态语句块) -- 子类(静态变量、静态语句块) -- 父类(实例变量、普通语句块) -- 父类(构造函数) -- 子类(实例变量、普通语句块) -- 子类(构造函数) diff --git a/Java基础教程/Java面试原理/03.线程的状态有哪些?它是如何工作的?.md b/Java基础教程/Java面试原理/03.线程的状态有哪些?它是如何工作的?.md new file mode 100644 index 00000000..5d86f6a7 --- /dev/null +++ b/Java基础教程/Java面试原理/03.线程的状态有哪些?它是如何工作的?.md @@ -0,0 +1,332 @@ +# 线程的状态有哪些?它是如何工作的? + +线程(Thread)是并发编程的基础,也是程序执行的最小单元,它依托进程而存在。一个进程中可以包含多个线程,多线程可以共享一块内存空间和一组系统资源,因此线程之间的切换更加节省资源、更加轻量化,也因此被称为轻量级的进程。 + +当然,线程也是面试中常被问到的一个知识点,是程序员必备的基础技能,使用它可以有效地提高程序的整体运行速度。 + +本课时的面试问题是,线程的状态有哪些?它是如何工作的? + +## 典型回答 + +线程的状态在 JDK 1.5 之后以枚举的方式被定义在 Thread 的源码中,它总共包含以下 6 个状态: + +* **NEW**,新建状态,线程被创建出来,但尚未启动时的状态; +* **RUNNABLE**,就绪状态,表示可以运行的线程状态,它可能正在运行,或者是排队等待CPU分配给它资源; +* **BLOCKED**,阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁,比如等待执行synchronized代码块或者使用synchronized标记的方法; +* **WAITING**,等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如,一个线程调用了`Object.wait()`方法,那它就在等待另一个线程调用`Object.notify()`或`Object.notifyAll()`方法; +* **TIMED_WAITING**,计时等待状态,和等待状态(`WAITING`)类似,它只是多了超时时间,比如调用了有超时时间设置的方法`Object.wait(longtimeout)`和`Thread.join(longtimeout)`等这些方法时,它才会进入此状态; +* **TERMINATED**,终止状态,表示线程已经执行完成。 + +线程状态的源代码如下: + +```java +public enum State { + /** + * 新建状态,线程被创建出来,但尚未启动时的状态 + */ + NEW, + + /** + * 就绪状态,表示可以运行的线程状态,它可能正在运行,或者是排队等待CPU分配给它资源 + */ + RUNNABLE, + + /** + * 阻塞等待锁的线程状态,表示处于阻塞状态的线程正在等待监视器锁, + * 比如等待执行synchronized代码块或者使用synchronized标记的方法 + */ + BLOCKED, + + /** + * 等待状态,一个处于等待状态的线程正在等待另一个线程执行某个特定的动作,比如, + * 一个线程调用了`Object.wait()`方法,那它就在等待另一个线程调用`Object.notify()` + * 或`Object.notifyAll()`方法 + */ + WAITING, + + /** + * 计时等待状态,和等待状态(`WAITING`)类似,它只是多了超时时间, + * 比如调用了有超时时间设置的方法`Object.wait(longtimeout)`和`Thread.join(longtimeout)` + * 等这些方法时,它才会进入此状态 + */ + TIMED_WAITING, + + /** + * 终止状态,表示线程已经执行完成 + */ + TERMINATED; + } +``` + +线程工作模式是,首先先要创建线程并指定线程需要执行的业务方法,然后再调用线程的`start()`方法,此时线程就从**NEW(新建)**状态变成了**RUNNABLE(就绪)**状态,此时线程会判断要执行的方法中有没有**synchronized**同步代码块,如果有并且其他线程也在使用此锁,那么线程就会变为**BLOCKED(阻塞等待)**状态,当其他线程使用完此锁之后,线程会继续执行剩余的方法。 + +当遇到**Object.wait()**或**Thread.join()**方法时,线程会变为**WAITING(等待状态)**状态,如果是带了超时时间的等待方法,那么线程会进入**TIMED_WAITING(计时等待)**状态,当有其他线程执行了**notify()**或**notifyAll()**方法之后,线程被唤醒继续执行剩余的业务方法,直到方法执行完成为止,此时整个线程的流程就执行完了,执行流程如下图所示: + +![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200322103225.png) + +## 考点分析 +线程一般会作为并发编程的起始问题,用于引出更多的关于并发编程的面试问题。当然对于线程的掌握程度也决定了你对并发编程的掌握程度,通常面试官还会问: + +* BLOCKED(阻塞等待)和 WAITING(等待)有什么区别? +* start() 方法和 run() 方法有什么区别? +* 线程的优先级有什么用?该如何设置? +* 线程的常用方法有哪些? + +# 知识扩展 + +## 1.BLOCKED 和 WAITING 的区别 + +虽然**BLOCKED**和**WAITING**都有等待的含义,但二者有着本质的区别,首先它们状态形成的调用方法不同,其次**BLOCKED**可以理解为当前线程还处于活跃状态,只是在阻塞等待其他线程使用完某个锁资源;而**WAITING**则是因为自身调用了**Object.wait()**或着是**Thread.join()**又或者是**LockSupport.park()**而进入等待状态,只能等待其他线程执行某个特定的动作才能被继续唤醒,比如当线程因为调用了**Object.wait()**而进入**WAITING**状态之后,则需要等待另一个线程执行**Object.notify()**或**Object.notifyAll()**才能被唤醒。 + +## 2.start() 和 run() 的区别 + +首先从 **Thread** 源码来看,**start()** 方法属于 **Thread** 自身的方法,并且使用了 **synchronized** 来保证线程安全,源码如下: + +```java +public synchronized void start() { + // 状态验证,不等于 NEW 的状态会抛出异常 + if (threadStatus != 0) + throw new IllegalThreadStateException(); + + // 通知线程组,此线程即将启动 + group.add(this); + + boolean started = false; + try { + start0(); + started = true; + } finally { + try { + if (!started) { + group.threadStartFailed(this); + } + } catch (Throwable ignore) { + // 不处理任何异常,如果 start0 抛出异常,则它将被传递到调用堆栈上 + } + } + } +``` + +**run()**方法为**Runnable**的抽象方法,必须由调用类重写此方法,重写的**run()**方法其实就是此线程要执行的业务方法,源码如下: + +```java +public class Thread implements Runnable{ + // 忽略其他方法... + private Runnable target; + @Override + public void run(){ + if(target!=nul1){ + target.run(); + } + } + @FunctionalInterface + public interface Runnable{ + public abstract void run(); + } +} +``` + +从执行的效果来说,**start()**方法可以开启多线程,让线程从**NEW**状态转换成**RUNNABLE**状态,而**run()**方法只是一个普通的方法。 +其次,它们可调用的次数不同,**start()**方法不能被多次调用,否则会抛出**java.lang.llegalStateException**;而**run()**方法可以进行多次调用,因为它只是一个普通的方法而已。 + +## 3.线程优先级 + +在 Thread 源码中和线程优先级相关的属性有 3 个: + +```java +//线程可以拥有的最小优先级 +public final static int MIN_PRIORITY =1; +//线程默认优先级 +public final static int NORM_PRIORITY =5; +//线程可以拥有的最大优先级 +public final static int MAX_PRIORITY =10 +``` + +线程的优先级可以理解为线程抢占CPU时间片的概率,优先级越高的线程优先执行的概率就越大,但并不能保证优先级高的线程一定先执行。 +在程序中我们可以通过**Thread.setPriority()**来设置优先级,**setPriority()**源码如下: + +```java +public final void setPriority(int newPriority) { + ThreadGroup g; + checkAccess(); + //先验证优先级的合理性 + if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { + throw new IllegalArgumentException(); + } + if((g = getThreadGroup()) != null) { + //优先级如果超过线程组的最高优先级,则把优先级设置为线程组的最高优先级 + if (newPriority > g.getMaxPriority()) { + newPriority = g.getMaxPriority(); + } + setPriority0(priority = newPriority); + } + } +``` + +## 4.线程常用的方法 + +### join() + +在一个线程中调用**other.join()**,这时候当前线程会让出执行权给**other**线程,直到**other**线程执行完或者过了超时时间之后再继续执行当前线程,**join()**源码如下: + +```java +public final synchronized void join(long millis) + throws InterruptedException { + long base = System.currentTimeMillis(); + long now = 0; + //超时时间不能小于0 + if (millis < 0) { + throw new IllegalArgumentException("timeout value is negative"); + } + //等于0表示无限等待,直到线程执行完为止 + if (millis == 0) { + //判断子线程(其他线程)为活跃线程,则一直等待 + while (isAlive()) { + wait(0); + } + } else { + //循环判断 + while (isAlive()) { + long delay = millis - now; + if (delay <= 0) { + break; + } + wait(delay); + now = System.currentTimeMillis() - base; + } + } + } +``` + +从源码中可以看出 **join()** 方法底层还是通过 **wait()** 方法来实现的。 + +例如,再未使用**join()**时,代码如下: + +```java +public class ThreadExample { + public static void main(String[] args) { + Thread thread = new Thread(()->{ + for (int i=1;i<6;i++){ + try { + Thread.sleep(1000); + }catch (InterruptedException e){ + e.printStackTrace(); + } + System.out.println("子线程休眠:"+i+"秒"); + } + }); + //开启线程 + thread.start(); + //主线程执行 + for(int i=1;i<4;i++){ + try { + Thread.sleep(1000); + }catch (InterruptedException e){ + e.printStackTrace(); + } + System.out.println("主线程休眠:"+i+"秒"); + } + } +} +``` + +结果如下: + +```java +主线程休眠:1秒 +子线程休眠:1秒 +主线程休眠:2秒 +子线程休眠:2秒 +主线程休眠:3秒 +子线程休眠:3秒 +子线程休眠:4秒 +子线程休眠:5秒 +``` + +从结果可以看出,在未使用 join() 时主子线程会交替执行。 + +然后我们再把 join() 方法加入到代码中,代码如下: + +```java +public class ThreadExample { + public static void main(String[] args) throws InterruptedException { + Thread thread = new Thread(()->{ + for (int i=1;i<6;i++){ + try { + Thread.sleep(1000); + }catch (InterruptedException e){ + e.printStackTrace(); + } + System.out.println("子线程休眠:"+i+"秒"); + } + }); + //开启线程 + thread.start(); + //等待子进程两秒钟 + thread.join(2000); + //主线程执行 + for(int i=1;i<4;i++){ + try { + Thread.sleep(1000); + }catch (InterruptedException e){ + e.printStackTrace(); + } + System.out.println("主线程休眠:"+i+"秒"); + } + } +} +``` + +结果如下: + +```java +子线程休眠:1秒 +子线程休眠:2秒 +子线程休眠:3秒 +主线程休眠:1秒 +主线程休眠:2秒 +子线程休眠:4秒 +主线程休眠:3秒 +子线程休眠:5秒 +``` + +从执行结果可以看出,添加 **join()** 方法之后,主线程会先等子线程执行 2 秒之后才继续执行。 + +### yield() + +看 Thread 的源码可以知道 yield() 为本地方法,也就是说 yield() 是由 C 或 C++ 实现的,源码如下: + +```java +public static native void yield(); +``` + +**yield()**方法表示给线程调度器一个当前线程愿意出让 CPU 使用权的暗示,但是线程调度器可能会忽略这个暗示。 + +比如我们执行这段包含了 yield() 方法的代码,如下所示: + +```java +public class YieldExample { + public static void main(String[] args) { + Runnable runnable = new Runnable() { + @Override + public void run() { + for (int i=0;i<10;i++){ + System.out.println("线程:"+Thread.currentThread().getName()+" I:"+i); + if (i == 5){ + Thread.yield(); + } + } + } + }; + Thread t1 = new Thread(runnable,"T1"); + Thread t2 = new Thread(runnable,"T2"); + t1.start(); + t2.start(); + } +} +``` + +当我们把这段代码执行多次之后会发现,每次执行的结果都不相同,这是因为**yield()**执行非常不稳定,线程调度器不一定会采纳**yield()**出让CPU使用权的建议,从而导致了这样的结果。 + +# 小结 +本课时我们介绍了线程的 6 种状态以及线程的执行流程,还介绍了 BLOCKED(阻塞等待)和 WAITING(等待)的区别,start() 方法和 run() 方法的区别,以及 join() 方法和 yield() 方法的作用,但我们不能死记硬背,要多动手实践才能真正的理解这些知识点。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/04.详解 ThreadPoolExecutor 的参数含义及源码执行流程?.md b/Java基础教程/Java面试原理/04.详解 ThreadPoolExecutor 的参数含义及源码执行流程?.md new file mode 100644 index 00000000..3ef71737 --- /dev/null +++ b/Java基础教程/Java面试原理/04.详解 ThreadPoolExecutor 的参数含义及源码执行流程?.md @@ -0,0 +1,362 @@ +# 详解 `ThreadPoolExecutor` 的参数含义及源码执行流程? + +线程池是为了避免线程频繁的创建和销毁带来的性能消耗,而建立的一种池化技术,它是把已创建的线程放入“池”中,当有任务来临时就可以重用已有的线程,无需等待创建的过程,这样就可以有效提高程序的响应速度。但如果要说线程池的话一定离不开**`ThreadPoolExecutor`**,在阿里巴巴的《Java开发手册》中是这样规定线程池的: +线程池不允许使用**`Executors`**去创建,而是通过**`ThreadPoolExecutor`**的方式,这样的处理方式让写的读者更加明确线程池的运行规则,规避资源耗尽的风险。 + +说明:Executors 返回的线程池对象的弊端如下: + +* 1)**`FixedThreadPool`** 和 **`SingleThreadPool`**:允许的请求队列长度为 **`Integer.MAX_VALUE`**,可能会堆积大量的请求,从而导致 **OOM**(Out Of Memory) +* 2)**`CachedThreadPool`** 和 **`ScheduledThreadPool`**:允许的创建线程数量为 **`Integer.MAX_VALUE`**,可能会创建大量的线程,从而导致 **OOM**。 + +其实当我们去看**`Executors`**的源码会发现,**`Executors.newFixedThreadPool()`**、**`Executors.newSingleThreadExecutor()`**和 **`Executors.newCachedThreadPool()`** 等方法的底层都是通过 **`ThreadPoolExecutor`** 实现的。 + +## 典型回答 + +**`ThreadPoolExecutor`** 的核心参数指的是它在构建时需要传递的参数,其构造方法如下所示: + +```java + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) { + if (corePoolSize < 0 || + //maximumPoolSize必须大于0,且必须大于corePoolSize + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.acc = System.getSecurityManager() == null ? + null : + AccessController.getContext(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + } +``` + +* 第1个参数:**`corePoolSize`**表示线程池的常驻核心线程数。如果设置为0,则表示在没有任何任务时,销毁线程池;如果大于0,即使没有任务时也会保证线程池的线程数量等于此值。但需要注意,此值如果设置的比较小,则会频繁的创建和销毁线程;如果设置的比较大,则会浪费系统资源,所以开发者需要根据自己的实际业务来调整此值。 +* 第2个参数:**`maximumPoolSize`**表示线程池在任务最多时,最大可以创建的线程数。官方规定此值必须大于0,也必须大于等于**`corePoolSize`**,此值只有在任务比较多,且不能存放在任务队列时,才会用到。 +* 第3个参数:**`keepAliveTime`**表示线程的存活时间,当线程池空闲时并且超过了此时间,多余的线程就会销毁,直到线程池中的线程数量销毁的等于**`corePoolSize`**为止,如果**`maximumPoolSize`**等于**`corePoolSize`**,那么线程池在空闲的时候也不会销毁任何线程。 +* 第4个参数:**unit** 表示存活时间的单位,它是配合**`keepAliveTime`**参数共同使用的。 +* 第5个参数:**`workQueue`**表示线程池执行的任务队列,当线程池的所有线程都在处理任务时,如果来了新任务就会缓存到此任务队列中排队等待执行。 +* 第6个参数:**`threadFactory`**表示线程的创建工厂,此参数一般用的比较少,我们通常在创建线程池时不指定此参数,它会使用默认的线程创建工厂的方法来创建线程,源代码如下: + +```java + public ThreadPoolExecutor(int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory) { + this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, + threadFactory, defaultHandler); + } + +public static ThreadFactory defaultThreadFactory() { + return new DefaultThreadFactory(); + } + +// 默认的线程创建工厂,需要实现 ThreadFactory 接口 +static class DefaultThreadFactory implements ThreadFactory { + private static final AtomicInteger poolNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String namePrefix; + + DefaultThreadFactory() { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + namePrefix = "pool-" + + poolNumber.getAndIncrement() + + "-thread-"; + } + + // 创建线程 + public Thread newThread(Runnable r) { + Thread t = new Thread(group, r, + namePrefix + threadNumber.getAndIncrement(), + 0); + if (t.isDaemon()) + //创建一个非守护线程 + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + //线程优先级设置为默认值 + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + } +``` + +我们也可以自定义一个线程工厂,通过实现**`ThreadFactory`**接口来完成,这样就可以自定义线程的名称或线程执行的优先级了。 + +* 第7个参数:**`RejectedExecutionHandler`** 表示指定线程池的拒绝策略,当线程池的任务已经在缓存队列**`workQueue`**中存储满了之后,并且不能创建新的线程来执行此任务时,就会用到此拒绝策略,它属于一种限流保护的机制。 + 线程池的工作流程要从它的执行方法**execute()**说起,源码如下: + + ```java + public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + /* +           *分3步进行: +           * +           * 1.如果少于正在运行的corePoolSize线程,请尝试 +           *以给定的命令作为第一个线程启动一个新线程 +           *任务。 对addWorker的调用自动检查runState和 +           * workerCount,因此可以防止假警报的增加 +           *通过返回false返回不应该执行的线程。 +           * +           * 2.如果任务可以成功排队,那么我们仍然需要 +           *仔细检查我们是否应该添加线程 +           *(因为现有的自上次检查后死亡)或 +           *自从进入此方法以来,该池已关闭。 所以我们 +           *重新检查状态,并在必要时回退排队 +           *停止,如果没有,则启动一个新线程。 +           * +           * 3.如果我们无法将任务排队,那么我们尝试添加一个新的 +           *线程。 如果失败,我们知道我们已经关闭或饱和 +           *并因此拒绝任务。 +           */ + int c = ctl.get(); + // 当前工作的线程小于核心线程数 + if (workerCountOf(c) < corePoolSize) { + //创建新的线程执行次任务 + if (addWorker(command, true)) + return; + c = ctl.get(); + } + // 检查线程池是否处于可运行状态,如果是则把任务添加到队列 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + //再次检查线程池是否处于可运行状态,防止第一次校验通过后线程池关闭 + //如果是非运行状态,则把刚加入队列的任务移除 + if (! isRunning(recheck) && remove(command)) + reject(command); + //如果线程池的线程数为0时(当corePoolSize设置为0时会发生) + else if (workerCountOf(recheck) == 0) + //新建线程执行任务 + addWorker(null, false); + } + //核心线程都在忙且队列都已爆满,尝试新启动一个线程执行失败 + else if (!addWorker(command, false)) + //执行拒绝策略 + reject(command); + } + ``` + + 其中**`addWorker(Runnable firstTask,boolean core)`**方法参数说明: + + * **`firstTask`**:线程首先执行的任务,如果没有则设置为null + * **core**:判断是否可以创建线程的阈值(最大值),如果等于true则表示使用**`corePoolSize`**作为阈值,false则表示使用**`maximumPoolSize`**作为阈值 + + +## 考点分析 + +本课时的这道面试题考察的是你对于线程池和 ThreadPoolExecutor 的掌握程度,也属于 Java 的基础知识,几乎所有的面试都会被问到,其中线程池任务执行的主要流程,可以参考以下流程图: + + ![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200323094637.png) + +与 ThreadPoolExecutor 相关的面试题还有以下几个: + +* ThreadPoolExecutor 的执行方法有几种?它们有什么区别? +* 什么是线程的拒绝策略? +* 拒绝策略的分类有哪些? +* 如何自定义拒绝策略? +* ThreadPoolExecutor 能不能实现扩展?如何实现扩展? + + # 知识扩展 + + ## execute()和submit() + + execute()和submit()都是用来执行线程任务的,他们最主要的区别是,submit()能接受线程池执行的返回值,execute()不能接受返回值。 + + 两个方法的具体使用: + + ```java + public static void main(String[] args) throws ExecutionException, InterruptedException { + ThreadPoolExecutor execute = new ThreadPoolExecutor(2,10,10L, TimeUnit.SECONDS,new LinkedBlockingQueue<>(20)); + //execute使用 + execute.execute(new Runnable() { + @Override + public void run() { + System.out.println("Hello,execute"); + } + }); + //submit使用 + Future future = execute.submit(new Callable() { + @Override + public String call() throws Exception { + System.out.println("Hello,submit"); + return "Success"; + } + }); + System.out.println(future.get()); + } + ``` + + 程序执行结果: + + ```java + Hello,execute + Hello,submit + Success + ``` + + 从以上结果可以看出submit()方法可以配合Futrue来接收线程执行的返回值。它们的另一个区别是execute()方法属于Executor接口的方法,而 submit() 方法则是属于 ExecutorService 接口的方法,它们的继承关系如下图所示: + + ![](https://raw.githubusercontent.com/krislinzhao/IMGcloud/master/img/20200323103705.png) + + ## 线程池拒绝策略 + +当线程池中的任务队列已经被存满,再有任务添加时会先判断当前线程池中的线程数是否大于等于线程池的最大值,如果是,则会触发线程池的拒绝策略。 + +Java自带的拒绝策略有四种: + +* `AbortPolicy`,终止策略,线程池会抛出异常并终止执行,它是默认的拒绝策略; +* `CallerRunsPolicy`,把任务交给当前线程来执行 +* `DiscardPolicy`,忽略此任务(最新的任务) +* `DiscardOldestPolicy`,忽略最早的任务(最先加入队列的任务) + +`AbortPolicy`拒绝策略示例: + +```java +ThreadPoolExecutor executor = new ThreadPoolExecutor(1,3,10,TimeUnit.SECONDS,new LinkedBlockingQueue<>(2),new ThreadPoolExecutor.AbortPolicy()); + for (int i=0;i<6;i++){ + executor.execute(()->{ + System.out.println(Thread.currentThread().getName()); + }); + } +``` + +程序运行结果: + +```java +pool-1-thread-1 +pool-1-thread-2 +pool-1-thread-2 +pool-1-thread-2 +pool-1-thread-3 +Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task example.ThreadPoolExecutorExample$$Lambda$1/1149319664@4dd8dc3 rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0] + at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) + at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) + at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) + at example.ThreadPoolExecutorExample.main(ThreadPoolExecutorExample.java:34) +``` + +可以看出当第6个任务来的时候,线程池则执行了`AbortPolicy` 拒绝策略,抛出了异常。因为队列最多存储2个任务,最大可以创建3个线程来执行任务(2+3=5),所以当第6个任务来的时候,此线程池就“忙”不过来了。 + +## 自定义拒绝策略 + +自定义拒绝策略只需要新建一个`RejectedExecutionHandler`对象,然后重写它的`rejectedExecution()`方法即可,如下代码所示: + +```java +ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 3, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(2), + //添加自定义拒绝策略 + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + //业务执行方法 + System.out.println("执行自定义拒绝策略"); + } + }); + for (int i=0;i<6;i++){ + executor.execute(()->{ + System.out.println(Thread.currentThread().getName()); + }); + } +``` + +程序执行结果: + +```java +执行自定义拒绝策略 +pool-1-thread-1 +pool-1-thread-1 +pool-1-thread-1 +pool-1-thread-2 +pool-1-thread-3 +``` + +可以看出线程池执行了自定义的拒绝策略,我们可以在`rejectedExecution`中添加自己业务处理的代码。 + +## `ThreadPoolExecutor`扩展 + +`ThreadPoolExecutor`的扩展主要是通过重写它的`beforeExecute()`和`afterExecute()`方法实现的,我们可以在扩展方法中添加日志或者实现数据统计,比如统计线程的执行时间,如下代码所示: + +```java +public class ThreadPoolExtend { + public static void main(String[] args) { + //线程池扩展调用 + MyThreadPoolExtend executor = new MyThreadPoolExtend(2,4,10, TimeUnit.SECONDS,new LinkedBlockingQueue<>()); + for (int i=0;i<3;i++){ + executor.execute(()->{ + System.out.println(Thread.currentThread().getName()); + }); + } + } + + /** + * 线程池扩展 + */ + static class MyThreadPoolExtend extends ThreadPoolExecutor{ + //保存线程开始执行的时间 + private final ThreadLocal localTime = new ThreadLocal<>(); + public MyThreadPoolExtend(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); + } + + /** + * 开始执行之前 + * @param t 线程 + * @param r 任务 + */ + @Override + protected void beforeExecute(Thread t, Runnable r) { + //开始时间(单位:纳秒) + Long sTime = System.nanoTime(); + localTime.set(sTime); + System.out.println(String.format("%s|before|time=%s",t.getName(),sTime)); + super.beforeExecute(t, r); + } + + /** + * 执行 完成之后 + * @param r 任务 + * @param t 抛出的遗产 + */ + @Override + protected void afterExecute(Runnable r, Throwable t) { + //结束时间(单位:纳秒) + long eTime = System.nanoTime(); + long totalTime = eTime - localTime.get(); + System.out.println(String.format("%s|after|time=%s|耗时:%s毫秒",Thread.currentThread().getName(),eTime,totalTime/1000000.0)); + super.afterExecute(r, t); + } + } +} +``` + +程序执行结果: + +```java +pool-1-thread-1|before|time=95659085427600 +pool-1-thread-1 +pool-1-thread-2|before|time=95659085423300 +pool-1-thread-2 +pool-1-thread-2|after|time=95659113130200|耗时:27.7069毫秒 +pool-1-thread-1|after|time=95659112193700|耗时:26.7661毫秒 +pool-1-thread-2|before|time=95659117635200 +pool-1-thread-2 +pool-1-thread-2|after|time=95659117822000|耗时:0.1868毫秒 +``` + +# 小结 +最后我们总结一下:线程池的使用必须要通过 ThreadPoolExecutor 的方式来创建,这样才可以更加明确线程池的运行规则,规避资源耗尽的风险。同时,也介绍了 ThreadPoolExecutor 的七大核心参数,包括核心线程数和最大线程数之间的区别,当线程池的任务队列没有可用空间且线程池的线程数量已经达到了最大线程数时,则会执行拒绝策略,Java 自动的拒绝策略有 4 种,用户也可以通过重写 rejectedExecution() 来自定义拒绝策略,我们还可以通过重写 beforeExecute() 和 afterExecute() 来实现 ThreadPoolExecutor 的扩展功能。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/05.synchronized和ReentrantLock.md b/Java基础教程/Java面试原理/05.synchronized和ReentrantLock.md new file mode 100644 index 00000000..2c9daac2 --- /dev/null +++ b/Java基础教程/Java面试原理/05.synchronized和ReentrantLock.md @@ -0,0 +1,258 @@ +# synchronized 和 ReentrantLock 的实现原理是什么?它们有什么区别? + +在 JDK 1.5 之前共享对象的协调机制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的机制 ReentrantLock,该机制的诞生并不是为了替代 synchronized,而是在 synchronized 不适用的情况下,提供一种可以选择的高级功能。 + +## 典型回答 + +synchronized 属于独占式悲观锁,是通过 JVM 隐式实现的,synchronized 只允许同一时刻只有一个线程操作资源。 + +在 Java 中每个对象都隐式包含一个 monitor(监视器)对象,加锁的过程其实就是竞争 monitor 的过程,当线程进入字节码 monitorenter 指令之后,线程将持有 monitor 对象,执行 monitorexit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞等待获取该对象。 + +ReentrantLock 是 Lock 的默认实现方式之一,它是基于 AQS(Abstract Queued Synchronizer,队列同步器)实现的,它默认是通过非公平锁实现的,在它的内部有一个 state 的状态字段用于表示锁是否被占用,如果是 0 则表示锁未被占用,此时线程就可以把 state 改为 1,并成功获得锁,而其他未获得锁的线程只能去排队等待获取锁资源。 + +synchronized 和 ReentrantLock 都提供了锁的功能,具备互斥性和不可见性。在 JDK 1.5 中 synchronized 的性能远远低于 ReentrantLock,但在 JDK 1.6 之后 synchronized 的性能略低于ReentrantLock,它的区别如下: + +* synchronized 是 JVM 隐式实现的,而 ReentrantLock 是 Java 语言提供的 API; + +* ReentrantLock 可设置为公平锁,而 synchronized 却不行; + +* ReentrantLock 只能修饰代码块,而 synchronized 可以用于修饰方法、修饰代码块等; + +* ReentrantLock 需要手动加锁和释放锁,如果忘记释放锁,则会造成资源被永久占用,而 synchronized 无需手动释放锁; + +* ReentrantLock 可以知道是否成功获得了锁,而 synchronized 却不行。 + +## 考点分析 +synchronized 和 ReentrantLock 是比线程池还要高频的面试问题,因为它包含了更多的知识点,且涉及到的知识点更加深入,对面试者的要求也更高,前面我们简要地介绍了 synchronized 和 ReentrantLock 的概念及执行原理,但很多大厂会更加深入的追问更多关于它们的实现细节,比如: + +* ReentrantLock 的具体实现细节是什么? +* JDK 1.6 时锁做了哪些优化? + +# 知识扩展 + +## ReentrantLock 源码分析 + +ReentrantLock 的两个构造函数: + +```java +public ReentrantLock() { + sync = new NonfairSync(); // 非公平锁 +} +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +无参的构造函数创建了一个非公平锁,用户也可以根据第二个构造函数,设置一个 boolean 类型的值,来决定是否使用公平锁来实现线程的调度。 + +## 公平锁 VS 非公平锁 + +公平锁的含义是线程需要按照请求的顺序来获得锁;而非公平锁则允许“插队”的情况存在,所谓的“插队”指的是,线程在发送请求的同时该锁的状态恰好变成了可用,那么此线程就可以跳过队列中所有排队的线程直接拥有该锁。 + +而公平锁由于有挂起和恢复所以存在一定的开销,因此性能不如非公平锁,所以 ReentrantLock 和 synchronized 默认都是非公平锁的实现方式。 + +ReentrantLock 是通过 lock() 来获取锁,并通过 unlock() 释放锁,使用代码如下: + +```java +Lock lock = new ReentrantLock(); +try { +    // 加锁 +    lock.lock(); +    //......业务处理 +} finally { +    // 释放锁 +    lock.unlock(); +} +``` + +ReentrantLock 中的 lock() 是通过 sync.lock() 实现的,但 Sync 类中的 lock() 是一个抽象方法,需要子类 NonfairSync 或 FairSync 去实现,NonfairSync 中的 lock() 源码如下: + +```java +final void lock() { +    if (compareAndSetState(0, 1)) +        // 将当前线程设置为此锁的持有者 +        setExclusiveOwnerThread(Thread.currentThread()); +    else +        acquire(1); +} +``` + +FairSync 中的 lock() 源码如下: + +```java +final void lock() { +    acquire(1); +} +``` + +可以看出非公平锁比公平锁只是多了一行 compareAndSetState 方法,该方法是尝试将 state 值由 0 置换为 1,如果设置成功的话,则说明当前没有其他线程持有该锁,不用再去排队了,可直接占用该锁,否则,则需要通过 acquire 方法去排队。 + +acquire 源码如下: + +```java +public final void acquire(int arg) { +    if (!tryAcquire(arg) &&  +        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) +        selfInterrupt(); +} +``` + +tryAcquire 方法尝试获取锁,如果获取锁失败,则把它加入到阻塞队列中,来看 tryAcquire 的源码: + +```java +protected final boolean tryAcquire(int acquires) { +    final Thread current = Thread.currentThread(); +    int c = getState(); +    if (c == 0) { +        // 公平锁比非公平锁多了一行代码 !hasQueuedPredecessors()  +        if (!hasQueuedPredecessors() && +            compareAndSetState(0, acquires)) { //尝试获取锁 +            setExclusiveOwnerThread(current); // 获取成功,标记被抢占 +            return true; +        } +    } +    else if (current == getExclusiveOwnerThread()) { +        int nextc = c + acquires; +        if (nextc < 0) +            throw new Error("Maximum lock count exceeded"); +        setState(nextc); // set state=state+1 +        return true; +    } +    return false; +} +``` + +对于此方法来说,公平锁比非公平锁只多一行代码 !hasQueuedPredecessors(),它用来查看队列中是否有比它等待时间更久的线程,如果没有,就尝试一下是否能获取到锁,如果获取成功,则标记为已经被占用。 + +如果获取锁失败,则调用 addWaiter 方法把线程包装成 Node 对象,同时放入到队列中,但 addWaiter 方法并不会尝试获取锁,acquireQueued 方法才会尝试获取锁,如果获取失败,则此节点会被挂起,源码如下: + +```java +/** + * 队列中的线程尝试获取锁,失败则会被挂起 + */ +final boolean acquireQueued(final Node node, int arg) { +    boolean failed = true; // 获取锁是否成功的状态标识 +    try { +        boolean interrupted = false; // 线程是否被中断 +        for (;;) { +            // 获取前一个节点(前驱节点) +            final Node p = node.predecessor(); +            // 当前节点为头节点的下一个节点时,有权尝试获取锁 +            if (p == head && tryAcquire(arg)) { +                setHead(node); // 获取成功,将当前节点设置为 head 节点 +                p.next = null; // 原 head 节点出队,等待被 GC +                failed = false; // 获取成功 +                return interrupted; +            } +  // 判断获取锁失败后是否可以挂起 +            if (shouldParkAfterFailedAcquire(p, node) && +                parkAndCheckInterrupt()) +                // 线程若被中断,返回 true +                interrupted = true; +        } +    } finally { +        if (failed) +            cancelAcquire(node); +    } +} +``` + +该方法会使用 for(;;) 无限循环的方式来尝试获取锁,若获取失败,则调用 shouldParkAfterFailedAcquire 方法,尝试挂起当前线程,源码如下: + +```java +/** + * 判断线程是否可以被挂起 + */ +private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { +    // 获得前驱节点的状态 +    int ws = pred.waitStatus; +    // 前驱节点的状态为 SIGNAL,当前线程可以被挂起(阻塞) +    if (ws == Node.SIGNAL) +        return true; +    if (ws > 0) {  +        do { +        // 若前驱节点状态为 CANCELLED,那就一直往前找,直到找到一个正常等待的状态为止 +            node.prev = pred = pred.prev; +        } while (pred.waitStatus > 0); +        // 并将当前节点排在它后边 +        pred.next = node; +    } else { +        // 把前驱节点的状态修改为 SIGNAL +        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); +    } +    return false; +} +``` + +线程入列被挂起的前提条件是,前驱节点的状态为 SIGNAL,SIGNAL 状态的含义是后继节点处于等待状态,当前节点释放锁后将会唤醒后继节点。所以在上面这段代码中,会先判断前驱节点的状态,如果为 SIGNAL,则当前线程可以被挂起并返回 true;如果前驱节点的状态 >0,则表示前驱节点取消了,这时候需要一直往前找,直到找到最近一个正常等待的前驱节点,然后把它作为自己的前驱节点;如果前驱节点正常(未取消),则修改前驱节点状态为 SIGNAL。 + +到这里整个加锁的流程就已经走完了,最后的情况是,没有拿到锁的线程会在队列中被挂起,直到拥有锁的线程释放锁之后,才会去唤醒其他的线程去获取锁资源,整个运行流程如下图所示: + +![](https://cdn.jsdelivr.net/gh/krislinzhao/IMGcloud/img/20200425133636.png) + +unlock 相比于 lock 来说就简单很多了,源码如下: + +```java +public void unlock() { +    sync.release(1); +} +public final boolean release(int arg) { +    // 尝试释放锁 +    if (tryRelease(arg)) { +        // 释放成功 +        Node h = head; +        if (h != null && h.waitStatus != 0) +            unparkSuccessor(h); +        return true; +    } +    return false; +} +``` + +锁的释放流程为,先调用 tryRelease 方法尝试释放锁,如果释放成功,则查看头结点的状态是否为 SIGNAL,如果是,则唤醒头结点的下个节点关联的线程;如果释放锁失败,则返回 false。 + +tryRelease 源码如下: + +```java +/** + * 尝试释放当前线程占有的锁 + */ +protected final boolean tryRelease(int releases) { +    int c = getState() - releases; // 释放锁后的状态,0 表示释放锁成功 +    // 如果拥有锁的线程不是当前线程的话抛出异常 +    if (Thread.currentThread() != getExclusiveOwnerThread()) +        throw new IllegalMonitorStateException(); +    boolean free = false; +    if (c == 0) { // 锁被成功释放 +        free = true; +        setExclusiveOwnerThread(null); // 清空独占线程 +    } +    setState(c); // 更新 state 值,0 表示为释放锁成功 +    return free; +} +``` + +在 tryRelease 方法中,会先判断当前的线程是不是占用锁的线程,如果不是的话,则会抛出异常;如果是的话,则先计算锁的状态值 getState() - releases 是否为 0,如果为 0,则表示可以正常的释放锁,然后清空独占的线程,最后会更新锁的状态并返回执行结果。 + +# JDK 1.6 锁优化 + +## 自适应自旋锁 + +JDK 1.5 在升级为 JDK 1.6 时,HotSpot 虚拟机团队在锁的优化上下了很大功夫,比如实现了自适应式自旋锁、锁升级等。 + +JDK 1.6 引入了自适应式自旋锁意味着自旋的时间不再是固定的时间了,比如在同一个锁对象上,如果通过自旋等待成功获取了锁,那么虚拟机就会认为,它下一次很有可能也会成功 (通过自旋获取到锁),因此允许自旋等待的时间会相对的比较长,而当某个锁通过自旋很少成功获得过锁,那么以后在获取该锁时,可能会直接忽略掉自旋的过程,以避免浪费 CPU 的资源,这就是自适应自旋锁的功能。 + +## 锁升级 + +锁升级其实就是从偏向锁到轻量级锁再到重量级锁升级的过程,这是 JDK 1.6 提供的优化功能,也称之为锁膨胀。 + +**偏向锁**是指在无竞争的情况下设置的一种锁状态。偏向锁的意思是它会偏向于第一个获取它的线程,当锁对象第一次被获取到之后,会在此对象头中设置标示为“01”,表示偏向锁的模式,并且在对象头中记录此线程的 ID,这种情况下,如果是持有偏向锁的线程每次在进入的话,不再进行任何同步操作,如 Locking、Unlocking 等,直到另一个线程尝试获取此锁的时候,偏向锁模式才会结束,偏向锁可以提高带有同步但无竞争的程序性能。但如果在多数锁总会被不同的线程访问时,偏向锁模式就比较多余了,此时可以通过 -XX:-UseBiasedLocking 来禁用偏向锁以提高性能。 + +**轻量锁**是相对于重量锁而言的,在 JDK 1.6 之前,synchronized 是通过操作系统的互斥量(mutex lock)来实现的,这种实现方式需要在用户态和核心态之间做转换,有很大的性能消耗,这种传统实现锁的方式被称之为重量锁。 + +而轻量锁是通过比较并交换(CAS,Compare and Swap)来实现的,它对比的是线程和对象的 Mark Word(对象头中的一个区域),如果更新成功则表示当前线程成功拥有此锁;如果失败,虚拟机会先检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,则说明当前线程已经拥有此锁,否则,则说明此锁已经被其他线程占用了。当两个以上的线程争抢此锁时,轻量级锁就膨胀为重量级锁,这就是锁升级的过程,也是 JDK 1.6 锁优化的内容。 + +# 小结 +本课时首先讲了 synchronized 和 ReentrantLock 的实现过程,然后讲了 synchronized 和 ReentrantLock 的区别,最后通过源码的方式讲了 ReentrantLock 加锁和解锁的执行流程。接着又讲了 JDK 1.6 中的锁优化,包括自适应式自旋锁的实现过程,以及 synchronized 的三种锁状态和锁升级的执行流程。 + +synchronized 刚开始为偏向锁,随着锁竞争越来越激烈,会升级为轻量级锁和重量级锁。如果大多数锁被不同的线程所争抢就不建议使用偏向锁了。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/06.谈谈你对锁的理解?如何手动模拟一个死锁?.md b/Java基础教程/Java面试原理/06.谈谈你对锁的理解?如何手动模拟一个死锁?.md new file mode 100644 index 00000000..33263bdd --- /dev/null +++ b/Java基础教程/Java面试原理/06.谈谈你对锁的理解?如何手动模拟一个死锁?.md @@ -0,0 +1,235 @@ +# 谈谈你对锁的理解?如何手动模拟一个死锁? + +在并发编程中有两个重要的概念:线程和锁,多线程是一把双刃剑,它在提高程序性能的同时,也带来了编码的复杂性,对开发者的要求也提高了一个档次。而锁的出现就是为了保障多线程在同时操作一组资源时的数据一致性,当我们给资源加上锁之后,只有拥有此锁的线程才能操作此资源,而其他线程只能排队等待使用此锁。当然,在所有的面试中也都少不了关于“锁”方面的相关问题。 + +## 典型回答 + +死锁是指两个线程同时占用两个资源,又在彼此等待对方释放锁资源,如下图所示: + +![](https://cdn.jsdelivr.net/gh/krislinzhao/IMGcloud/img/20200425140218.png) + +死锁的代码演示如下: + +```java +import java.util.concurrent.TimeUnit; + +public class LockExample { + public static void main(String[] args) { + deadLock(); // 死锁 + } +/** + * 死锁 + */ +private static void deadLock() { + Object lock1 = new Object(); + Object lock2 = new Object(); + // 线程一拥有 lock1 试图获取 lock2 + new Thread(() -> { + synchronized (lock1) { + System.out.println("获取 lock1 成功"); + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 试图获取锁 lock2 + synchronized (lock2) { + System.out.println(Thread.currentThread().getName()); + } + } + }).start(); + // 线程二拥有 lock2 试图获取 lock1 + new Thread(() -> { + synchronized (lock2) { + System.out.println("获取 lock2 成功"); + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 试图获取锁 lock1 + synchronized (lock1) { + System.out.println(Thread.currentThread().getName()); + } + } + }).start(); +} +``` + +以上程序执行结果如下: + +``` +获取 lock1 成功 +获取 lock2 成功 +``` + + +可以看出当我们使用线程一拥有锁 lock1 的同时试图获取 lock2,而线程二在拥有 lock2 的同时试图获取 lock1,这样就会造成彼此都在等待对方释放资源,于是就形成了死锁。 + +锁是指在并发编程中,当有多个线程同时操作一个资源时,为了保证数据操作的正确性,我们需要让多线程排队一个一个的操作此资源,而这个过程就是给资源加锁和释放锁的过程,就好像去公共厕所一样,必须一个一个排队使用,并且在使用时需要锁门和开门一样。 + +## 考点分析 +锁的概念不止出现在 Java 语言中,比如乐观锁和悲观锁其实很早就存在于数据库中了。锁的概念其实不难理解,但要真正的了解锁的原理和实现过程,才能打动面试官。 + +和锁相关的面试问题,还有以下几个: + +* 什么是乐观锁和悲观锁?它们的应用都有哪些?乐观锁有什么问题? +* 什么是可重入锁?用代码如何实现?它的实现原理是什么? +* 什么是共享锁和独占锁? + +# 知识扩展 + +## 1.悲观锁和乐观锁 + +悲观锁指的是数据对外界的修改采取保守策略,它认为线程很容易会把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用。 + +我们来看一下悲观锁的实现流程,以 synchronized 为例,代码如下: + +```java +public class LockExample { + public static void main(String[] args) { + synchronized (LockExample.class) { + System.out.println("lock"); + } + } +} +``` + +我们使用反编译工具查到的结果如下: + +```classes +Compiled from "LockExample.java" +public class com.lagou.interview.ext.LockExample { + public com.lagou.interview.ext.LockExample(); + Code: + 0: aload_0 + 1: invokespecial #1 // Method java/lang/Object."":()V + 4: return + + public static void main(java.lang.String[]); + Code: + 0: ldc #2 // class com/lagou/interview/ext/LockExample + 2: dup + 3: astore_1 + 4: monitorenter // 加锁 + 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; + 8: ldc #4 // String lock + 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V + 13: aload_1 + 14: monitorexit // 释放锁 + 15: goto 23 + 18: astore_2 + 19: aload_1 + 20: monitorexit + 21: aload_2 + 22: athrow + 23: return + Exception table: + from to target type + 5 15 18 any + 18 21 18 any +} +``` + + +可以看出被 synchronized 修饰的代码块,在执行之前先使用 monitorenter 指令加锁,然后在执行结束之后再使用 monitorexit 指令释放锁资源,在整个执行期间此代码都是锁定的状态,这就是典型悲观锁的实现流程。 + +乐观锁和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测。 + +Java 中的乐观锁大部分都是通过 CAS(Compare And Swap,比较并交换)操作实现的,CAS 是一个多线程同步的原子指令,CAS 操作包含三个重要的信息,即内存位置、预期原值和新值。如果内存位置的值和预期的原值相等的话,那么就可以把该位置的值更新为新值,否则不做任何修改。 + +CAS 可能会造成 ABA 的问题,ABA 问题指的是,线程拿到了最初的预期原值 A,然而在将要进行 CAS 的时候,被其他线程抢占了执行权,把此值从 A 变成了 B,然后其他线程又把此值从 B 变成了 A,然而此时的 A 值已经并非原来的 A 值了,但最初的线程并不知道这个情况,在它进行 CAS 的时候,只对比了预期原值为 A 就进行了修改,这就造成了 ABA 的问题。 + +以警匪剧为例,假如某人把装了 100W 现金的箱子放在了家里,几分钟之后要拿它去赎人,然而在趁他不注意的时候,进来了一个小偷,用空箱子换走了装满钱的箱子,当某人进来之后看到箱子还是一模一样的,他会以为这就是原来的箱子,就拿着它去赎人了,这种情况肯定有问题,因为箱子已经是空的了,这就是 ABA 的问题。 + +ABA 的常见处理方式是添加版本号,每次修改之后更新版本号,拿上面的例子来说,假如每次移动箱子之后,箱子的位置就会发生变化,而这个变化的位置就相当于“版本号”,当某人进来之后发现箱子的位置发生了变化就知道有人动了手脚,就会放弃原有的计划,这样就解决了 ABA 的问题。 + +JDK 在 1.5 时提供了 AtomicStampedReference 类也可以解决 ABA 的问题,此类维护了一个“版本号” Stamp,每次在比较时不止比较当前值还比较版本号,这样就解决了 ABA 的问题。 + +相关源码如下: + +```java +public class AtomicStampedReference { + private static class Pair { + final T reference; + final int stamp; // “版本号” + private Pair(T reference, int stamp) { + this.reference = reference; + this.stamp = stamp; + } + static Pair of(T reference, int stamp) { + return new Pair(reference, stamp); + } + } + // 比较并设置 + public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, // 原版本号 + int newStamp) { // 新版本号 + Pair current = pair; + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); + } + //.......省略其他源码 +} +``` + + +可以看出它在修改时会进行原值比较和版本号比较,当比较成功之后会修改值并修改版本号。 + +小贴士:乐观锁有一个优点,它在提交的时候才进行锁定的,因此不会造成死锁。 + +## 2.可重入锁 + +可重入锁也叫递归锁,指的是同一个线程,如果外面的函数拥有此锁之后,内层的函数也可以继续获取该锁。在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁。 + +下面我们用 synchronized 来演示一下什么是可重入锁,代码如下: + +```java +public class LockExample { + public static void main(String[] args) { + reentrantA(); // 可重入锁 + } + /** + * 可重入锁 A 方法 + */ + private synchronized static void reentrantA() { + System.out.println(Thread.currentThread().getName() + ":执行 reentrantA"); + reentrantB(); + } + /** + * 可重入锁 B 方法 + */ + private synchronized static void reentrantB() { + System.out.println(Thread.currentThread().getName() + ":执行 reentrantB"); + } +} +``` + +以上代码的执行结果如下: + +``` +main:执行 reentrantA +main:执行 reentrantB +``` + + +从结果可以看出 reentrantA 方法和 reentrantB 方法的执行线程都是“main” ,我们调用了 reentrantA 方法,它的方法中嵌套了 reentrantB,如果 synchronized 是不可重入的话,那么线程会被一直堵塞。 + +可重入锁的实现原理,是在锁内部存储了一个线程标识,用于判断当前的锁属于哪个线程,并且锁的内部维护了一个计数器,当锁空闲时此计数器的值为 0,当被线程占用和重入时分别加 1,当锁被释放时计数器减 1,直到减到 0 时表示此锁为空闲状态。 + +## 3.共享锁和独占锁 + +只能被单线程持有的锁叫独占锁,可以被多线程持有的锁叫共享锁。 + +独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 synchronized 就是独占锁,而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。 + +独占锁可以理解为悲观锁,当每次访问资源时都要加上互斥锁,而共享锁可以理解为乐观锁,它放宽了加锁的条件,允许多线程同时访问该资源。 + +# 小结 + +本节我们讲了悲观锁和乐观锁,其中悲观锁的典型应用为 synchronized,而 ReadWriteLock 为乐观锁的典型应用,乐观锁可能会导致 ABA 的问题,常见的解决方案是添加版本号来防止 ABA 问题的发生;同时,还讲了可重入锁,在 Java 中,synchronized 和 ReentrantLock 都是可重入锁;最后讲了独占锁和共享锁,其中独占锁可以理解为悲观锁,而共享锁可以理解为乐观锁。 diff --git a/Java基础教程/Java面试原理/07.深克隆和浅克隆有什么区别?它的实现方式有哪些?.md b/Java基础教程/Java面试原理/07.深克隆和浅克隆有什么区别?它的实现方式有哪些?.md new file mode 100644 index 00000000..5e48072d --- /dev/null +++ b/Java基础教程/Java面试原理/07.深克隆和浅克隆有什么区别?它的实现方式有哪些?.md @@ -0,0 +1,519 @@ +# 深克隆和浅克隆有什么区别?它的实现方式有哪些? + +使用克隆可以为我们快速地构建出一个已有对象的副本,它属于 Java 基础的一部分,也是面试中常被问到的知识点之一。 + +我们本课时的面试题是,什么是浅克隆和深克隆?如何实现克隆? + +## 典型回答 + +浅克隆(Shadow Clone)是把原型对象中成员变量为值类型的属性都复制给克隆对象,把原型对象中成员变量为引用类型的引用地址也复制给克隆对象,也就是原型对象中如果有成员变量为引用对象,则此引用对象的地址是共享给原型对象和克隆对象的。 + +简单来说就是浅克隆只会复制原型对象,但不会复制它所引用的对象,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200605211316.png) + +深克隆(Deep Clone)是将原型对象中的所有类型,无论是值类型还是引用类型,都复制一份给克隆对象,也就是说深克隆会把原型对象和原型对象所引用的对象,都复制一份给克隆对象,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200605211420.png) + +在 Java 语言中要实现克隆则需要实现 Cloneable 接口,并重写 Object 类中的 clone() 方法,实现代码如下: + +```java +public class CloneExample { +    public static void main(String[] args) throws CloneNotSupportedException { +        // 创建被赋值对象 +        People p1 = new People(); +        p1.setId(1); +        p1.setName("Java"); +        // 克隆 p1 对象 +        People p2 = (People) p1.clone(); +        // 打印名称 +        System.out.println("p2:" + p2.getName()); +    } +    static class People implements Cloneable { +        // 属性 +        private Integer id; +        private String name; +        /** +         * 重写 clone 方法 +         * @throws CloneNotSupportedException +         */ +        @Override +        protected Object clone() throws CloneNotSupportedException { +            return super.clone(); +        } +        public Integer getId() { +            return id; +        } +        public void setId(Integer id) { +            this.id = id; +        } +        public String getName() { +            return name; +        } +        public void setName(String name) { +            this.name = name; +        } +    } +} +``` + +以上程序执行的结果为: + +``` +p2:Java +``` + +## 考点分析 +克隆相关的面试题不算太难,但因为使用频率不高,因此很容易被人忽略,面试官通常会在一面或者二面的时候问到此知识点,和它相关的面试题还有以下这些: + +* 在 java.lang.Object 中对 clone() 方法的约定有哪些? +* Arrays.copyOf() 是深克隆还是浅克隆? +* 深克隆的实现方式有几种? +* Java 中的克隆为什么要设计成,既要实现空接口 Cloneable,还要重写 Object 的 clone() 方法? + + +# 知识扩展 + +## 1. clone() 源码分析 +要想真正的了解克隆,首先要从它的源码入手,代码如下: + +```java +/** + * Creates and returns a copy of this object.  The precise meaning + * of "copy" may depend on the class of the object. The general + * intent is that, for any object {@code x}, the expression: + * 
+ * 
+ * x.clone() != x
+ * will be true, and that the expression: + * 
+ * 
+ * x.clone().getClass() == x.getClass()
+ * will be {@code true}, but these are not absolute requirements. + * While it is typically the case that: + * 
+ * 
+ * x.clone().equals(x)
+ * will be {@code true}, this is not an absolute requirement. + * 

+ * By convention, the returned object should be obtained by calling + * {@code super.clone}.  If a class and all of its superclasses (except + * {@code Object}) obey this convention, it will be the case that + * {@code x.clone().getClass() == x.getClass()}. + * 

+ * By convention, the object returned by this method should be independent + * of this object (which is being cloned).  To achieve this independence, + * it may be necessary to modify one or more fields of the object returned + * by {@code super.clone} before returning it.  Typically, this means + * copying any mutable objects that comprise the internal "deep structure" + * of the object being cloned and replacing the references to these + * objects with references to the copies.  If a class contains only + * primitive fields or references to immutable objects, then it is usually + * the case that no fields in the object returned by {@code super.clone} + * need to be modified. + * 

+ * ...... + */ +protected native Object clone() throws CloneNotSupportedException; +``` + +从以上源码的注释信息中我们可以看出,Object 对 clone() 方法的约定有三条: + +* 对于所有对象来说,x.clone() !=x 应当返回 true,因为克隆对象与原对象不是同一个对象; + +* 对于所有对象来说,x.clone().getClass() == x.getClass() 应当返回 true,因为克隆对象与原对象的类型是一样的; + +* 对于所有对象来说,x.clone().equals(x) 应当返回 true,因为使用 equals 比较时,它们的值都是相同的。 + + +除了注释信息外,我们看 clone() 的实现方法,发现 clone() 是使用 native 修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行。 + +## 2. Arrays.copyOf() +如果是数组类型,我们可以直接使用 Arrays.copyOf() 来实现克隆,实现代码如下: + +```java +People[] o1 = {new People(1, "Java")}; +People[] o2 = Arrays.copyOf(o1, o1.length); +// 修改原型对象的第一个元素的值 +o1[0].setName("Jdk"); +System.out.println("o1:" + o1[0].getName()); +System.out.println("o2:" + o2[0].getName()); +``` + +以上程序的执行结果为: + +``` +o1:jdk +o2:jdk +``` + +从结果可以看出,我们在修改克隆对象的第一个元素之后,原型对象的第一个元素也跟着被修改了,这说明 Arrays.copyOf() 其实是一个浅克隆。 + +因为数组比较特殊数组本身就是引用类型,因此在使用 Arrays.copyOf() 其实只是把引用地址复制了一份给克隆对象,如果修改了它的引用对象,那么指向它的(引用地址)所有对象都会发生改变,因此看到的结果是,修改了克隆对象的第一个元素,原型对象也跟着被修改了。 + +## 3. 深克隆实现方式汇总 +深克隆的实现方式有很多种,大体可以分为以下几类: + +* 所有对象都实现克隆方法; + +* 通过构造方法实现深克隆; + +* 使用 JDK 自带的字节流实现深克隆; + +* 使用第三方工具实现深克隆,比如 Apache Commons Lang; + +* 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。 + +接下来我们分别来实现以上这些方式,在开始之前先定义一个公共的用户类,代码如下: + +```java +/** + * 用户类 + */ +public class People { +    private Integer id; +    private String name; +    private Address address; // 包含 Address 引用对象 +    // 忽略构造方法、set、get 方法 +} +/** + * 地址类 + */ +public class Address { +    private Integer id; +    private String city; +    // 忽略构造方法、set、get 方法 +} +``` + +可以看出在 People 对象中包含了一个引用对象 Address。 + +### 1.所有对象都实现克隆 + +这种方式我们需要修改 People 和 Address 类,让它们都实现 Cloneable 的接口,让所有的引用对象都实现克隆,从而实现 People 类的深克隆,代码如下: + +```java +public class CloneExample { +    public static void main(String[] args) throws CloneNotSupportedException { +          // 创建被赋值对象 +          Address address = new Address(110, "北京"); +          People p1 = new People(1, "Java", address); +          // 克隆 p1 对象 +          People p2 = p1.clone(); +          // 修改原型对象 +          p1.getAddress().setCity("西安"); +          // 输出 p1 和 p2 地址信息 +          System.out.println("p1:" + p1.getAddress().getCity() + +                  " p2:" + p2.getAddress().getCity()); +    } +    /** +     * 用户类 +     */ +    static class People implements Cloneable { +        private Integer id; +        private String name; +        private Address address; +        /** +         * 重写 clone 方法 +         * @throws CloneNotSupportedException +         */ +        @Override +        protected People clone() throws CloneNotSupportedException { +            People people = (People) super.clone(); +            people.setAddress(this.address.clone()); // 引用类型克隆赋值 +            return people; +        } +        // 忽略构造方法、set、get 方法 +    } +    /** +     * 地址类 +     */ +    static class Address implements Cloneable { +        private Integer id; +        private String city; +        /** +         * 重写 clone 方法 +         * @throws CloneNotSupportedException +         */ +        @Override +        protected Address clone() throws CloneNotSupportedException { +            return (Address) super.clone(); +        } +        // 忽略构造方法、set、get 方法 +    } +} +``` + +以上程序的执行结果为: + +``` +p1:西安 p2:北京 +``` + +从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆。 + +### 2.通过构造方法实现深克隆 + +《Effective Java》 中推荐使用构造器(Copy Constructor)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象,实现代码如下: + +```java +public class SecondExample { +    public static void main(String[] args) throws CloneNotSupportedException { +        // 创建对象 +        Address address = new Address(110, "北京"); +        People p1 = new People(1, "Java", address); + +        // 调用构造函数克隆对象 +        People p2 = new People(p1.getId(), p1.getName(), +                new Address(p1.getAddress().getId(), p1.getAddress().getCity())); + +        // 修改原型对象 +        p1.getAddress().setCity("西安"); + +        // 输出 p1 和 p2 地址信息 +        System.out.println("p1:" + p1.getAddress().getCity() + +                " p2:" + p2.getAddress().getCity()); +    } + +    /** +     * 用户类 +     */ +    static class People { +        private Integer id; +        private String name; +        private Address address; +        // 忽略构造方法、set、get 方法 +    } + +    /** +     * 地址类 +     */ +    static class Address { +        private Integer id; +        private String city; +        // 忽略构造方法、set、get 方法 +    } +} +``` + +以上程序的执行结果为: + +``` +p1:西安 p2:北京 +``` + +从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆。 + +### 3.通过字节流实现深克隆 + +通过 JDK 自带的字节流实现深克隆的方式,是要先将原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆,代码如下: + +```java +import java.io.*; + +public class ThirdExample { +    public static void main(String[] args) throws CloneNotSupportedException { +        // 创建对象 +        Address address = new Address(110, "北京"); +        People p1 = new People(1, "Java", address); + +        // 通过字节流实现克隆 +        People p2 = (People) StreamClone.clone(p1); + +        // 修改原型对象 +        p1.getAddress().setCity("西安"); + +        // 输出 p1 和 p2 地址信息 +        System.out.println("p1:" + p1.getAddress().getCity() + +                " p2:" + p2.getAddress().getCity()); +    } + +    /** +     * 通过字节流实现克隆 +     */ +    static class StreamClone { +        public static  T clone(People obj) { +            T cloneObj = null; +            try { +                // 写入字节流 +                ByteArrayOutputStream bo = new ByteArrayOutputStream(); +                ObjectOutputStream oos = new ObjectOutputStream(bo); +                oos.writeObject(obj); +                oos.close(); +                // 分配内存,写入原始对象,生成新对象 +                ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流 +                ObjectInputStream oi = new ObjectInputStream(bi); +                // 返回生成的新对象 +                cloneObj = (T) oi.readObject(); +                oi.close(); +            } catch (Exception e) { +                e.printStackTrace(); +            } +            return cloneObj; +        } +    } + +    /** +     * 用户类 +     */ +    static class People implements Serializable { +        private Integer id; +        private String name; +        private Address address; +        // 忽略构造方法、set、get 方法 +    } + +    /** +     * 地址类 +     */ +    static class Address implements Serializable { +        private Integer id; +        private String city; +        // 忽略构造方法、set、get 方法 +    } +} +``` + +以上程序的执行结果为: + +``` +p1:西安 p2:北京 +``` + +此方式需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须实现 Serializable 接口,标识自己可以被序列化,否则会抛出异常 (java.io.NotSerializableException)。 + +### 4.通过第三方工具实现深克隆 + +本课时使用 Apache Commons Lang 来实现深克隆,实现代码如下: + +```java +import org.apache.commons.lang3.SerializationUtils; + +import java.io.Serializable; + +/** + * 深克隆实现方式四:通过 apache.commons.lang 实现 + */ +public class FourthExample { +    public static void main(String[] args) throws CloneNotSupportedException { +        // 创建对象 +        Address address = new Address(110, "北京"); +        People p1 = new People(1, "Java", address); + +        // 调用 apache.commons.lang 克隆对象 +        People p2 = (People) SerializationUtils.clone(p1); + +        // 修改原型对象 +        p1.getAddress().setCity("西安"); + +        // 输出 p1 和 p2 地址信息 +        System.out.println("p1:" + p1.getAddress().getCity() + +                " p2:" + p2.getAddress().getCity()); +    } + +    /** +     * 用户类 +     */ +    static class People implements Serializable { +        private Integer id; +        private String name; +        private Address address; +        // 忽略构造方法、set、get 方法 +    } + +    /** +     * 地址类 +     */ +    static class Address implements Serializable { +        private Integer id; +        private String city; +        // 忽略构造方法、set、get 方法 +    } +} +``` + +以上程序的执行结果为: + +``` +p1:西安 p2:北京 +``` + +可以看出此方法和第三种实现方式类似,都需要实现 Serializable 接口,都是通过字节流的方式实现的,只不过这种实现方式是第三方提供了现成的方法,让我们可以直接调用。 + +### 5.通过 JSON 工具类实现深克隆 + +本课时我们使用 Google 提供的 JSON 转化工具 Gson 来实现,其他 JSON 转化工具类也是类似的,实现代码如下: + +```java +import com.google.gson.Gson; + +/** + * 深克隆实现方式五:通过 JSON 工具实现 + */ +public class FifthExample { +    public static void main(String[] args) throws CloneNotSupportedException { +        // 创建对象 +        Address address = new Address(110, "北京"); +        People p1 = new People(1, "Java", address); + +        // 调用 Gson 克隆对象 +        Gson gson = new Gson(); +        People p2 = gson.fromJson(gson.toJson(p1), People.class); + +        // 修改原型对象 +        p1.getAddress().setCity("西安"); + +        // 输出 p1 和 p2 地址信息 +        System.out.println("p1:" + p1.getAddress().getCity() + +                " p2:" + p2.getAddress().getCity()); +    } + +    /** +     * 用户类 +     */ +    static class People { +        private Integer id; +        private String name; +        private Address address; +        // 忽略构造方法、set、get 方法 +    } + +    /** +     * 地址类 +     */ +    static class Address { +        private Integer id; +        private String city; +        // 忽略构造方法、set、get 方法 +    } +} +``` + +以上程序的执行结果为: + +``` +p1:西安 p2:北京 +``` + +使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的。 + +### 6. 克隆设计理念猜想 +对于克隆为什么要这样设计,官方没有直接给出答案,我们只能凭借一些经验和源码文档来试着回答一下这个问题。Java 中实现克隆需要两个主要的步骤,一是 实现 Cloneable 空接口,二是重写 Object 的 clone() 方法再调用父类的克隆方法 (super.clone()),那为什么要这么做? + +从源码中可以看出 Cloneable 接口诞生的比较早,JDK 1.0 就已经存在了,因此从那个时候就已经有克隆方法了,那我们怎么来标识一个类级别对象拥有克隆方法呢?克隆虽然重要,但我们不能给每个类都默认加上克隆,这显然是不合适的,那我们能使用的手段就只有这几个了: + +* 在类上新增标识,此标识用于声明某个类拥有克隆的功能,像 final 关键字一样; +* 使用 Java 中的注解; +* 实现某个接口; +* 继承某个类。 + +先说第一个,为了一个重要但不常用的克隆功能, 单独新增一个类标识,这显然不合适;再说第二个,因为克隆功能出现的比较早,那时候还没有注解功能,因此也不能使用;第三点基本满足我们的需求,第四点和第一点比较类似,为了一个克隆功能需要牺牲一个基类,并且 Java 只能单继承,因此这个方案也不合适。采用排除法,无疑使用实现接口的方式是那时最合理的方案了,而且在 Java 语言中一个类可以实现多个接口。 + +那为什么要在 Object 中添加一个 clone() 方法呢? + +因为 clone() 方法语义的特殊性,因此最好能有 JVM 的直接支持,既然要 JVM 直接支持,就要找一个 API 来把这个方法暴露出来才行,最直接的做法就是把它放入到一个所有类的基类 Object 中,这样所有类就可以很方便地调用到了。 + +# 小结 + +本课时我们讲了浅克隆和深克隆的概念,以及 Object 对 clone() 方法的约定;还演示了数组的 copyOf() 方法其实为浅克隆,以及深克隆的 5 种实现方式;最后我们讲了 Java 语言中克隆的设计思路猜想。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/08.动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?.md b/Java基础教程/Java面试原理/08.动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?.md new file mode 100644 index 00000000..c4f8b2c7 --- /dev/null +++ b/Java基础教程/Java面试原理/08.动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?.md @@ -0,0 +1,270 @@ +# 动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别? + +90% 的程序员直接或者间接的使用过动态代理,无论是日志框架或 Spring 框架,它们都包含了动态代理的实现代码。动态代理是程序在运行期间动态构建代理对象和动态调用代理方法的一种机制。 + +我们本课时的面试题是,如何实现动态代理?JDK Proxy 和 CGLib 有什么区别? + +## 典型回答 + +动态代理的常用实现方式是反射。反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。 + +但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。 + +JDK Proxy 和 CGLib 的区别主要体现在以下几个方面: + +* JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现; +* Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多; +* JDK Proxy 是通过拦截器加反射的方式实现的; +* JDK Proxy 只能代理继承接口的类; +* JDK Proxy 实现和调用起来比较简单; +* CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高; +* CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。 + +## 考点分析 + +本课时考察的是你对反射、动态代理及 CGLib 的了解,很多人经常会把反射和动态代理划为等号,但从严格意义上来说,这种想法是不正确的,真正能搞懂它们之间的关系,也体现了你扎实 Java 的基本功。和这个问题相关的知识点,还有以下几个: + +* 你对 JDK Proxy 和 CGLib 的掌握程度。 +* Lombok 是通过反射实现的吗? +* 动态代理和静态代理有什么区别? +* 动态代理的使用场景有哪些? +* Spring 中的动态代理是通过什么方式实现的? + +# 知识扩展 + +## 1. JDK Proxy 和 CGLib 的使用及代码分析 + +### JDK Proxy 动态代理实现 + +JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示: + +```java +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * JDK Proxy 相关示例 + */ +public class ProxyExample { +    static interface Car { +        void running(); +    } + +    static class Bus implements Car { +        @Override +        public void running() { +            System.out.println("The bus is running."); +        } +    } + +    static class Taxi implements Car { +        @Override +        public void running() { +            System.out.println("The taxi is running."); +        } +    } + +    /** +     * JDK Proxy +     */ +    static class JDKProxy implements InvocationHandler { +        private Object target; // 代理对象 + +        // 获取到代理对象 +        public Object getInstance(Object target) { +            this.target = target; +            // 取得代理对象 +            return Proxy.newProxyInstance(target.getClass().getClassLoader(), +                    target.getClass().getInterfaces(), this); +        } + +        /** +         * 执行代理方法 +         * @param proxy  代理对象 +         * @param method 代理方法 +         * @param args   方法的参数 +         * @return +         * @throws InvocationTargetException +         * @throws IllegalAccessException +         */ +        @Override +        public Object invoke(Object proxy, Method method, Object[] args) +                throws InvocationTargetException, IllegalAccessException { +            System.out.println("动态代理之前的业务处理."); +            Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理) +            return result; +        } +    } + +    public static void main(String[] args) { +        // 执行 JDK Proxy +        JDKProxy jdkProxy = new JDKProxy(); +        Car carInstance = (Car) jdkProxy.getInstance(new Taxi()); +        carInstance.running(); + } +``` + +以上程序的执行结果是: + +``` +动态代理之前的业务处理. +The taxi is running. +``` + +可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下: + +```java +public interface InvocationHandler { +  public Object invoke(Object proxy, Method method, Object[] args) +          throws Throwable; +} +``` + +这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。 + +### CGLib 的实现 + +在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置: + +```xml + + +    cglib +    cglib +    3.3.0 + +``` + +CGLib 实现代码如下: + +```java +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; + +public class CGLibExample { + +    static class Car { +        public void running() { +            System.out.println("The car is running."); +        } +    } + +    /** +     * CGLib 代理类 +     */ +    static class CGLibProxy implements MethodInterceptor { +        private Object target; // 代理对象 + +        public Object getInstance(Object target) { +            this.target = target; +            Enhancer enhancer = new Enhancer(); +            // 设置父类为实例类 +            enhancer.setSuperclass(this.target.getClass()); +            // 回调方法 +            enhancer.setCallback(this); +            // 创建代理对象 +            return enhancer.create(); +        } + +        @Override +        public Object intercept(Object o, Method method, +                                Object[] objects, MethodProxy methodProxy) throws Throwable { +            System.out.println("方法调用前业务处理."); +            Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用 +            return result; +        } +    } + +    // 执行 CGLib 的方法调用 +    public static void main(String[] args) { +        // 创建 CGLib 代理类 +        CGLibProxy proxy = new CGLibProxy(); +        // 初始化代理对象 +        Car car = (Car) proxy.getInstance(new Car()); +        // 执行方法 +        car.running(); + } +``` + +以上程序的执行结果是: + +``` +方法调用前业务处理. +The car is running. +``` + +可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。 + +## 2. Lombok 原理分析 +在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。 + +例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的: + +```java +public class Person { +    private Integer id; +    private String name; +    public Integer getId() { +        return id; +    } +    public void setId(Integer id) { +        this.id = id; +    } +    public String getName() { +        return name; +    } +    public void setName(String name) { +        this.name = name; +    } +} +``` + +在使用 Lombok 之后,代码是这样的: + +```java +@Data +public class Person { +    private Integer id; +    private String name; +} +``` + +可以看出 Lombok 让代码简单和优雅了很多。 + +> 小贴士:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示: +> +> ![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200606112218.png) + +接下来讲讲 Lombok 的原理。 + +Lombok 的实现和反射没有任何关系,前面我们说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢? + +回到我们刚才 Setter/Getter 的方法,当我们打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码竟然是这样的: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200606112319.png) + +可以看出 Lombok 是在编译期就为我们生成了对应的字节码。 + +其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200606112419.png) + +从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。 + +## 3.动态代理知识点扩充 + +当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。 + +动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。 + +Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 ` `即可。 + +# 小结 +本课时我们介绍了 JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。 + +除了 JDK Proxy 和 CGLib 之外,我们还讲了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/09.如何实现本地缓存和分布式缓存?.md b/Java基础教程/Java面试原理/09.如何实现本地缓存和分布式缓存?.md new file mode 100644 index 00000000..e7ce4ff7 --- /dev/null +++ b/Java基础教程/Java面试原理/09.如何实现本地缓存和分布式缓存?.md @@ -0,0 +1,416 @@ +# 如何实现本地缓存和分布式缓存? + +缓存(Cache) 是指将程序或系统中常用的数据对象存储在像内存这样特定的介质中,以避免在每次程序调用时,重新创建或组织数据所带来的性能损耗,从而提高了系统的整体运行速度。 + +以目前的系统架构来说,用户的请求一般会先经过缓存系统,如果缓存中没有相关的数据,就会在其他系统中查询到相应的数据并保存在缓存中,最后返回给调用方。 + +缓存既然如此重要,那本课时我们就来重点看一下,应该如何实现本地缓存和分布式缓存? + +## 典型回答 + +本地缓存是指程序级别的缓存组件,它的特点是本地缓存和应用程序会运行在同一个进程中,所以本地缓存的操作会非常快,因为在同一个进程内也意味着不会有网络上的延迟和开销。 + +本地缓存适用于单节点非集群的应用场景,它的优点是快,缺点是多程序无法共享缓存,比如分布式用户 Session 会话信息保存,由于每次用户访问的服务器可能是不同的,如果不能共享缓存,那么就意味着每次的请求操作都有可能被系统阻止,因为会话信息只保存在某一个服务器上,当请求没有被转发到这台存储了用户信息的服务器时,就会被认为是非登录的违规操作。 + +除此之外,无法共享缓存可能会造成系统资源的浪费,这是因为每个系统都单独维护了一份属于自己的缓存,而同一份缓存有可能被多个系统单独进行存储,从而浪费了系统资源。 + +分布式缓存是指将应用系统和缓存组件进行分离的缓存机制,这样多个应用系统就可以共享一套缓存数据了,它的特点是共享缓存服务和可集群部署,为缓存系统提供了高可用的运行环境,以及缓存共享的程序运行机制。 + +本地缓存可以使用 EhCache 和 Google 的 Guava 来实现,而分布式缓存可以使用 Redis 或 Memcached 来实现。 + +由于 Redis 本身就是独立的缓存系统,因此可以作为第三方来提供共享的数据缓存,而 Redis 的分布式支持主从、哨兵和集群的模式,所以它就可以支持分布式的缓存,而 Memcached 的情况也是类似的。 + +## 考点分析 +本课时的面试题显然不只是为了问你如何实现本地缓存和分布式缓存这么简单,主要考察的是你对缓存系统的理解,以及对缓存本质原理的洞察,和缓存相关的面试题还有这些: + +更加深入的谈谈 EhCache 和 Guava。 +如何自己手动实现一个缓存系统? + +# 知识扩展 + +## 1. EhCache 和 Guava 的使用及特点分析 +EhCache 是目前比较流行的开源缓存框架,是用纯 Java 语言实现的简单、快速的 Cache 组件。EhCache 支持内存缓存和磁盘缓存,支持 LRU(Least Recently Used,最近很少使用)、LFU(Least Frequently Used,最近不常被使用)和 FIFO(First In First Out,先进先出)等多种淘汰算法,并且支持分布式的缓存系统。 + +EhCache 最初是独立的本地缓存框架组件,在后期的发展中(从 1.2 版)开始支持分布式缓存,分布式缓存主要支持 RMI、JGroups、EhCache Server 等方式。 + +LRU 和 LFU 的区别 + +LRU 算法有一个缺点,比如说很久没有使用的一个键值,如果最近被访问了一次,那么即使它是使用次数最少的缓存,它也不会被淘汰;而 LFU 算法解决了偶尔被访问一次之后,数据就不会被淘汰的问题,它是根据总访问次数来淘汰数据的,其核心思想是“如果数据过去被访问多次,那么将来它被访问次数也会比较多”。因此 LFU 可以理解为比 LRU 更加合理的淘汰算法。 + +EhCache 基础使用 + +首先,需要在项目中添加 EhCache 框架,如果为 Maven 项目,则需要在 pom.xml 中添加如下配置: + +```xml + + +    org.ehcache +    ehcache +    3.8.1 + +``` + +无配置参数的 EhCache 3.x 使用代码如下: + +```java +import org.ehcache.Cache; +import org.ehcache.CacheManager; +import org.ehcache.config.builders.CacheConfigurationBuilder; +import org.ehcache.config.builders.CacheManagerBuilder; +import org.ehcache.config.builders.ResourcePoolsBuilder; + +public class EhCacheExample { +    public static void main(String[] args) { +        // 创建缓存管理器 +        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(); +        // 初始化 EhCache +        cacheManager.init(); +        // 创建缓存(存储器) +        Cache myCache = cacheManager.createCache("MYCACHE", +                CacheConfigurationBuilder.newCacheConfigurationBuilder( +                        String.class, String.class, +                        ResourcePoolsBuilder.heap(10))); // 设置缓存的最大容量 +        // 设置缓存 +        myCache.put("key", "Hello,Java."); +        // 读取缓存 +        String value = myCache.get("key"); +        // 输出缓存 +        System.out.println(value); +        // 关闭缓存 +        cacheManager.close(); +    } +} +``` + +其中: + +* CacheManager:是缓存管理器,可以通过单例或者多例的方式创建,也是 Ehcache 的入口类; +* Cache:每个 CacheManager 可以管理多个 Cache,每个 Cache 可以采用 hash 的方式存储多个元素。 + +它们的关系如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200606211651.png) + +更多使用方法,请参考官方文档。 + +EhCache 的特点是,它使用起来比较简单,并且本身的 jar 包不是很大,简单的配置之后就可以正常使用了。EhCache 的使用比较灵活,它支持多种缓存策略的配置,它同时支持内存和磁盘缓存两种方式,在 EhCache 1.2 之后也开始支持分布式缓存了。 + +Guava Cache 是 Google 开源的 Guava 里的一个子功能,它是一个内存型的本地缓存实现方案,提供了线程安全的缓存操作机制。 + +Guava Cache 的架构设计灵感来源于 ConcurrentHashMap,它使用了多个 segments 方式的细粒度锁,在保证线程安全的同时,支持了高并发的使用场景。Guava Cache 类似于 Map 集合的方式对键值对进行操作,只不过多了过期淘汰等处理逻辑。 + +在使用 Guava Cache 之前,我们需要先在 pom.xml 中添加 Guava 框架,配置如下: + +```xml + + +    com.google.guava +    guava +    28.2-jre + +``` + +Guava Cache 的创建有两种方式,一种是 LoadingCache,另一种是 Callable,代码示例如下: + +```java +import com.google.common.cache.*; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class GuavaExample { +    public static void main(String[] args) throws ExecutionException { +        // 创建方式一:LoadingCache +        LoadingCache loadCache = CacheBuilder.newBuilder() +                // 并发级别设置为 5,是指可以同时写缓存的线程数 +                .concurrencyLevel(5) +                // 设置 8 秒钟过期 +                .expireAfterWrite(8, TimeUnit.SECONDS) +                //设置缓存容器的初始容量为 10 +                .initialCapacity(10) +                // 设置缓存最大容量为 100,超过之后就会按照 LRU 算法移除缓存项 +                .maximumSize(100) +                // 设置要统计缓存的命中率 +                .recordStats() +                // 设置缓存的移除通知 +                .removalListener(new RemovalListener() { +                    public void onRemoval(RemovalNotification notification) { +                        System.out.println(notification.getKey() + " was removed, cause is " + notification.getCause()); +                    } +                }) +                // 指定 CacheLoader,缓存不存在时,可自动加载缓存 +                .build( +                        new CacheLoader() { +                            @Override +                            public String load(String key) throws Exception { +                                // 自动加载缓存的业务 +                                return "cache-value:" + key; +                            } +                        } +                ); +        // 设置缓存 +        loadCache.put("c1", "Hello, c1."); +        // 查询缓存 +        String val = loadCache.get("c1"); +        System.out.println(val); +        // 查询不存在的缓存 +        String noval = loadCache.get("noval"); +        System.out.println(noval); + +        // 创建方式二:Callable +        Cache cache = CacheBuilder.newBuilder() +                .maximumSize(2) // 设置缓存最大长度 +                .build(); +        // 设置缓存 +        cache.put("k1", "Hello, k1."); +        // 查询缓存 +        String value = cache.get("k1", new Callable() { +            @Override +            public String call() { +                // 缓存不存在时,执行 +                return "nil"; +            } +        }); +        // 输出缓存值 +        System.out.println(value); +        // 查询缓存 +        String nokey = cache.get("nokey", new Callable() { +            @Override +            public String call() { +                // 缓存不存在时,执行 +                return "nil"; +            } +        }); +        // 输出缓存值 +        System.out.println(nokey); +    } +} +``` + +以上程序的执行结果为: + +``` +Hello, c1. +cache-value:noval +Hello, k1. +nil +``` + +可以看出 Guava Cache 使用了编程式的 build 生成器进行创建和管理,让使用者可以更加灵活地操纵代码,并且 Guava Cache 提供了灵活多样的个性化配置,以适应各种使用场景。 + +## 2. 手动实现一个缓存系统 +上面我们讲了通过 EhCache 和 Guava 实现缓存的方式,接下来我们来看看自己如何自定义一个缓存系统,当然这里说的是自己手动实现一个本地缓存。 + +要自定义一个缓存,首先要考虑的是数据类型,我们可以使用 Map 集合中的 HashMap、Hashtable 或 ConcurrentHashMap 来实现,非并发情况下我们可以使用 HashMap,并发情况下可以使用 Hashtable 或 ConcurrentHashMap,由于 ConcurrentHashMap 的性能比 Hashtable 的高,因此在高并发环境下我们可以倾向于选择 ConcurrentHashMap,不过它们对元素的操作都是类似的。 + +选定了数据类型之后,我们还需要考虑缓存过期和缓存淘汰等问题,在这里我们可以借鉴 Redis 对待过期键的处理策略。 + +目前比较常见的过期策略有以下三种: + +* 定时删除 +* 惰性删除 +* 定期删除 + +**定时删除**是指在设置键值的过期时间时,创建一个定时事件,当到达过期时间后,事件处理器会执行删除过期键的操作。它的优点是可以及时的释放内存空间,缺点是需要开启多个延迟执行事件来处理清除任务,这样就会造成大量任务事件堆积,占用了很多系统资源。 + +**惰性删除**不会主动删除过期键,而是在每次请求时才会判断此值是否过期,如果过期则删除键值,否则就返回 null。它的优点是只会占用少量的系统资源,缺点是清除不够及时,会造成一定的空间浪费。 + +**定期删除**是指每隔一段时间检查一次数据库,随机删除一些过期键值。 + +Redis 使用的是定期删除和惰性删除这两种策略,我们本课时也会参照这两种策略。 + +先来说一下自定义缓存的实现思路,首先需要定义一个存放缓存值的实体类,这个类里包含了缓存的相关信息,比如缓存的 key 和 value,缓存的存入时间、最后使用时间和命中次数(预留字段,用于支持 LFU 缓存淘汰),再使用 ConcurrentHashMap 保存缓存的 key 和 value 对象(缓存值的实体类),然后再新增一个缓存操作的工具类,用于添加和删除缓存,最后再缓存启动时,开启一个无限循环的线程用于检测并删除过期的缓存,实现代码如下。 + +首先,定义一个缓存值实体类,代码如下: + +```java +import lombok.Getter; +import lombok.Setter; + +/** + * 缓存实体类 + */ +@Getter +@Setter +public class CacheValue implements Comparable { +    // 缓存键 +    private Object key; +    // 缓存值 +    private Object value; +    // 最后访问时间 +    private long lastTime; +    // 创建时间 +    private long writeTime; +    // 存活时间 +    private long expireTime; +    // 命中次数 +    private Integer hitCount; + +    @Override +    public int compareTo(CacheValue o) { +        return hitCount.compareTo(o.hitCount); +    } +} +``` + +然后定义一个全局缓存对象,代码如下: + +```java +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * Cache 全局类 + */ +public class CacheGlobal { +    // 全局缓存对象 +    public static ConcurrentMap concurrentMap = new ConcurrentHashMap<>(); +} +``` + +定义过期缓存检测类的代码如下: + +```java +import java.util.concurrent.TimeUnit; + +/** + * 过期缓存检测线程 + */ +public class ExpireThread implements Runnable { +    @Override +    public void run() { +        while (true) { +            try { +                // 每十秒检测一次 +                TimeUnit.SECONDS.sleep(10); +                // 缓存检测和清除的方法 +                expireCache(); +            } catch (Exception e) { +                e.printStackTrace(); +            } +        } +    } + +    /** +     * 缓存检测和清除的方法 +     */ +    private void expireCache() { +        System.out.println("检测缓存是否过期缓存"); +        for (String key : CacheGlobal.concurrentMap.keySet()) { +            MyCache cache = CacheGlobal.concurrentMap.get(key); +            // 当前时间 - 写入时间 +            long timoutTime = TimeUnit.NANOSECONDS.toSeconds( +                    System.nanoTime() - cache.getWriteTime()); +            if (cache.getExpireTime() > timoutTime) { +                // 没过期 +                continue; +            } +            // 清除过期缓存 +            CacheGlobal.concurrentMap.remove(key); +        } +    } +} +``` + +接着,我们要新增一个缓存操作的工具类,用于查询和存入缓存,实现代码如下: + +```java +import org.apache.commons.lang3.StringUtils; + +import java.util.concurrent.TimeUnit; + +/** + * 缓存操作工具类 + */ +public class CacheUtils { + + /** + * 添加缓存 + * @param key + * @param value + * @param expire + */ + public void put(String key, Object value, long expire) { + // 非空判断,借助 commons-lang3 + if (StringUtils.isBlank(key)) return; + // 当缓存存在时,更新缓存 + if (CacheGlobal.concurrentMap.containsKey(key)) { + MyCache cache = CacheGlobal.concurrentMap.get(key); + cache.setHitCount(cache.getHitCount() + 1); + cache.setWriteTime(System.currentTimeMillis()); + cache.setLastTime(System.currentTimeMillis()); + cache.setExpireTime(expire); + cache.setValue(value); + return; + } + // 创建缓存 + MyCache cache = new MyCache(); + cache.setKey(key); + cache.setValue(value); + cache.setWriteTime(System.currentTimeMillis()); + cache.setLastTime(System.currentTimeMillis()); + cache.setHitCount(1); + cache.setExpireTime(expire); + CacheGlobal.concurrentMap.put(key, cache); + } + + /** + * 获取缓存 + * @param key + * @return + */ + public Object get(String key) { + // 非空判断 + if (StringUtils.isBlank(key)) return null; + // 字典中不存在 + if (CacheGlobal.concurrentMap.isEmpty()) return null; + if (!CacheGlobal.concurrentMap.containsKey(key)) return null; + MyCache cache = CacheGlobal.concurrentMap.get(key); + if (cache == null) return null; + // 惰性删除,判断缓存是否过期 + long timoutTime = TimeUnit.NANOSECONDS.toSeconds( + System.nanoTime() - cache.getWriteTime()); + // 缓存过期 + if (cache.getExpireTime() <= timoutTime) { + // 清除过期缓存 + CacheGlobal.concurrentMap.remove(k + return null; + } + cache.setHitCount(cache.getHitCount() + 1); + cache.setLastTime(System.currentTimeMillis()); + return cache.getValue(); + } +} +``` + +最后是调用缓存的测试代码: + +```java +public class MyCacheTest { +    public static void main(String[] args) { +        CacheUtils cache = new CacheUtils(); +        // 存入缓存 +        cache.put("key", "老王", 10); +        // 查询缓存 +        String val = (String) cache.get("key"); +        System.out.println(val); +        // 查询不存在的缓存 +        String noval = (String) cache.get("noval"); +        System.out.println(noval); +    } +} +``` + +以上程序的执行结果如下: + +``` +老王 +null +``` + +到目前为止,自定义缓存系统就已经实现完了。 + +# 小结 + +本课时讲解了本地缓存和分布式缓存这两个概念和实现的具体方式,其中本地缓存可以通过自己手动编码或借助 Guava Cache 来实现,而分布式缓存可以使用 Redis 或 EhCache 来实现。此外,本课时重点演示了手动实现缓存代码的方式和实现思路,并使用定期删除和惰性删除策略来实现缓存的清除。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/10.如何手写一个消息队列和延迟消息队列?.md b/Java基础教程/Java面试原理/10.如何手写一个消息队列和延迟消息队列?.md new file mode 100644 index 00000000..08ec6144 --- /dev/null +++ b/Java基础教程/Java面试原理/10.如何手写一个消息队列和延迟消息队列?.md @@ -0,0 +1,244 @@ +# 如何手写一个消息队列和延迟消息队列? + +第一次听到“消息队列”这个词时,不知你是不是和我反应一样,感觉很高阶很厉害的样子,其实当我们了解了消息队列之后,发现它与普通的技术类似,当我们熟悉之后,也能很快地上手并使用。 + +我们本课时的面试题是,消息队列的使用场景有哪些?如何手动实现一个消息队列和延迟消息队列? + +## 典型回答 + +消息队列的使用场景有很多,最常见的使用场景有以下几个。 + +### 1.商品秒杀 + +比如,我们在做秒杀活动时,会发生短时间内出现爆发式的用户请求,如果不采取相关的措施,会导致服务器忙不过来,响应超时的问题,轻则会导致服务假死,重则会让服务器直接宕机,给用户带来的体验也非常不好。如果这个时候加上了消息队列,服务器接收到用户的所有请求后,先把这些请求全部写入到消息队列中再排队处理,这样就不会导致同时处理多个请求的情况;如果消息队列长度超过可以承载的最大数量,那么我们可以抛弃当前用户的请求,通知前台用户“页面出错啦,请重新刷新”等提示,这样就会有更好的交互体验。 + +### 2.系统解耦 + +使用了消息队列之后,我们可以把系统的业务功能模块化,实现系统的解耦。例如,在没有使用消息队列之前,当前台用户完善了个人信息之后,首先我们需要更新用户的资料,再添加一条用户信息修改日志。但突然有一天产品经理提了一个需求,在前台用户信息更新之后,需要给此用户的增加一定的积分奖励,然后没过几天产品经理又提了一个需求,在前台用户信息更新之后,不但要增加积分奖励,还要增加用户的经验值,但没过几天产品经理的需求又变了,他要求完善资料无需增加用户的积分了,这样反反复复、来来回回的折腾,我想研发的同学一定受不了,但这是互联网公司的常态,那我们有没有一劳永逸的办法呢? + +没错,这个时候我们想到了使用消息队列来实现系统的解耦,每个功能的实现独立开,只需要一个订阅或者取消订阅的开关就可以了,当需要增加功能时,只需要打开订阅“用户信息完善”的队列就行,如果过两天不用了,再把订阅的开关关掉就行了,这样我们就不用来来回回的改业务代码了,也就轻松的实现了系统模块间的解耦。 + +### 3.日志记录 + +我们大部分的日志记录行为其实是和前台用户操作的主业务没有直接关系的,只是我们的运营人和经营人员需要拿到这部分用户操作的日志信息,来进行用户行为分析或行为监控。在我们没有使用消息队列之前,笼统的做法是当有用户请求时,先处理用户的请求再记录日志,这两个操作是放在一起的,而前台用户也需要等待日志添加完成之后才能拿到后台的响应信息,这样其实浪费了前台用户的部分时间。此时我们可以使用消息队列,当响应完用户请求之后,只需要把这个操作信息放入消息队列之后,就可以直接返回结果给前台用户了,无序等待日志处理和日志添加完成,从而缩短了前台用户的等待时间。 + +我们可以通过 JDK 提供的 Queue 来实现自定义消息队列,使用 DelayQueue 实现延迟消息队列。 + +## 考点分析 +对于消息队列的考察更侧重于消息队列的核心思想,因为只有理解了什么是消息队列?以及什么情况下要用消息队列?才能解决我们日常工作中遇到的问题,而消息队列的具体实现,只需要掌握一个消息中间件的使用即可,因为消息队列中间件的核心实现思路是一致的,不但如此,消息队列中间件的使用也大致类似,只要掌握了一个就能触类旁通的用好其他消息中间件。 + +和本课时相关的面试题,还有以下这两个: + +* 介绍一个你熟悉的消息中间件? +* 如何手动实现消息队列? + +# 知识扩展 +## 1.常用消息中间件 RabbitMQ + +目前市面上比较常用的 MQ(Message Queue,消息队列)中间件有 RabbitMQ、Kafka、RocketMQ,如果是轻量级的消息队列可以使用 Redis 提供的消息队列,本课时我们先来介绍一下 RabbitMQ。 + +RabbitMQ 是一个老牌开源的消息中间件,它实现了标准的 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)消息中间件,使用 Erlang 语言开发,支持集群部署,和多种客户端语言混合调用,它支持的主流开发语言有以下这些: + +* Java and Spring + +* .NET + +* Ruby + +* Python + +* PHP + +* JavaScript and Node + +* Objective-C and Swift + +* Rust + +* Scala + +* Go + +更多支持语言,请点击这里访问官网查看。 + +RabbitMQ 中有 3 个重要的概念:生产者、消费者和代理。 + +* 生产者:消息的创建者,负责创建和推送数据到消息服务器。 + +* 消费者:消息的接收方,用于处理数据和确认消息。 + +* 代理:也就是 RabbitMQ 服务本身,它用于扮演“快递”的角色,因为它本身并不生产消息,只是扮演了“快递”的角色,把消息进行暂存和传递。 + +它们的运行流程,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200607112505.png) + +RabbitMQ 具备以下几个优点: + +* **支持持久化**,RabbitMQ 支持磁盘持久化功能,保证了消息不会丢失; +* **高并发**,RabbitMQ 使用了 Erlang 开发语言,Erlang 是为电话交换机开发的语言,天生自带高并发光环和高可用特性; +* **支持分布式集群**,正是因为 Erlang 语言实现的,因此 RabbitMQ 集群部署也非常简单,只需要启动每个节点并使用 --link 把节点加入到集群中即可,并且 RabbitMQ 支持自动选主和自动容灾; +* **支持多种语言**,比如 Java、.NET、PHP、Python、JavaScript、Ruby、Go 等; +* **支持消息确认**,支持消息消费确认(ack)保证了每条消息可以被正常消费; +* **支持很多插件**,比如网页控制台消息管理插件、消息延迟插件等,RabbitMQ 的插件很多并且使用都很方便。 + +RabbitMQ 的消息类型,分为以下四种: + +* **direct(默认类型)模式**,此模式为一对一的发送方式,也就是一条消息只会发送给一个消费者; +* **headers 模式**,允许你匹配消息的 header 而非路由键(RoutingKey),除此之外 headers 和 direct 的使用完全一致,但因为 headers 匹配的性能很差,几乎不会被用到; +* **fanout 模式**,为多播的方式,会把一个消息分发给所有的订阅者; +* **topic 模式**,为主题订阅模式,允许使用通配符(#、*)匹配一个或者多个消息,我可以使用“cn.mq.#”匹配到多个前缀是“cn.mq.xxx”的消息,比如可以匹配到“cn.mq.rabbit”、“cn.mq.kafka”等消息。 + +## 2.自定义消息队列 +我们可使用 Queue 来实现消息队列,Queue 大体可分为以下三类: + +* **双端队列(Deque)**是 Queue 的子类也是 Queue 的补充类,头部和尾部都支持元素插入和获取; +* **阻塞队列**指的是在元素操作时(添加或删除),如果没有成功,会阻塞等待执行,比如当添加元素时,如果队列元素已满,队列则会阻塞等待直到有空位时再插入; +* **非阻塞队列**,和阻塞队列相反,它会直接返回操作的结果,而非阻塞等待操作,双端队列也属于非阻塞队列。 + +自定义消息队列的实现代码如下: + +```java +import java.util.LinkedList; +import java.util.Queue; + +public class CustomQueue { +    // 定义消息队列 +    private static Queue queue = new LinkedList<>(); + +    public static void main(String[] args) { +        producer(); // 调用生产者 +        consumer(); // 调用消费者 +    } + +    // 生产者 +    public static void producer() { +        // 添加消息 +        queue.add("first message."); +        queue.add("second message."); +        queue.add("third message."); +    } + +    // 消费者 +    public static void consumer() { +        while (!queue.isEmpty()) { +            // 消费消息 +            System.out.println(queue.poll()); +        } +    } +} +``` + +以上程序的执行结果是: + +``` +first message. +second message. +third message. +``` + +可以看出消息是以先进先出顺序进行消费的。 + +实现自定义延迟队列需要实现 Delayed 接口,重写 getDelay() 方法,延迟队列完整实现代码如下: + +```java +import lombok.Getter; +import lombok.Setter; + +import java.text.DateFormat; +import java.util.Date; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +/** + * 自定义延迟队列 + */ +public class CustomDelayQueue { +    // 延迟消息队列 +    private static DelayQueue delayQueue = new DelayQueue(); + +    public static void main(String[] args) throws InterruptedException { +        producer(); // 调用生产者 +        consumer(); // 调用消费者 +    } + +    // 生产者 +    public static void producer() { +        // 添加消息 +        delayQueue.put(new MyDelay(1000, "消息1")); +        delayQueue.put(new MyDelay(3000, "消息2")); +    } + +    // 消费者 +    public static void consumer() throws InterruptedException { +        System.out.println("开始执行时间:" + +                DateFormat.getDateTimeInstance().format(new Date())); +        while (!delayQueue.isEmpty()) { +            System.out.println(delayQueue.take()); +        } +        System.out.println("结束执行时间:" + +                DateFormat.getDateTimeInstance().format(new Date())); +    } + +    /** +     * 自定义延迟队列 +     */ +    static class MyDelay implements Delayed { +        // 延迟截止时间(单位:毫秒) +        long delayTime = System.currentTimeMillis(); + +        // 借助 lombok 实现 +        @Getter +        @Setter +        private String msg; + +        /** +         * 初始化 +         * @param delayTime 设置延迟执行时间 +         * @param msg       执行的消息 +         */ +        public MyDelay(long delayTime, String msg) { +            this.delayTime = (this.delayTime + delayTime); +            this.msg = msg; +        } + +        // 获取剩余时间 +        @Override +        public long getDelay(TimeUnit unit) { +            return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); +        } + +        // 队列里元素的排序依据 +        @Override +        public int compareTo(Delayed o) { +            if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) { +                return 1; +            } else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) { +                return -1; +            } else { +                return 0; +            } +        } + +        @Override +        public String toString() { +            return this.msg; +        } +    } +} +``` + +以上程序的执行结果是: + +``` +开始时间:2020-6-7 +MyDelay{msg='消息1'} +MyDelay{msg='消息2'} +结束时间:2020-6-7 +``` + +可以看出,消息 1 和消息 2 都实现了延迟执行的功能。 + +# 小结 +本课时讲了消息队列的使用场景:商品秒杀、系统解耦和日志记录,我们还介绍了 RabbitMQ 以及它的消息类型和它的特点等内容,同时还使用 Queue 的子类 LinkedList 实现了自定义消息队列,使用 DelayQueue 实现了自定义延迟消息队列。 diff --git a/Java基础教程/Java面试原理/11.底层源码分析Spring的核心功能和执行流程(上).md b/Java基础教程/Java面试原理/11.底层源码分析Spring的核心功能和执行流程(上).md new file mode 100644 index 00000000..c45d227e --- /dev/null +++ b/Java基础教程/Java面试原理/11.底层源码分析Spring的核心功能和执行流程(上).md @@ -0,0 +1,422 @@ +# 底层源码分析 Spring 的核心功能和执行流程(上) + +Spring Framework 已是公认的 Java 标配开发框架了,甚至还有人说 Java 编程就是面向 Spring 编程的,可见 Spring 在整个 Java 体系中的重要位置。 + +Spring 中包含了众多的功能和相关模块,比如 spring-core、spring-beans、spring-aop、spring-context、spring-expression、spring-test 等,本课时先从面试中必问的问题出发,来帮你更好的 Spring 框架。 + +我们本课时的面试题是,Spring Bean 的作用域有哪些?它的注册方式有几种? + +## 典型回答 + +在 Spring 容器中管理一个或多个 Bean,这些 Bean 的定义表示为 BeanDefinition 对象,这些对象包含以下重要信息: + +* Bean 的实际实现类 +* Bean 的作用范围 +* Bean 的引用或者依赖项 + +### Bean 的三种注册方式 + +* XML 配置文件的注册方式 +* Java 注解的注册方式 +* Java API 的注册方式 + +#### 1. XML 配置文件注册方式 + +```xml + +    +    + +``` + +#### 2. Java 注解注册方式 + +可以使用 @Component 注解方式来注册 Bean,代码如下: + +```java +@Component +public class Person { +   private Integer id; +   private String name +   // 忽略其他方法 +} +``` + +也可以使用 @Bean 注解方式来注册 Bean,代码如下: + +```java +@Configuration +public class Person { +   @Bean +   public Person  person(){ +      return new Person(); +   } +   // 忽略其他方法 +} +``` + +其中 @Configuration 可理解为 XML 配置里的 `` 标签,而 @Bean 可理解为用 XML 配置里面的 `` 标签。 + +#### 3. Java API 注册方式 + +使用 BeanDefinitionRegistry.registerBeanDefinition() 方法的方式注册 Bean,代码如下: + +```java +public class CustomBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + RootBeanDefinition personBean = new RootBeanDefinition(Person.class); + // 新增 Bean + registry.registerBeanDefinition("person", personBean); + } +} +``` + +### Bean的作用域 + +#### 1.singleton作用域 + +表示在 Spring 容器中只有一个 Bean 实例,以单例的形式存在,是默认的 Bean 作用域。 + +配置方式,缺省即可,XML 的配置方式如下: + +```xml + +``` + +#### 2. prototype作用域 + +原型作用域,每次调用 Bean 时都会创建一个新实例,也就是说每次调用 getBean() 方法时,相当于执行了 new Bean()。 + +XML 的配置方式如下: + +```xml + +``` + +#### 3.request作用域 + +每次 Http 请求时都会创建一个新的 Bean,该作用域仅适应于 WebApplicationContext 环境。 + +XML 的配置方式如下: + +```xml + +``` + +Java注解的配置方式如下: + +```java +@Scope(WebApplicationContext.SCOPE_REQUEST) +``` + +或是: + +```java +@RequestScope(WebApplicationContext.SCOPE_REQUEST) +``` + +#### 4.session作用域 + +同一个 Http Session 共享一个 Bean 对象,不同的 Session 拥有不同的 Bean 对象,仅适用于 WebApplicationContext 环境。 + +XML 的配置方式如下: + +```xml + +``` + +Java注解的配置方式如下: + +```java +@Scope(WebApplicationContext.SCOPE_SESSION) +``` + +或是: + +```java +@RequestScope(WebApplicationContext.SCOPE_SESSION) +``` + +#### 5.application作用域 + +全局的 Web 作用域,类似于 Servlet 中的 Application。 + +XML 的配置方式如下: + +```xml + +``` + +Java注解的配置方式如下: + +```java +@Scope(WebApplicationContext.SCOPE_APPLICATION) +``` + +或是: + +```java +@RequestScope(WebApplicationContext.SCOPE_APPLICATION) +``` + +## 考点分析 + +在 Spring 中最核心的概念是 AOP(面向切面编程)、IOC(控制反转)、DI(依赖注入)等,而最实用的功能则是 Bean,他们是概念和具体实现的关系。和 Bean 相关的面试题,还有以下几个: + +* 什么是同名 Bean?它是如何产生的?应该如何避免? +* 聊一聊 Bean 的生命周期。 + +# 知识扩展 + +## 1.同名Bean问题 + +每个 Bean 拥有一个或多个标识符,在基于 XML 的配置中,我们可以使用 id 或者 name 来作为 Bean 的标识符。通常 Bean 的标识符由字母组成,允许使用特殊字符。 + +同一个 Spring 配置文件中 Bean 的 id 和 name 是不能够重复的,否则 Spring 容器启动时会报错。但如果 Spring 加载了多个配置文件的话,可能会出现同名 Bean 的问题。同名 Bean 指的是多个 Bean 有相同的 name 或者 id。 + +Spring 对待同名 Bean 的处理规则是使用最后面的 Bean 覆盖前面的 Bean,所以我们在定义 Bean 时,尽量使用长命名非重复的方式来定义,避免产生同名 Bean 的问题。 + +Bean 的 id 或 name 属性并非必须指定,如果留空的话,容器会为 Bean 自动生成一个唯一的名称,这样也不会出现同名 Bean 的问题。 + +## 2.Bean生命周期 + +对于 Spring Bean 来说,并不是启动阶段就会触发 Bean 的实例化,只有当客户端通过显式或者隐式的方式调用 BeanFactory 的 getBean() 方法时,它才会触发该类的实例化方法。当然对于 BeanFactory 来说,也不是所有的 getBean() 方法都会实例化 Bean 对象,例如作用域为 singleton 时,只会在第一次,实例化该 Bean 对象,之后会直接返回该对象。但如果使用的是 ApplicationContext 容器,则会在该容器启动的时候,立即调用注册到该容器所有 Bean 的实例化方法。 + +getBean() 既然是 Bean 对象的入口,我们就先从这个方法说起,getBean() 方法是属于 BeanFactory 接口的,它的真正实现是 AbstractAutowireCapableBeanFactory 的 createBean() 方法,而 createBean() 是通过 doCreateBean() 来实现的,具体源码实现如下: + +```java +@Override +protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) +        throws BeanCreationException { +    if (logger.isTraceEnabled()) { +        logger.trace("Creating instance of bean '" + beanName + "'"); +    } +    RootBeanDefinition mbdToUse = mbd; +    // 确定并加载 Bean 的 class +    Class resolvedClass = resolveBeanClass(mbd, beanName); +    if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { +        mbdToUse = new RootBeanDefinition(mbd); +        mbdToUse.setBeanClass(resolvedClass); +    } +    // 验证以及准备需要覆盖的方法 +    try { +        mbdToUse.prepareMethodOverrides(); +    } +    catch (BeanDefinitionValidationException ex) { +        throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), +                beanName, "Validation of method overrides failed", ex); +    } +    try { +        // 给BeanPostProcessors 一个机会来返回代理对象来代替真正的 Bean 实例,在这里实现创建代理对象功能 +        Object bean = resolveBeforeInstantiation(beanName, mbdToUse); +        if (bean != null) { +            return bean; +        } +    } +    catch (Throwable ex) { +        throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, +                "BeanPostProcessor before instantiation of bean failed", ex); +    } +    try { +        // 创建 Bean +        Object beanInstance = doCreateBean(beanName, mbdToUse, args); +        if (logger.isTraceEnabled()) { +            logger.trace("Finished creating instance of bean '" + beanName + "'"); +        } +        return beanInstance; +    } +    catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { +        throw ex; +    } +    catch (Throwable ex) { +        throw new BeanCreationException( +                mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); +    } +} +``` + +doCreateBean 源码如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) +        throws BeanCreationException { +    // 实例化 bean,BeanWrapper 对象提供了设置和获取属性值的功能 +    BeanWrapper instanceWrapper = null; +    // 如果 RootBeanDefinition 是单例,则移除未完成的 FactoryBean 实例的缓存 +    if (mbd.isSingleton()) { +        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); +    } +    if (instanceWrapper == null) { +        // 创建 bean 实例 +        instanceWrapper = createBeanInstance(beanName, mbd, args); +    } +    // 获取 BeanWrapper 中封装的 Object 对象,其实就是 bean 对象的实例 +    final Object bean = instanceWrapper.getWrappedInstance(); +    // 获取 BeanWrapper 中封装 bean 的 Class +    Class beanType = instanceWrapper.getWrappedClass(); +    if (beanType != NullBean.class) { +        mbd.resolvedTargetType = beanType; +    } +    // 应用 MergedBeanDefinitionPostProcessor 后处理器,合并 bean 的定义信息 +    // Autowire 等注解信息就是在这一步完成预解析,并且将注解需要的信息放入缓存 +    synchronized (mbd.postProcessingLock) { +        if (!mbd.postProcessed) { +            try { +                applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); +            } catch (Throwable ex) { +                throw new BeanCreationException(mbd.getResourceDescription(), beanName, +                        "Post-processing of merged bean definition failed", ex); +            } +            mbd.postProcessed = true; +        } +    } +    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && +            isSingletonCurrentlyInCreation(beanName)); +    if (earlySingletonExposure) { +        if (logger.isTraceEnabled()) { +            logger.trace("Eagerly caching bean '" + beanName + +                    "' to allow for resolving potential circular references"); +        } +        // 为了避免循环依赖,在 bean 初始化完成前,就将创建 bean 实例的 ObjectFactory 放入工厂缓存(singletonFactories) +        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +    } +    // 对 bean 属性进行填充 +    Object exposedObject = bean; +    try { +        populateBean(beanName, mbd, instanceWrapper); +        // 调用初始化方法,如 init-method 注入 Aware 对象 +        exposedObject = initializeBean(beanName, exposedObject, mbd); +    } catch (Throwable ex) { +        if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { +            throw (BeanCreationException) ex; +        } else { +            throw new BeanCreationException( +                    mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); +        } +    } +    if (earlySingletonExposure) { +        // 如果存在循环依赖,也就是说该 bean 已经被其他 bean 递归加载过,放入了提早公布的 bean 缓存中 +        Object earlySingletonReference = getSingleton(beanName, false); +        if (earlySingletonReference != null) { +            // 如果 exposedObject 没有在 initializeBean 初始化方法中被增强 +            if (exposedObject == bean) { +                exposedObject = earlySingletonReference; +            } else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { +                // 依赖检测 +                String[] dependentBeans = getDependentBeans(beanName); +                Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); +                for (String dependentBean : dependentBeans) { +                    if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { +                        actualDependentBeans.add(dependentBean); +                    } +                } +                // 如果 actualDependentBeans 不为空,则表示依赖的 bean 并没有被创建完,即存在循环依赖 +                if (!actualDependentBeans.isEmpty()) { +                    throw new BeanCurrentlyInCreationException(beanName, +                            "Bean with name '" + beanName + "' has been injected into other beans [" + +                                    StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + +                                    "] in its raw version as part of a circular reference, but has eventually been " + +                                    "wrapped. This means that said other beans do not use the final version of the " + +                                    "bean. This is often the result of over-eager type matching - consider using " + +                                    "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); +                } +            } +        } +    } +    try { +        // 注册 DisposableBean 以便在销毁时调用 +        registerDisposableBeanIfNecessary(beanName, bean, mbd); +    } catch (BeanDefinitionValidationException ex) { +        throw new BeanCreationException( +                mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); +    } +    return exposedObject; +} +``` + +从上述源码中可以看出,在 doCreateBean() 方法中,首先对 Bean 进行了实例化工作,它是通过调用 createBeanInstance() 方法来实现的,该方法返回一个 BeanWrapper 对象。BeanWrapper 对象是 Spring 中一个基础的 Bean 接口,说它是基础接口是因为它连基本的属性都没有。 + +BeanWrapper 接口有一个默认实现类 BeanWrapperImpl,其主要作用是对 Bean 进行填充,比如填充和注入 Bean 的属性等。 + +当 Spring 完成 Bean 对象实例化并且设置完相关属性和依赖后,则会调用 Bean 的初始化方法 initializeBean(),初始化第一个阶段是检查当前 Bean 对象是否实现了 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口,源码如下: + +```java +private void invokeAwareMethods(final String beanName, final Object bean) { +    if (bean instanceof Aware) { +        if (bean instanceof BeanNameAware) { +            ((BeanNameAware) bean).setBeanName(beanName); +        } +        if (bean instanceof BeanClassLoaderAware) { +            ClassLoader bcl = getBeanClassLoader(); +            if (bcl != null) { +                ((BeanClassLoaderAware) bean).setBeanClassLoader(bcl); +            } +        } +        if (bean instanceof BeanFactoryAware) { +            ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); +        } +    } +} +``` + +其中,BeanNameAware 是把 Bean 对象定义的 beanName 设置到当前对象实例中; + +BeanClassLoaderAware 是将当前 Bean 对象相应的 ClassLoader 注入到当前对象实例中; + +BeanFactoryAware 是 BeanFactory 容器会将自身注入到当前对象实例中,这样当前对象就会拥有一个 BeanFactory 容器的引用。 + +初始化第二个阶段则是 BeanPostProcessor 增强处理,它主要是对 Spring 容器提供的 Bean 实例对象进行有效的扩展,允许 Spring 在初始化 Bean 阶段对其进行定制化修改,比如处理标记接口或者为其提供代理实现。 + +在初始化的前置处理完成之后就会检查和执行 InitializingBean 和 init-method 方法。 + +InitializingBean 是一个接口,它有一个 afterPropertiesSet() 方法,在 Bean 初始化时会判断当前 Bean 是否实现了 InitializingBean,如果实现了则调用 afterPropertiesSet() 方法,进行初始化工作;然后再检查是否也指定了 init-method,如果指定了则通过反射机制调用指定的 init-method 方法,它的实现源码如下: + +```java +protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) +        throws Throwable { +    // 判断当前 Bean 是否实现了 InitializingBean,如果是的话需要调用 afterPropertiesSet() +    boolean isInitializingBean = (bean instanceof InitializingBean); +    if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { +        if (logger.isTraceEnabled()) { +            logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); +        } +        if (System.getSecurityManager() != null) { // 安全模式 +            try { +                AccessController.doPrivileged((PrivilegedExceptionAction) () -> { +                    ((InitializingBean) bean).afterPropertiesSet(); // 属性初始化 +                    return null; +                }, getAccessControlContext()); +            } catch (PrivilegedActionException pae) { +                throw pae.getException(); +            } +        } else { +            ((InitializingBean) bean).afterPropertiesSet(); // 属性初始化 +        } +    } +    // 判断是否指定了 init-method() +    if (mbd != null && bean.getClass() != NullBean.class) { +        String initMethodName = mbd.getInitMethodName(); +        if (StringUtils.hasLength(initMethodName) && +                !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && +                !mbd.isExternallyManagedInitMethod(initMethodName)) { +            // 利用反射机制执行指定方法 +            invokeCustomInitMethod(beanName, bean, mbd); +        } +    } +} +``` + +初始化完成之后就可以正常的使用 Bean 对象了,在 Spring 容器关闭时会执行销毁方法,但是 Spring 容器不会自动去调用销毁方法,而是需要我们主动的调用。 + +如果是 BeanFactory 容器,那么我们需要主动调用 destroySingletons() 方法,通知 BeanFactory 容器去执行相应的销毁方法;如果是 ApplicationContext 容器,那么我们需要主动调用 registerShutdownHook() 方法,告知 ApplicationContext 容器执行相应的销毁方法。 + +> 注:本课时源码基于 Spring 5.2.2.RELEASE。 + +# 小结 + Bean 的三种注册方式:XML、Java 注解和 JavaAPI,以及 Bean 的五个作用域:singleton、prototype、request、session 和 application;还讲了读取多个配置文件可能会出现同名 Bean 的问题,以及通过源码讲了 Bean 执行的生命周期,它的生命周期如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200612141655.png) \ No newline at end of file diff --git a/Java基础教程/Java面试原理/12.底层源码分析 Spring 的核心功能和执行流程(下).md b/Java基础教程/Java面试原理/12.底层源码分析 Spring 的核心功能和执行流程(下).md new file mode 100644 index 00000000..cc51ae5d --- /dev/null +++ b/Java基础教程/Java面试原理/12.底层源码分析 Spring 的核心功能和执行流程(下).md @@ -0,0 +1,366 @@ +# 底层源码分析 Spring 的核心功能和执行流程(下) + +上一课时我们讲了 Bean 相关的内容,它其实也是属于 IOC 的具体实现之一,本课时我们就来讲讲 Spring 中其他几个高频的面试点,希望能起到抛砖引玉的作用,能为你理解 Spring 打开一扇门。因为 Spring 涉及的内容和知识点太多了,用它来写一本书也绰绰有余,因此这里我们只讲核心的内容,希望下来你能查漏补缺,完善自己的 Spring 技术栈。 + +我们本课时的面试题是,谈一谈你对 IOC 和 DI 的理解。 + +## 典型回答 + +**IOC**(Inversion of Control,翻译为“控制反转”)不是一个具体的技术,而是一种设计思想。与传统控制流相比,IOC 会颠倒控制流,在传统的编程中需要开发者自行创建并销毁对象,而在 IOC 中会把这些操作交给框架来处理,这样开发者就不用关注具体的实现细节了,拿来直接用就可以了,这就是控制反转。 + +IOC 很好的体现出了面向对象的设计法则之一——好莱坞法则:“别找我们,我们找你”。即由 IOC 容器帮对象找到相应的依赖对象并注入,而不是由对象主动去找。 + +举个例子,比如说传统找对象,先要设定好你的要求,如身高、体重、长相等,然后再一个一个的主动去找符合要求的对象,而 IOC 相当于,你把这些要求直接告诉婚介中心,由他们直接给你匹配到符合要求的对象,理想情况下是直接会帮你找到合适的对象,这就是传统编程模式和 IOC 的区别。 + +**DI**(Dependency Injection,翻译为“依赖注入”)表示组件间的依赖关系交由容器在运行期自动生成,也就是说,由容器动态的将某个依赖关系注入到组件之中,这样就能提升组件的重用频率。通过依赖注入机制,我们只需要通过简单的配置,就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心资源来自哪里、由谁实现等问题。 + +IOC 和 DI 其实是同一个概念从不同角度的描述的,由于控制反转这个概念比较含糊(可能只理解成了容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年被开发者尊称为“教父”的 Martin Fowler(世界顶级专家,敏捷开发方法的创始人之一)又给出了一个新的名字“依赖注入”,相对 IOC 而言,“依赖注入”明确描述了“被注入对象依赖 IOC 容器配置依赖对象”。 + +## 考点分析 + +IOC 和 DI 为 Spring 框架设计的精髓所在,也是面试中必问的考点之一,这个优秀的设计思想对于初学者来说可能理解起来比较困难,但对于 Spring 的使用者来说可以很快的看懂。因此如果对于此概念还有疑问的话,建议先上手使用 Spring 实现几个小功能再回头来看这些概念,相信你会豁然开朗。 + +Spring 相关的高频面试题,还有以下这些: + +* Spring IOC 有哪些优势? +* IOC 的注入方式有哪些? +* 谈一谈你对 AOP 的理解。 + +# 知识扩展 + +## 1.Spring IOC 的优点 +IOC 的优点有以下几个: + +* 使用更方便,拿来即用,无需显式的创建和销毁的过程; +* 可以很容易提供众多服务,比如事务管理、消息服务等; +* 提供了单例模式的支持; +* 提供了 AOP 抽象,利用它很容易实现权限拦截、运行期监控等功能; +* 更符合面向对象的设计法则; +* 低侵入式设计,代码的污染极低,降低了业务对象替换的复杂性。 + +## 2.Spring IOC 注入方式汇总 +IOC 的注入方式有三种:构造方法注入、Setter 注入和接口注入。 + +### 1.构造方法注入 + +构造方法注入主要是依赖于构造方法去实现,构造方法可以是有参的也可以是无参的,我们平时 new 对象时就是通过类的构造方法来创建类对象的,每个类对象默认会有一个无参的构造方法,Spring 通过构造方法注入的代码示例如下: + +```java +public class Person { +    public Person() { + } + public Person(int id, String name) { + this.id = id; + this.name = name; + } + private int id; + private String name; +    // 忽略 Setter、Getter 的方法 +} +``` + +applicationContext.xml 配置如下: + +```xml + +     +     + +``` + +### 2.Setter注入 + +Setter 方法注入的方式是目前 Spring 主流的注入方式,它可以利用 Java Bean 规范所定义的 Setter/Getter 方法来完成注入,可读性和灵活性都很高,它不需要使用声明式构造方法,而是使用 Setter 注入直接设置相关的值,实现示例如下: + +```xml + +     +     + +``` + +### 3.接口注入 + +接口注入方式是比较古老的注入方式,因为它需要被依赖的对象实现不必要的接口,带有侵入性,因此现在已经被完全舍弃了,所以本文也不打算做过多的描述,大家只要知道有这回事就行了。 + +## 3.Spring AOP + +AOP(Aspect-Oriented-Programming,面向切面编程)可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善,OOP 引入封装、继承和多态性等概念来建立一种公共对象处理的能力,当我们需要处理公共行为的时候,OOP 就会显得无能为力,而 AOP 的出现正好解决了这个问题。比如统一的日志处理模块、授权验证模块等都可以使用 AOP 很轻松的处理。 + +Spring AOP 目前提供了三种配置方式: + +* 基于 Java API 的方式; +* 基于 @AspectJ(Java)注解的方式; +* 基于 XML` `标签的方式。 + +### 1.基于Java API的方式 + +此配置方式需要实现相关的接口,例如 `MethodBeforeAdvice` 和 `AfterReturningAdvice`,并且在 XML 配置中定义相应的规则即可实现。 + +我们先来定义一个实体类,代码如下: + +```java +package org.springframework.beans; + + +public class Person { +   public Person findPerson() { +      Person person = new Person(1, "JDK"); +      System.out.println("findPerson 被执行"); +      return person; +   } +   public Person() { +   } +   public Person(Integer id, String name) { +      this.id = id; +      this.name = name; +   } +   private Integer id; +   private String name; +   // 忽略 Getter、Setter 方法 +} +``` + +再定义一个 advice 类,用于对拦截方法的调用之前和调用之后进行相关的业务处理,实现代码如下: + +```java +import org.springframework.aop.AfterReturningAdvice; +import org.springframework.aop.MethodBeforeAdvice; + +import java.lang.reflect.Method; + +public class MyAdvice implements MethodBeforeAdvice, AfterReturningAdvice { +   @Override +   public void before(Method method, Object[] args, Object target) throws Throwable { +      System.out.println("准备执行方法: " + method.getName()); +   } + +   @Override +   public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { +      System.out.println(method.getName() + " 方法执行结束"); +   } +``` + +然后需要在 application.xml 文件中配置相应的拦截规则,配置如下: + +```xml + + + + +     +     + + + + +``` + +从以上配置中可以看出,我们需要配置一个拦截方法的规则,然后定义一个 DefaultAdvisorAutoProxyCreator 让所有的 advisor 配置自动生效。 + +最后,我们使用测试代码来完成调用: + +```java +public class MyApplication { +   public static void main(String[] args) { +      ApplicationContext context = +            new ClassPathXmlApplicationContext("classpath*:application.xml"); +      Person person = context.getBean("person", Person.class); +      person.findPerson(); +   } +} +``` + +以上程序的执行结果为: + +``` +准备执行方法: findPerson +findPerson 被执行 +findPerson 方法执行结束 +``` + +可以看出 AOP 的拦截已经成功了。 + +### 2.基于@AspectJ注解的方式 + +首先需要在项目中添加 aspectjweaver 的 jar 包,配置如下: + +```xml + + +    org.aspectj +    aspectjweaver +    1.9.5 + +``` + +此 jar 包来自于 AspectJ,因为 Spring 使用了 AspectJ 提供的一些注解,因此需要添加此 jar 包。之后,我们需要开启 @AspectJ 的注解,开启方式有两种。 + +可以在 application.xml 配置如下代码中开启 @AspectJ 的注解: + +```xml + +``` + +也可以使用 `@EnableAspectJAutoProxy`注解开启,代码如下: + +```java +@Configuration +@EnableAspectJAutoProxy +public class AppConfig { +} +``` + +之后我们需要声明拦截器的类和拦截方法,以及配置相应的拦截规则,代码如下: + +```java +import org.aspectj.lang.annotation.After; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; + +@Aspect +public class MyAspectJ { + +   // 配置拦截类 Person +   @Pointcut("execution(* org.springframework.beans.Person.*(..))") +   public void pointCut() { +   } + +   @Before("pointCut()") +   public void doBefore() { +      System.out.println("执行 doBefore 方法"); +   } + +   @After("pointCut()") +   public void doAfter() { +      System.out.println("执行 doAfter 方法"); + } +} +``` + +然后我们只需要在 application.xml 配置中添加注解类,配置如下: + +```xml + +``` + +紧接着,我们添加一个需要拦截的方法: + +```java +package org.springframework.beans; + +// 需要拦截的 Bean +public class Person { +   public Person findPerson() { +      Person person = new Person(1, "JDK"); +      System.out.println("执行 findPerson 方法"); +      return person; +   } +    // 获取其他方法 +} +``` + +最后,我们开启测试代码: + +```java +import org.springframework.beans.Person; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class MyApplication { +   public static void main(String[] args) { +      ApplicationContext context = +            new ClassPathXmlApplicationContext("classpath*:application.xml"); +      Person person = context.getBean("person", Person.class); +      person.findPerson(); +   } +} +``` + +以上程序的执行结果为: + +``` +执行 doBefore 方法 +执行 findPerson 方法 +执行 doAfter 方法 +``` + +可以看出 AOP 拦截成功了。 + +### 3.基于 XML `` 标签的方式 + +基于 XML 的方式与基于注解的方式类似,只是无需使用注解,把相关信息配置到 application.xml 中即可,配置如下: + +```xml + + + +     +     +     +     +         +         +     + +``` + +之后,添加一个普通的类来进行拦截业务的处理,实现代码如下: + +```java +public class MyPointcut { +   public void doBefore() { +      System.out.println("执行 doBefore 方法"); +   } +   public void doAfter() { +      System.out.println("执行 doAfter 方法"); +   } +} +``` + +拦截的方法和测试代码与第二种注解的方式相同,这里就不在赘述。 + +最后执行程序,执行结果为: + +``` +执行 doBefore 方法 +执行 findPerson 方法 +执行 doAfter 方法 +``` + +可以看出 AOP 拦截成功了。 + +Spring AOP 的原理其实很简单,它其实就是一个动态代理,我们在调用 getBean() 方法的时候返回的其实是代理类的实例,而这个代理类在 Spring 中使用的是 JDK Proxy 或 CgLib 实现的,它的核心代码在 DefaultAopProxyFactory#createAopProxy(...) 中,源码如下: + +```java +public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { + + @Override + public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } +            // 判断目标类是否为接口 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { +                // 是接口使用 jdk 的代理 + return new JdkDynamicAopProxy(config); + } +            // 其他情况使用 CgLib 代理 + return new ObjenesisCglibAopProxy(config); + } + else { + return new JdkDynamicAopProxy(config); + } + } +    // 忽略其他代码 +} +``` + +# 小结 + +本课时讲了 IOC 和 DI 概念,以及 IOC 的优势和 IOC 注入的三种方式:构造方法注入、Setter 注入和接口注入,最后讲了 Spring AOP 的概念与它的三种配置方式:基于 Java API 的方式、基于 Java 注解的方式和基于 XML 标签的方式。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/13.MyBatis使用了哪些设计模式?在源码中是如何体现的?.md b/Java基础教程/Java面试原理/13.MyBatis使用了哪些设计模式?在源码中是如何体现的?.md new file mode 100644 index 00000000..68d37104 --- /dev/null +++ b/Java基础教程/Java面试原理/13.MyBatis使用了哪些设计模式?在源码中是如何体现的?.md @@ -0,0 +1,314 @@ +# MyBatis 使用了哪些设计模式?在源码中是如何体现的? + +MyBatis 的前身是 IBatis,IBatis 是由 Internet 和 Abatis 组合而成,其目的是想当做互联网的篱笆墙,围绕着数据库提供持久化服务的一个框架,2010 年正式改名为 MyBatis。它是一款优秀的持久层框架,支持自定义 SQL、存储过程及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作,还可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Ordinary Java Object,普通 Java 对象)为数据库中的记录。 + +关于 MyBatis 的介绍与使用,官方已经提供了比较详尽的中文参考文档,可点击这里查看,而本课时则以面试的角度出发,聊一聊不一样的知识点,它也是 MyBatis 比较热门的面试题之一,MyBatis 使用了哪些设计模式?在源码中是如何体现的? + +注意:本课时使用的 MyBatis 源码为 3.5.5。 + +## 典型回答 + +### 1.工厂模式 +工厂模式想必都比较熟悉,它是 Java 中最常用的设计模式之一。工厂模式就是提供一个工厂类,当有客户端需要调用的时候,只调用这个工厂类就可以得到自己想要的结果,从而无需关注某类的具体实现过程。这就好比你去餐馆吃饭,可以直接点菜,而不用考虑厨师是怎么做的。 + +工厂模式在 MyBatis 中的典型代表是 SqlSessionFactory。 + +SqlSession 是 MyBatis 中的重要 Java 接口,可以通过该接口来执行 SQL 命令、获取映射器示例和管理事务,而 SqlSessionFactory 正是用来产生 SqlSession 对象的,所以它在 MyBatis 中是比较核心的接口之一。 + +工厂模式应用解析:SqlSessionFactory 是一个接口类,它的子类 DefaultSqlSessionFactorys 有一个 openSession(ExecutorType execType) 的方法,其中使用了工厂模式,源码如下: + +```java +private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { + Transaction tx = null; + try { + final Environment environment = configuration.getEnvironment(); + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); + final Executor executor = configuration.newExecutor(tx, execType); + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + closeTransaction(tx); + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } +} +``` + +从该方法我们可以看出它会 configuration.newExecutor(tx, execType) 读取对应的环境配置,而此方法的源码如下: + +```java +public Executor newExecutor(Transaction transaction, ExecutorType executorType) { + executorType = executorType == null ? defaultExecutorType : executorType; + executorType = executorType == null ? ExecutorType.SIMPLE : executorType; + Executor executor; + if (ExecutorType.BATCH == executorType) { + executor = new BatchExecutor(this, transaction); + } else if (ExecutorType.REUSE == executorType) { + executor = new ReuseExecutor(this, transaction); + } else { + executor = new SimpleExecutor(this, transaction); + } + if (cacheEnabled) { + executor = new CachingExecutor(executor); + } + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + +可以看出 newExecutor() 方法为标准的工厂模式,它会根据传递 ExecutorType 值生成相应的对象然后进行返回。 + +### 2.建造者模式(Builder) +建造者模式指的是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。也就是说建造者模式是通过多个模块一步步实现了对象的构建,相同的构建过程可以创建不同的产品。 + +例如,组装电脑,最终的产品就是一台主机,然而不同的人对它的要求是不同的,比如设计人员需要显卡配置高的;而影片爱好者则需要硬盘足够大的(能把视频都保存起来),但对于显卡却没有太大的要求,我们的装机人员根据每个人不同的要求,组装相应电脑的过程就是建造者模式。 + +建造者模式在 MyBatis 中的典型代表是 SqlSessionFactoryBuilder。 + +普通的对象都是通过 new 关键字直接创建的,但是如果创建对象需要的构造参数很多,且不能保证每个参数都是正确的或者不能一次性得到构建所需的所有参数,那么就需要将构建逻辑从对象本身抽离出来,让对象只关注功能,把构建交给构建类,这样可以简化对象的构建,也可以达到分步构建对象的目的,而 SqlSessionFactoryBuilder 的构建过程正是如此。 + +在 SqlSessionFactoryBuilder 中构建 SqlSessionFactory 对象的过程是这样的,首先需要通过 XMLConfigBuilder 对象读取并解析 XML 的配置文件,然后再将读取到的配置信息存入到 Configuration 类中,然后再通过 build 方法生成我们需要的 DefaultSqlSessionFactory 对象,实现源码如下(在 SqlSessionFactoryBuilder 类中): + +```java +public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { + try { + XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); + return build(parser.parse()); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error building SqlSession.", e); + } finally { + ErrorContext.instance().reset(); + try { + inputStream.close(); + } catch (IOException e) { + // Intentionally ignore. Prefer previous error. + } + } +} +public SqlSessionFactory build(Configuration config) { + return new DefaultSqlSessionFactory(config); +} +``` + +SqlSessionFactoryBuilder 类相当于一个建造工厂,先读取文件或者配置信息、再解析配置、然后通过反射生成对象,最后再把结果存入缓存,这样就一步步构建造出一个 SqlSessionFactory 对象。 + +### 3.单例模式 +单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。 + +单例模式也比较好理解,比如一个人一生当中只能有一个真实的身份证号,每个收费站的窗口都只能一辆车子一辆车子的经过,类似的场景都是属于单例模式。 + +单例模式在 MyBatis 中的典型代表是 ErrorContext。 + +ErrorContext 是线程级别的的单例,每个线程中有一个此对象的单例,用于记录该线程的执行环境的错误信息。 + +ErrorContext 的实现源码如下: + +```java +public class ErrorContext { + private static final String LINE_SEPARATOR = System.lineSeparator(); + // 每个线程存储的容器 + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(ErrorContext::new); + public static ErrorContext instance() { + return LOCAL.get(); + } + // 忽略其他 +} +``` + +可以看出 ErrorContext 使用 private 修饰的 ThreadLocal 来保证每个线程拥有一个 ErrorContext 对象,在调用 instance() 方法时再从 ThreadLocal 中获取此单例对象。 + +### 4.适配器模式 +适配器模式是指将一个不兼容的接口转换成另一个可以兼容的接口,这样就可以使那些不兼容的类可以一起工作。 + +例如,最早之前我们用的耳机都是圆形的,而现在大多数的耳机和电源都统一成了方形的 typec 接口,那之前的圆形耳机就不能使用了,只能买一个适配器把圆形接口转化成方形的,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613153121.png) + +而这个转换头就相当于程序中的适配器模式,适配器模式在 MyBatis 中的典型代表是 Log。 + +MyBatis 中的日志模块适配了以下多种日志类型: + +* SLF4J +* Apache Commons Logging +* Log4j 2 +* Log4j +* JDK logging + +首先 MyBatis 定义了一个 Log 的接口,用于统一和规范接口的行为,源码如下: + +```java +public interface Log { + boolean isDebugEnabled(); + boolean isTraceEnabled(); + void error(String s, Throwable e); + void error(String s); + void debug(String s); + void trace(String s); + void warn(String s); +} +``` + +然后 MyBatis 定义了多个适配接口,例如 Log4j2 实现源码如下: + +```java +public class Log4j2Impl implements Log { + private final Log log; + public Log4j2Impl(String clazz) { + Logger logger = LogManager.getLogger(clazz); + if (logger instanceof AbstractLogger) { + log = new Log4j2AbstractLoggerImpl((AbstractLogger) logger); + } else { + log = new Log4j2LoggerImpl(logger); + } + } + @Override + public boolean isDebugEnabled() { + return log.isDebugEnabled(); + } + @Override + public boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + @Override + public void error(String s, Throwable e) { + log.error(s, e); + } + @Override + public void error(String s) { + log.error(s); + } + @Override + public void debug(String s) { + log.debug(s); + } + @Override + public void trace(String s) { + log.trace(s); + } + @Override + public void warn(String s) { + log.warn(s); + } +} +``` + +这样当你项目中添加了 Log4j2 时,MyBatis 就可以直接使用它打印 MyBatis 的日志信息了。Log 的所有子类如下图所示:![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613153328.png) + +### 5.代理模式 +代理模式指的是给某一个对象提供一个代理对象,并由代理对象控制原对象的调用。 + +代理模式在生活中也比较常见,比如我们常见的超市、小卖店其实都是一个个“代理”,他们的最上游是一个个生产厂家,他们这些代理负责把厂家生产出来的产品卖出去。 + +代理模式在 MyBatis 中的典型代表是 MapperProxyFactory。 + +MapperProxyFactory 的 newInstance() 方法就是生成一个具体的代理来实现功能的,源码如下: + +```java +public class MapperProxyFactory { + private final Class mapperInterface; + private final Map methodCache = new ConcurrentHashMap<>(); + public MapperProxyFactory(Class mapperInterface) { + this.mapperInterface = mapperInterface; + } + public Class getMapperInterface() { + return mapperInterface; + } + public Map getMethodCache() { + return methodCache; + } + // 创建代理类 + @SuppressWarnings("unchecked") + protected T newInstance(MapperProxy mapperProxy) { + return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); + } + public T newInstance(SqlSession sqlSession) { + final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); + return newInstance(mapperProxy); + } +} +``` + +### 6.模板方法模式 +模板方法模式是最常用的设计模式之一,它是指定义一个操作算法的骨架,而将一些步骤的实现延迟到子类中去实现,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。此模式是基于继承的思想实现代码复用的。 + +例如,我们喝茶的一般步骤都是这样的: + +* 把热水烧开 +* 把茶叶放入壶中 +* 等待一分钟左右 +* 把茶倒入杯子中 +* 喝茶 + +整个过程都是固定的,唯一变的就是泡入茶叶种类的不同,比如今天喝的是绿茶,明天可能喝的是红茶,那么我们就可以把流程定义为一个模板,而把茶叶的种类延伸到子类中去实现,这就是模板方法的实现思路。 + +模板方法在 MyBatis 中的典型代表是 BaseExecutor。 + +在 MyBatis 中 BaseExecutor 实现了大部分 SQL 执行的逻辑,然后再把几个方法交给子类来实现,它的继承关系如下图所示:![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613153623.png) + +比如 doUpdate() 就是交给子类自己去实现的,它在 BaseExecutor 中的定义如下: + +```java +protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; +``` + +在 SimpleExecutor 中的实现如下: + +```java +public class SimpleExecutor extends BaseExecutor { + // 构造方法 + public SimpleExecutor(Configuration configuration, Transaction transaction) { + super(configuration, transaction); + } + // 更新方法 + @Override + public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); + stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.update(stmt); + } finally { + closeStatement(stmt); + } + } + // 忽略其他代码... +} +``` + +可以看出 SimpleExecutor 每次使用完 Statement 对象之后,都会把它关闭掉,而 ReuseExecutor 中的实现源码如下: + +```java +public class ReuseExecutor extends BaseExecutor { + private final Map statementMap = new HashMap<>(); + public ReuseExecutor(Configuration configuration, Transaction transaction) { + super(configuration, transaction); + } + // 更新方法 + @Override + public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); + Statement stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.update(stmt); + } + // 忽略其他代码... +} +``` + +可以看出,ReuseExecutor 每次使用完 Statement 对象之后不会把它关闭掉。 + +### 7.装饰器模式 +装饰器模式允许向一个现有的对象添加新的功能,同时又不改变其结构,这种类型的设计模式属于结构型模式,它是作为现有类的一个包装。 + +装饰器模式在生活中很常见,比如装修房子,我们在不改变房子结构的同时,给房子添加了很多的点缀;比如安装了天然气报警器,增加了热水器等附加的功能都属于装饰器模式。 + +装饰器模式在 MyBatis 中的典型代表是 Cache。 + +Cache 除了有数据存储和缓存的基本功能外(由 PerpetualCache 永久缓存实现),还有其他附加的 Cache 类,比如先进先出的 FifoCache、最近最少使用的 LruCache、防止多线程并发访问的 SynchronizedCache 等众多附加功能的缓存类,Cache 所有实现子类如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613154009.png) + +# 小结 +本课时我们重点讲了 MyBatis 源码中的几个主要设计模式,即工厂模式、建造者模式、单例模式、适配器模式、代理模式、模板方法模式等,希望本课时的内容能起到抛砖引玉的作用,对你理解设计模式和 MyBatis 提供一些帮助,如果想要阅读全部的 MyBatis 源码可以访问:https://github.com/mybatis/mybatis-3 。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/14.SpringBoot有哪些优点?它和Spring有什么区别?.md b/Java基础教程/Java面试原理/14.SpringBoot有哪些优点?它和Spring有什么区别?.md new file mode 100644 index 00000000..24863fe5 --- /dev/null +++ b/Java基础教程/Java面试原理/14.SpringBoot有哪些优点?它和Spring有什么区别?.md @@ -0,0 +1,266 @@ +# SpringBoot 有哪些优点?它和 Spring 有什么区别? + +Spring 和 Spring Boot 的区别很多新手容易搞混,从这道简单的面试题也可以很轻易试探出你的 Java 基础功底,如果连这个问题都答不上来的话,通常就没有什么下文了,因为这已经是用人单位对面试者的最低要求了,所以本课时我们就来看一下二者的区别,以及 Spring Boot 的特性。 + +我们本课时的面试题是,Spring 和 Spring Boot 有什么区别?Spring Boot 的优点有哪些? + +## 典型回答 + +作为 Java 开发人员对 Spring 框架都很熟悉,Spring 为 Java 程序提供了全面的基础架构支持,包含了很多非常实用的功能,如 Spring JDBC、Spring AOP、Spring ORM、Spring Test 等,这些模块的出现,大大的缩短了应用程序的开发时间,同时提高了应用开发的效率。 + +Spring Boot 本质上是 Spring 框架的延伸和扩展,它的诞生是为了简化 Spring 框架初始搭建以及开发的过程,使用它可以不再依赖 Spring 应用程序中的 XML 配置,为更快、更高效的开发 Spring 提供更加有力的支持。Spring Boot 具体的特性如下。 + +### Spring Boot 特性一:更快速的构建能力 +Spring Boot 提供了更多的 Starters 用于快速构建业务框架,Starters 可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找依赖包。 + +例如在 Spring 中如果要创建 Web 应用程序的最小依赖项为: + +```xml + + org.springframework + spring-web + xxx + + + org.springframework + spring-webmvc + xxx + +``` + +而 Spring Boot 只需要一个依赖项就可以来启动和运行 Web 应用程序,如下所示: + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +当我们添加了 Starter 模块支持之后,在项目的构建期,它就会把所有其他依赖项将自动添加到项目中。 + +这样的例子还有很多,比如测试库,如果是 Spring 项目我们通常要添加 Spring Test、JUnit、Hamcrest 和 Mockito 库;而如果是 Spring Boot 项目的话,只需要添加 spring-boot-starter-test 即可,它会自动帮我们把其他的依赖项添加到项目中。 + +常见的 Starters 有以下几个: + +* spring-boot-starter-test +* spring-boot-starter-web +* spring-boot-starter-data-jpa +* spring-boot-starter-thymeleaf + +点击这里访问文档,查看完整的 Starters 列表。 + +### Spring Boot 特性二:起步依赖 +Spring Boot 提供了起步依赖,也就是在创建 Spring Boot 时可以直接勾选依赖模块,这样在项目初始化时就会把相关依赖直接添加到项目中,大大缩短了查询并添加依赖的时间,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613155541.png) + +### Spring Boot 特性三:内嵌容器支持 +Spring Boot 内嵌了 Tomcat、Jetty、Undertow 三种容器,其默认嵌入的容器是 Tomcat,这个在我们启动 Spring Boot 项目的时候,在控制台上就能看到,具体信息如下: + +> o.s.b.w.embedded.tomcat.TomcatWebServer :Tomcat started on port(s): 8080 (http) with context path '' + +可以看出 Spring Boot 默认使用的是 Tomcat 容器启动的。 + +我们可以通过修改 pom.xml 来移除内嵌的 Tomcat 更换为其他的容器,比如更换为 Jetty 容器,配置如下: + +```xml + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-jetty + +``` + +当我们添加完成之后,再重新生成 pom.xml 文件,然后再启动 Spring Boot 项目容器信息就变了,如下所示: + +> o.e.jetty.server.AbstractConnector: Started ServerConnector@53f9009d{HTTP/1.1, (http/1.1)}{0.0.0.0:8080} +> o.s.b.web.embedded.jetty.JettyWebServer + +可以看出 Spring Boot 使用了我们指定的 Jetty 容器启动了。 + +### Spring Boot 特性四:Actuator 监控 +Spring Boot 自带了 Actuator 监控功能,主要用于提供对应用程序监控,以及控制的能力,比如监控应用程序的运行状况,或者内存、线程池、Http 请求统计等,同时还提供了关闭应用程序等功能。 + +Actuator 提供了 19 个接口,接口请求地址和代表含义如下表所示: + +| 访问路径 | 描述 | +| :---------------: | :----------------------------------------------------------: | +| /auditevents | 显示应用暴露的审计事件(比如认证进入) | +| /beans | 显示应用程序中所有 Spring Bean 的完整列表 | +| /caches | 公开可用的缓存 | +| /conditions | 显示在配置和自动配置类上评估的条件以及它们匹配或不匹配的原因 | +| /configprops | 显示所有 @ConfigurationPropertie 的整理列表 | +| /env | 获取全部环境属性 | +| /flyway | 提供一份 Flyway 数据库迁移信息 | +| /health | 显示应用程序运行状况信息 | +| /httptrace | 显示 HTTP 跟踪信息(默认情况下,最近 100 个 HTTP 请求-响应交换) | +| /info | 获取应用程序的定制信息,这些信息由 info 开头的属性提供 | +| /integrationgraph | 显示 Spring Integration 图,需要依赖于 spring-integration-core | +| /loggers | 显示和修改应用程序的配置 | +| /liquibase | 显示已应用的所有 Liquibase 数据库迁移 | +| /metrics/{name} | 报告指定名称的应用程序度量值 | +| /mappings | 显示所有 @RequestMapping 路径的列表 | +| /scheduledtasks | 显示应用程序中的计划任务 | +| /sessions | 允许从 Spring Session 支持的会话存储中检索和删除用户会话,需要使用 Spring Session 基于 Servlet 的 Web 应用程序 | +| /shutdown | 使应用程序正常关闭,默认禁用 | +| /threaddump | 获取线程活动的快照 | + +## 考点分析 +很多人都知道 Spring Boot 是基于 Spring 的,使用它可以更加快速高效的构建 Spring,然而当面试官问到 Spring Boot 是如何高效构建 Spring 时,可能大部分人回答不上来了,上面讲解的 Spring Boot 四大特性基本涵盖了此问题的答案。如果面试官继续追问更深的细节的话,可能会问到关于 Spring Boot 执行的源码细节,比如 Spring Boot 的启动流程是怎么样的? + +# 知识扩展 + +## Spring Boot 启动源码分析 +我们知道 Spring Boot 程序的入口是 SpringApplication.run(Application.class, args) 方法,那么就从 run() 方法开始分析吧,它的源码如下: + +```java +public ConfigurableApplicationContext run(String... args) { + // 1.创建并启动计时监控类 + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + // 2.声明应用上下文对象和异常报告集合 + ConfigurableApplicationContext context = null; + Collection exceptionReporters = new ArrayList(); + // 3.设置系统属性 headless 的值 + this.configureHeadlessProperty(); + // 4.创建所有 Spring 运行监听器并发布应用启动事件 + SpringApplicationRunListeners listeners = this.getRunListeners(args); + listeners.starting(); + Collection exceptionReporters; + try { + // 5.处理 args 参数 + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + // 6.准备环境 + ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); + this.configureIgnoreBeanInfo(environment); + // 7.创建 Banner 的打印类 + Banner printedBanner = this.printBanner(environment); + // 8.创建应用上下文 + context = this.createApplicationContext(); + // 9.实例化异常报告器 + exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context); + // 10.准备应用上下文 + this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); + // 11.刷新应用上下文 + this.refreshContext(context); + // 12.应用上下文刷新之后的事件的处理 + this.afterRefresh(context, applicationArguments); + // 13.停止计时监控类 + stopWatch.stop(); + // 14.输出日志记录执行主类名、时间信息 + if (this.logStartupInfo) { + (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); + } + // 15.发布应用上下文启动完成事件 + listeners.started(context); + // 16.执行所有 Runner 运行器 + this.callRunners(context, applicationArguments); + } catch (Throwable var10) { + this.handleRunFailure(context, var10, exceptionReporters, listeners); + throw new IllegalStateException(var10); + } + try { + // 17.发布应用上下文就绪事件 + listeners.running(context); + // 18.返回应用上下文对象 + return context; + } catch (Throwable var9) { + this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null); + throw new IllegalStateException(var9); + } +} +``` + +从以上源码可以看出 Spring Boot 的启动总共分为以下 18 个步骤。 + +## Spring Boot 的启动流程 +### 1.创建并启动计时监控类 + +此计时器是为了监控并记录 Spring Boot 应用启动的时间的,它会记录当前任务的名称,然后开启计时器。 + +### 2.声明应用上下文对象和异常报告集合 + +此过程声明了应用上下文对象和一个异常报告的 ArrayList 集合。 + +### 3.设置系统属性 headless 的值 + +设置 Java.awt.headless = true,其中 awt(Abstract Window Toolkit)的含义是抽象窗口工具集。设置为 true 表示运行一个 headless 服务器,可以用它来作一些简单的图像处理。 + +### 4.创建所有 Spring 运行监听器并发布应用启动事件 + +此过程用于获取配置的监听器名称并实例化所有的类。 + +### 5.初始化默认应用的参数类 + +也就是说声明并创建一个应用参数对象。 + +### 6.准备环境 + +创建配置并且绑定环境(通过 property sources 和 profiles 等配置文件)。 + +### 7.创建 Banner 的打印类 + +Spring Boot 启动时会打印 Banner 图片,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200613160904.png) + +此 banner 信息是在 SpringBootBanner 类中定义的,我们可以通过实现 Banner 接口来自定义 banner 信息,然后通过代码 setBanner() 方法设置 Spring Boot 项目使用自己自定义 Banner 信息,或者是在 resources 下添加一个 banner.txt,把 banner 信息添加到此文件中,就可以实现自定义 banner 的功能了。 + +### 8.创建应用上下文 + +根据不同的应用类型来创建不同的 ApplicationContext 上下文对象。 + +### 9.实例化异常报告器 + +它调用的是 getSpringFactoriesInstances() 方法来获取配置异常类的名称,并实例化所有的异常处理类。 + +### 10.准备应用上下文 + +此方法的主要作用是把上面已经创建好的对象,传递给 prepareContext 来准备上下文,例如将环境变量 environment 对象绑定到上下文中、配置 bean 生成器以及资源加载器、记录启动日志等操作。 + +### 11.刷新应用上下文 + +此方法用于解析配置文件,加载 bean 对象,并且启动内置的 web 容器等操作。 + +### 12.应用上下文刷新之后的事件处理 + +这个方法的源码是空的,可以做一些自定义的后置处理操作。 + +### 13.停止计时监控类 + +停止此过程第一步中的程序计时器,并统计任务的执行信息。 + +### 14.输出日志信息 + +把相关的记录信息,如类名、时间等信息进行控制台输出。 + +### 15.发布应用上下文启动完成事件 + +触发所有 SpringApplicationRunListener 监听器的 started 事件方法。 + +### 16.执行所有 Runner 运行器 + +执行所有的 ApplicationRunner 和 CommandLineRunner 运行器。 + +### 17.发布应用上下文就绪事件 + +触发所有的 SpringApplicationRunListener 监听器的 running 事件。 + +### 18.返回应用上下文对象 + +到此为止 Spring Boot 的启动程序就结束了,我们就可以正常来使用 Spring Boot 框架了。 + +# 小结 +本课时首先讲了 Spring 和 Spring Boot 的区别,Spring Boot 本质上是 Spring 的延伸,它是基于 Spring 的,它为快速构建和开发 Spring 提供了有力的支撑;接着介绍了 Spring Boot 的四大特性:更快速的构建能力、起步依赖、内嵌容器支持、Actuator 监控支持等,最后 还介绍了 Spring Boot 启动的 18 个步骤。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/15.MQ有什么作用?你都用过哪些MQ中间件.md b/Java基础教程/Java面试原理/15.MQ有什么作用?你都用过哪些MQ中间件.md new file mode 100644 index 00000000..4394b252 --- /dev/null +++ b/Java基础教程/Java面试原理/15.MQ有什么作用?你都用过哪些MQ中间件.md @@ -0,0 +1,112 @@ +# MQ 有什么作用?你都用过哪些 MQ 中间件? + +在第10篇中讲过“手写消息队列”,当时粗略的讲了 Java API 中使用 Queue 实现自定义消息队列,以及使用 Delayed 实现延迟队列的示例;同时还讲了 RabbitMQ 中的一些基础概念。本课时我们将会更加深入的讲解 MQ(Message Queue,消息队列)中间件,以及这些热门中间件的具体使用。 + +我们本课时的面试题是,MQ 常见的使用场景有哪些?你都用过哪些 MQ 中间件? + +## 典型回答 + +在介绍 MQ 的使用场景之前,先来回忆一下 MQ 的作用。MQ 可以用来实现削峰填谷,也就是使用它可以解决短时间内爆发式的请求任务,在不使用 MQ 的情况下会导致服务处理不过来,出现应用程序假死的情况,而使用了 MQ 之后可以把这些请求先暂存到消息队列中,然后进行排队执行,那么就不会出现应用程序假死的情况了,所以它的第一个应用就是商品秒杀以及产品抢购等使用场景,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614145120.png) + +### 使用 MQ 实现消息通讯 + +使用 MQ 可以作为消息通讯的实现手段,利用它可以实现点对点的通讯或者多对多的聊天室功能。 + +点对点的消息通讯如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614145219.png) + +多对多的消息通讯如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614145241.png) + +### 使用 MQ 实现日志系统 +可使用 MQ 实现对日志的采集和转发,比如有多个日志写入到程序中,然后把日志添加到 MQ,紧接着由日志处理系统订阅 MQ,最后 MQ 将消息接收并转发给日志处理系统,这样就完成了日志的分析和保存功能,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614145326.png) + +常用的 MQ 中间件有 RabbitMQ、Kafka 和 Redis 等,其中 Redis 属于轻量级的消息队列,而 RabbitMQ、Kafka 属于比较成熟且比较稳定和高效的 MQ 中间件。 + +## 考点分析 + +MQ 属于中高级或优秀的程序员必备的技能,对于 MQ 中间件掌握的数量则是你技术广度和编程经验的直接体现信息之一。值得庆幸的是,关于 MQ 中间件的实现原理和使用方式都比较类似,因此如果开发者掌握一项 MQ 中间件再去熟悉其他 MQ 中间件时,会非常的容易。 + +MQ 相关的面试题还有这些: + +* MQ 的特点是什么?引入 MQ 中间件会带来哪些问题? +* 常见的 MQ 中间件的优缺点分析。 + +# 知识扩展 + +## MQ 的特点及注意事项 +### MQ 具有以下 5 个特点 + +* **先进先出**:消息队列的顺序一般在入列时就基本确定了,最先到达消息队列的信息,一般情况下也会先转发给订阅的消费者,我们把这种实现了先进先出的数据结构称之为队列。 +* **发布、订阅工作模式**:生产者也就是消息的创建者,负责创建和推送数据到消息服务器;消费者也就是消息的接收方,用于处理数据和确认消息的消费;消息队列也是 MQ 服务器中最重要的组成元素之一,它负责消息的存储,这三者是 MQ 中的三个重要角色。而它们之间的消息传递与转发都是通过发布以及订阅的工作模式来进行的,即生产者把消息推送到消息队列,消费者订阅到相关的消息后进行消费,在消息非阻塞的情况下,此模式基本可以实现同步操作的效果。并且此种工作模式会把请求的压力转移给 MQ 服务器,以减少了应用服务器本身的并发压力。 +* **持久化**:持久化是把消息从内存存储到磁盘的过程,并且在服务器重启或者发生宕机的情况下,重新启动服务器之后是保证数据不会丢失的一种手段,也是目前主流 MQ 中间件都会提供的重要功能。 +* **分布式**:MQ 的一个主要特性就是要应对大流量、大数据的高并发环境,一个单体的 MQ 服务器是很难应对这种高并发的压力的,所以 MQ 服务器都会支持分布式应用的部署,以分摊和降低高并发对 MQ 系统的冲击。 +* **消息确认**:消息消费确认是程序稳定性和安全性的一个重要考核指标,假如消费者在拿到消息之后突然宕机了,那么 MQ 服务器会误认为此消息已经被消费者消费了,从而造成消息丢失的问题,而目前市面上的主流 MQ 都实现了消息确认的功能,保证了消息不会丢失,从而保证了系统的稳定性。 + +### 引入 MQ 系统会带来的问题 + +任何系统的引入都是有两面性的,MQ 也不例外,在引入 MQ 之后,可能会带来以下两个问题。 + +* **增加了系统的运行风险**:引入 MQ 系统,则意味着新增了一套系统,并且其他的业务系统会对 MQ 系统进行深度依赖,系统部署的越多则意味着发生故障的可能性就越大,如果 MQ 系统挂掉的话可能会导致整个业务系统瘫痪。 +* **增加了系统的复杂度**:引入 MQ 系统后,需要考虑消息丢失、消息重复消费、消息的顺序消费等问题,同时还需要引入新的客户端来处理 MQ 的业务,增加了编程的运维门槛,增加了系统的复杂性。 + +### 使用 MQ 需要注意的问题 + +不要过度依赖 MQ,比如发送短信验证码或邮件等功能,这种低频但有可能比较耗时的功能可以使用多线程异步处理即可,不用任何的功能都依赖 MQ 中间件来完成,但像秒杀抢购可能会导致超卖(也就是把货卖多了,库存变成负数了)等短时间内高并发的请求,此时建议使用 MQ 中间件。 + +## 常用的 MQ 中间件 +常用的 MQ 中间件有 Redis、RabbitMQ、Kafka,下来我们分别来看看各自的作用。 + +### Redis 轻量级的消息中间件 + +Redis 是一个高效的内存性数据库中间件,但使用 Redis 也可以实现消息队列的功能。 + +早期的 Redis(Redis 5.0 之前)是不支持消息确认的,那时候我们可以通过 List 数据类型的 lpush 和 rpop 方法来实现队列消息的存入和读取功能,或者使用 Redis 提供的发布订阅(pub/sub)功能来实现消息队列,但这种模式不支持持久化,List 虽然支持持久化但不能设置复杂的路由规则来匹配多个消息,并且他们二者都不支持消息消费确认。 + +于是在 Redis 5.0 之后提供了新的数据类型 Stream 解决了消息确认的问题,但它同样不能提供复杂的路由匹配规则,因此在业务不复杂的场景下可以尝试性的使用 Redis 提供的消息队列。 + +### RabbitMQ + +在第10篇中,我们对 RabbitMQ 有过初步的讲解,它是一个实现了标准的高级消息队列协议(AMQP,Advanced Message Queuing Protocol)的老牌开源消息中间件,最初起源于金融系统,后来被普遍应用在了其他分布式系统中,它支持集群部署,和多种客户端调用。 + +之前主要介绍了 RabbitMQ 的基础功能,本课时我们重点来看 RabbitMQ 集群相关的内容。 + +RabbitMQ 集群是由多个节点组成,但默认情况下每个节点并不是存储所有队列的完整拷贝,这是出于存储空间和性能的考虑,因为如果存储了队列的完整拷贝,那么就会有很多冗余的重复数据,并且在新增节点的情况下,不但没有新增存储空间,反而需要更大的空间来存储旧的数据;同样的道理,如果每个节点都保存了所有队列的完整信息,那么非查询操作的性能就会很慢,就会需要更多的网络带宽和磁盘负载来存储这些数据。 + +为了能兼顾性能和稳定性,RabbitMQ 集群的节点分为两种类型,即磁盘节点和内存节点,对于磁盘节点来说显然它的优势就是稳定,可以把相关数据保存下来,若 RabbitMQ 因为意外情况宕机,重启之后保证了数据不丢失;而内存节点的优势是快,因为是在内存中进行数据交换和操作,因此性能比磁盘节点要高出很多倍。 + +如果是单个 RabbitMQ 那么就必须要求是磁盘节点,否则当 RabbitMQ 服务器重启之后所有的数据都会丢失,这样显然是不能接受的。在 RabbitMQ 的集群中,至少需要一个磁盘节点,这样至少能保证集群数据的相对可靠性。 + +如果集群中的某一个磁盘节点崩溃了,此时整个 RabbitMQ 服务也不会处于崩溃的状态,不过部分操作会受影响,比如不能创建队列、交换器、也不能添加用户及修改用户权限,更不能添加和删除集群的节点等功能。 + +> 小贴士:对于 RabbitMQ 集群来说,我们启动集群节点的顺序应该是先启动磁盘节点再启动内存节点,而关闭的顺序正好和启动的顺序相反,不然可能会导致 RabbitMQ 集群启动失败或者是数据丢失等异常问题。 + +### Kafka + +Kafka 是 LinkedIn 公司开发的基于 ZooKeeper 的多分区、多副本的分布式消息系统,它于 2010 年贡献给了 Apache 基金会,并且成为了 Apache 的顶级开源项目。其中 ZooKeeper 的作用是用来为 Kafka 提供集群元数据管理以及节点的选举和发现等功能。 + +与 RabbitMQ 比较类似,一个典型的 Kafka 是由多个 Broker、多个生产者和消费者,以及 ZooKeeper 集群组成的,其中 Broker 可以理解为一个代理,Kafka 集群中的一台服务器称之为一个 Broker,其组成框架图如下所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614150422.png) + +### Kafka VS RabbitMQ + +Kafka(2.0.0)和 RabbitMQ(3.6.10)的区别主要体现在以下几点: + +* Kafka 支持消息回溯,它可以根据 Offset(消息偏移量)、TimeStamp(时间戳)等维度进行消息回溯,而 RabbitMQ 并不支持消息回溯; +* Kafka 的消息消费是基于拉取数据的模式,也就是消费者主动向服务器端发送拉取消息请求,而 RabbitMQ 支持拉取数据模式和主动推送数据的模式,也就说 RabbitMQ 服务器会主动把消息推送给订阅的消费者; +* 在相同配置下,Kafka 的吞吐量通常会比 RabbitMQ 高一到两个级别,比如在单机模式下,RabbitMQ 的吞吐量大概是万级别的处理能力,而 Kafka 则可以到达十万甚至是百万的吞吐级别; +* Kafka 从 0.11 版本就开始支持幂等性了,当然所谓的幂等性指的是对单个生产者在单分区上的单会话的幂等操作,但对于全局幂等性则还需要结合业务来处理,比如,消费者在消费完一条消息之后没有来得及确认就发生异常了,等到恢复之后又得重新消费原来消费过的消息,类似这种情况,是无法在消息中间件层面来保证的,这个时候则需要引入更多的外部资源来保证全局幂等性,比如唯一的订单号、消费之前先做去重判断等;而 RabbitMQ 是没有幂等性功能支持的; +* RabbitMQ 支持多租户的功能,也就是常说的 Virtual Host(vhost),每一个 vhost 相当于一个独立的小型 RabbitMQ 服务器,它们拥有自己独立的交换器、消息队列及绑定关系等,并且拥有自己独立权限,而且多个 vhost 之间是绝对隔离的,但 Kafka 并不支持多租户的功能。 + +Kafka 和 RabbitMQ 都支持分布式集群部署,并且都支持数据持久化和消息消费确认等 MQ 的核心功能,对于 MQ 的选型要结合自己团队本身的情况,从性能、稳定性及二次开发的难易程度等维度来进行综合的考量并选择。 + +# 小结 + +本篇我们讲了 MQ 的常见使用场景,以及常见的 MQ 中间件(Redis、RabbitMQ、Kafka)及其优缺点分析;同时还了解了 MQ 的五大特点:先进先出、发布和订阅的模式、持久化、分布式和消息确认等;接着讲了 MQ 引入对系统可能带来的风险;最后讲了 MQ 在使用时需要注意的问题。希望本课时对你整体了解 MQ 系统有所帮助。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/16.MySQL 的运行机制是什么?它有哪些引擎?.md b/Java基础教程/Java面试原理/16.MySQL 的运行机制是什么?它有哪些引擎?.md new file mode 100644 index 00000000..a25fbb2e --- /dev/null +++ b/Java基础教程/Java面试原理/16.MySQL 的运行机制是什么?它有哪些引擎?.md @@ -0,0 +1,111 @@ +# MySQL 的运行机制是什么?它有哪些引擎? + +数据库是 Java 程序员面试必问的知识点之一,它和 Java 的核心面试点共同组成了一个完整的技术面试。而数据库一般泛指的就是 MySQL,因为 MySQL 几乎占据了数据库的半壁江山,即使有些公司没有使用 MySQL 数据库,如果你对 MySQL 足够精通的话,也是会被他们录取的。因为数据库的核心与原理基本是相通的,所以有了 MySQL 的基础之后,再去熟悉其他数据库也是非常快的,那么接下来的几个课时就让我们好好的学习一下 MySQL。 + +我们本课时的面试题是,MySQL 是如何运行的?说一下它有哪些引擎? + +## 典型回答 + +MySQL 的执行流程是这样的,首先客户端先要发送用户信息去服务器端进行授权认证。如果使用的是命令行工具,通常需要输入如下信息: + +> mysql -h 主机名(IP) -u 用户名 -P 端口 -p + +其中: + +* -h 表示要连接的数据库服务器的主机名或者 IP 信息; +* -u 表示数据库的用户名称; +* -P 表示数据库服务器的端口号, +* 小写的 -p 表示需要输入数据库的密码。 + +具体使用示例,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614151032.png) + +当输入正确密码之后可以连接到数据库了,如果密码输入错误,则会提示`“Access denied for user 'xxx'@'xxx' (using password: YES)”`密码错误信息,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614151107.png) + +当连接服务器端成功之后就可以正常的执行 SQL 命令了,MySQL 服务器拿到 SQL 命令之后,会使用 MySQL 的分析器解析 SQL 指令,同时会根据语法分析器验证 SQL 指令,查询 SQL 指令是否满足 MySQL 的语法规则。如果不支持此语法,则会提示“SQL syntax”语法错误信息。 + +当分析器验证并解析 SQL 命令之后,会进入优化器阶段,执行生成计划,并设置相应的索引;当上面的这些步骤都执行完之后,就进入了执行器阶段,并开始正式执行 SQL 命令。同样在执行命令之前,它会先对你的执行命令进行权限查询,看看是否有操作某个表的权限,如果有相应的权限,执行器就去调用 MySQL 数据库引擎提供的接口,执行相应的命令;如果是非查询操作会记录对应的操作日志,再命令执行完成之后返回结果给客户端,这就是整个 MySQL 操作的完整流程。 + +需要注意的是,如果执行的是 select 语句并且是 MySQL 8.0 之前的版本的话,则会去 MySQL 的查询缓存中查看之前是否有执行过这条 SQL;如果缓存中可以查到,则会直接返回查询结果,这样查询性能就会提升很高。 + +整个 SQL 的执行流程,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614151300.png) + +我们可以使用 SHOW ENGINES 命令来查看 MySQL 数据库使用的存储引擎,如下图所示: + +```sql +SHOW ENGINES +``` + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200614151407.png) + +常用的数据库引擎有 InnoDB、MyISAM、MEMORY 等,其中 InnoDB 支持事务功能,而 MyISAM 不支持事务,但 MyISAM 拥有较高的插入和查询的速度。而 MEMORY 是内存型的数据库引擎,它会将表中的数据存储到内存中,因为它是内存级的数据引擎,因此具备最快速的查询效率,但它的缺点是,重启数据库之后,所有数据都会丢失,因为这些数据是存放在内存中的。 + +## 考点分析 + +此面试题考察的是面试者对 MySQL 基础知识的掌握程度,以及对于 MySQL 引擎的了解程度,这些都是属于 MySQL 最核心的原理之一,也是面试中常见的面试问题,它一般作为数据库面试题的开始题目,和此面试题相关的面试点还有以下几个: + +* 查询缓存存在什么问题? +* 如何选择数据库的引擎? +* InnoDB 自增索引的持久化问题。 + +# 知识扩展 + +## 1.查询缓存的利弊 +MySQL 8.0 之前可以正常的使用查询缓存的功能,可通过`SHOW GLOBAL VARIABLES LIKE 'query_cache_type`命令查询数据库是否开启了查询缓存的功能,它的结果值有以下三项: + +* OFF,关闭了查询缓存功能; +* ON,开启了查询缓存功能; +* DEMAND,在 sql 语句中指定 sql_cache 关键字才会有查询缓存,也就是说必须使用 sql_cache 才可以把该 select 语句的查询结果缓存起来,比如`select sql_cache name from token where tid=1010`语句。 + +开启和关闭查询缓存可以通过修改 MySQL 的配置文件 my.cnf 进行修改,它的配置项如下: + +```cnf +query_cache_type = ON +``` + +> 注意:配置被更改之后需要重启 MySQL 服务才能生效。 + +查询缓存的功能要根据实际的情况进行使用,建议设置为按需缓存(DEMAND)模式,因为查询缓存的功能并不是那么好用。比如我们设置了 `query_cache_type = ON`,当我们好不容易缓存了很多查询语句之后,任何一条对此表的更新操作都会把和这个表关联的所有查询缓存全部清空,那么在更新频率相对较高的业务中,查询缓存功能完全是一个鸡肋。因此,在 MySQL 8.0 的版本中已经完全移除了此功能,也就是说在 MySQL 8.0 之后就完全没有查询缓存这个概念和功能了。 + +## 2.如何选择数据库引擎 +选择数据库引擎要从实际的业务情况入手,比如是否需要支持事务?是否需要支持外键?是否需要支持持久化?以及是否支持地理位置存储以及索引等方面进行综合考量。 + +我们最常用的数据库引擎是 InnoDB,它是 MySQL 5.5.5 之后的默认引擎,其优点是支持事务,且支持 4 种隔离级别。 + +* 读未提交:也就是一个事务还没有提交时,它做的变更就能被其他事务看到。 +* 读已提交:指的是一个事务只有提交了之后,其他事务才能看得到它的变更。 +* 可重复读:此方式为默认的隔离级别,它是指一个事务在执行过程中(从开始到结束)看到的数据都是一致的,在这个过程中未提交的变更对其他事务也是不可见的。 +* 串行化:是指对同一行记录的读、写都会添加读锁和写锁,后面访问的事务必须等前一个事务执行完成之后才能继续执行,所以这种事务的执行效率很低。 + +InnoDB 还支持外键、崩溃后的快速恢复、支持全文检索(需要 5.6.4+ 版本)、集群索引,以及地理位置类型的存储和索引等功能。 + +MyISAM 引擎是 MySQL 原生的引擎,但它并不支持事务功能,这也是后来被 InnoDB 替代为默认引擎的主要原因。MyISAM 有独立的索引文件,因此在读取数据方面的性能很高,它也支持全文索引、地理位置存储和索引等功能,但不支持外键。 + +InnoDB 和 MyISAM 都支持持久化,但 MEMORY 引擎是将数据直接存储在内存中了,因此在重启服务之后数据就会丢失,但它带来的优点是执行速度很快,可以作为临时表来使用。 + +我们可以根据实际的情况设置相关的数据库引擎,还可以针对不同的表设置不同的数据引擎,只需要在创建表的时候指定 engine=引擎名称即可,SQL 代码如下: + +```sql +create table student( + id int primary key auto_increment, + uname varchar(60), + age int +) engine=Memory; +``` + +### 3.InnoDB 自增主键 +在面试的过程中我们经常看到这样一道面试题: + +> 在一个自增表里面一共有 5 条数据,id 从 1 到 5,删除了最后两条数据,也就是 id 为 4 和 5 的数据,之后重启的 MySQL 服务器,又新增了一条数据,请问新增的数据 id 为几? + +我们通常的答案是如果表为 MyISAM 引擎,那么 id 就是 6,如果是 InnoDB 那么 id 就是 4。 + +但是这个情况在高版本的 InnoDB 中,也就是 MySQL 8.0 之后就不准确了,它的 id 就不是 4 了,而是 6 了。因为在 MySQL 8.0 之后 InnoDB 会把索引持久化到日志中,重启服务之后自增索引是不会丢失的,因此答案是 6,这个需要面试者注意一下。 + +# 小结 +本课时我们讲了 MySQL 数据库运行流程的几个阶段,先从连接器授权,再到分析器进行语法分析。如果是 MySQL 8.0 之前的 select 语句可能会先查询缓存,如果有缓存则会直接返回结果给客户端,否则会从分析器进入优化器生成 SQL 的执行计划,然后交给执行器调用操作引擎执行相关的 SQL,再把结果返回给客户端。我们还讲了最常见的三种数据库引擎 InnoDB、MyISAM、MEMORY,以及它们的利弊分析。最后讲了 InnoDB 在高版本(8.0)之后可以持久化自增主键的小特性。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/17.MySQL 的优化方案有哪些?.md b/Java基础教程/Java面试原理/17.MySQL 的优化方案有哪些?.md new file mode 100644 index 00000000..39fa633e --- /dev/null +++ b/Java基础教程/Java面试原理/17.MySQL 的优化方案有哪些?.md @@ -0,0 +1,135 @@ +# MySQL 的优化方案有哪些? + +性能优化(Optimize)指的是在保证系统正确性的前提下,能够更快速响应请求的一种手段。而且有些性能问题,比如慢查询等,如果积累到一定的程度或者是遇到急速上升的并发请求之后,会导致严重的后果,轻则造成服务繁忙,重则导致应用不可用。它对我们来说就像一颗即将被引爆的定时炸弹一样,时刻威胁着我们。因此在上线项目之前需要严格的把关,以确保 MySQL 能够以最优的状态进行运行。同时,在实际工作中还有面试中关于 MySQL 优化的知识点,都是面试官考察的重点内容。 + +我们本课时的面试题是,MySQL 的优化方案有哪些? + +## 典型回答 + +MySQL 数据库常见的优化手段分为三个层面:**SQL和索引优化**、**数据库结构优化**、**系统硬件优化**等,然而每个大的方向中又包含多个小的优化点,下面我们具体来看看。 + +### 1.SQL 和索引优化 +此优化方案指的是通过优化 SQL 语句以及索引来提高 MySQL 数据库的运行效率,具体内容如下。 + +#### 1.使用正确的索引 + +索引是数据库中最重要的概念之一,也是提高数据库性能最有效的手段之一,它的诞生本身就是为了提高数据查询效率的,就像字典的目录一样,通过目录可以很快找到相关的内容。 + +假如我们没有添加索引,那么在查询时就会触发全表扫描,因此查询的数据就会很多,并且查询效率会很低,为了提高查询的性能,我们就需要给最常使用的查询字段上,添加相应的索引,这样才能提高查询的性能。 + +> 小贴士:我们应该尽可能的使用主键查询,而非其他索引查询,因为主键查询不会触发回表查询,因此节省了一部分时间,变相的提高了查询的性能。 + +在 MySQL 5.0 之前的版本要尽量避免使用 or 查询,可以使用 union 或者子查询来替代,因为早期的 MySQL 版本使用 or 查询可能会导致索引失效,在 MySQL 5.0 之后的版本中引入了索引合并,简单来说就是把多条件查询,比如 or 或 and 查询的结果集进行合并交集或并集的功能,因此就不会导致索引失效的问题了。 + +避免在 where 查询条件中使用 != 或者 <> 操作符,因为这些操作符会导致查询引擎放弃索引而进行全表扫描。 + +适当使用前缀索引,MySQL 是支持前缀索引的,也就是说我们可以定义字符串的一部分来作为索引。我们知道索引越长占用的磁盘空间就越大,那么在相同数据页中能放下的索引值也就越少,这就意味着搜索索引需要的查询时间也就越长,进而查询的效率就会降低,所以我们可以适当的选择使用前缀索引,以减少空间的占用和提高查询效率。比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引。 + +#### 2.查询具体的字段而非全部字段 + +要尽量避免使用 select *,而是查询需要的字段,这样可以提升速度,以及减少网络传输的带宽压力。 + +#### 3.优化子查询 + +尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,但 Join 语句并不会创建临时表,因此性能会更高。 + +#### 4.注意查询结果集 + +我们要尽量使用小表驱动大表的方式进行查询,也就是如果 B 表的数据小于 A 表的数据,那执行的顺序就是先查 B 表再查 A 表,具体查询语句如下: + +```sql +select name from A where id in (select id from B); +``` + +#### 5.不要在列上进行运算操作 + +不要在列字段上进行算术运算或其他表达式运算,否则可能会导致查询引擎无法正确使用索引,从而影响了查询的效率。 + +#### 6.适当增加冗余字段 + +增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段,以减少多张表的关联查询,这是以空间换时间的优化策略。 + +### 2.数据库结构优化 +#### 1.最小数据长度 + +一般说来数据库的表越小,那么它的查询速度就越快,因此为了提高表的效率,应该将表的字段设置的尽可能小,比如身份证号,可以设置为 char(18) 就不要设置为 varchar(18)。 + +#### 2.使用最简单数据类型 + +能使用 int 类型就不要使用 varchar 类型,因为 int 类型比 varchar 类型的查询效率更高。 + +#### 3.尽量少定义 text 类型 + +text 类型的查询效率很低,如果必须要使用 text 定义字段,可以把此字段分离成子表,需要查询此字段时使用联合查询,这样可以提高主表的查询效率。 + +#### 4.适当分表、分库策略 + +分表和分库方案也是我们经常说的垂直分隔(分表)和水平分隔(分库)。 + +**分表**是指当一张表中的字段更多时,可以尝试将一张大表拆分为多张子表,把使用比较高频的主信息放入主表中,其他的放入子表,这样我们大部分查询只需要查询字段更少的主表就可以完成了,从而有效的提高了查询的效率。 + +**分库**是指将一个数据库分为多个数据库。比如我们把一个数据库拆分为了多个数据库,一个主数据库用于写入和修改数据,其他的用于同步主数据并提供给客户端查询,这样就把一个库的读和写的压力,分摊给了多个库,从而提高了数据库整体的运行效率。 + +### 3.硬件优化 +MySQL 对硬件的要求主要体现在三个方面:磁盘、网络和内存。 + +#### 1.磁盘 + +磁盘应该尽量使用有高性能读写能力的磁盘,比如固态硬盘,这样就可以减少 I/O 运行的时间,从而提高了 MySQL 整体的运行效率。 + +磁盘也可以尽量使用多个小磁盘而不是一个大磁盘,因为磁盘的转速是固定的,有多个小磁盘就相当于拥有多个并行运行的磁盘一样。 + +#### 2.网络 + +保证网络带宽的通畅(低延迟)以及够大的网络带宽是 MySQL 正常运行的基本条件,如果条件允许的话也可以设置多个网卡,以提高网络高峰期 MySQL 服务器的运行效率。 + +#### 3.内存 + +MySQL 服务器的内存越大,那么存储和缓存的信息也就越多,而内存的性能是非常高的,从而提高了整个 MySQL 的运行效率。 + +## 考点分析 + +MySQL 性能优化的方案很多,因此它可以全面考察的一个程序员的经验是否丰富。当然这个问题的回答也是可深可浅,不同的岗位对此问题的答案要求也是不同的,这个问题也可以引申出更多的面试问题,比如: + +* 联合索引需要注意什么问题? +* 如何排查慢查询? + +# 知识扩展 + +## 正确使用联合索引 +使用了 B+ 树的 MySQL 数据库引擎,比如 InnoDB 引擎,在每次查询复合字段时是从左往右匹配数据的,因此在创建联合索引的时候需要注意索引创建的顺序。例如,我们创建了一个联合索引是 idx(name,age,sex),那么当我们使用,姓名+年龄+性别、姓名+年龄、姓名等这种最左前缀查询条件时,就会触发联合索引进行查询;然而如果非最左匹配的查询条件,例如,性别+姓名这种查询条件就不会触发联合索引。 + +当然,当我们已经有了(name,age)这个联合索引之后,一般情况下就不需要在 name 字段单独创建索引了,这样就可以少维护一个索引。 + +## 慢查询 +慢查询通常的排查手段是先使用慢查询日志功能,查询出比较慢的 SQL 语句,然后再通过 explain 来查询 SQL 语句的执行计划,最后分析并定位出问题的根源,再进行处理。 + +慢查询日志指的是在 MySQL 中可以通过配置来开启慢查询日志的记录功能,超过 long_query_time 值的 SQL 将会被记录在日志中。我们可以通过设置“slow_query_log=1”来开启慢查询,它的开启方式有两种: + +* 通过 MySQL 命令行的模式进行开启,只需要执行“set global slow_query_log=1”即可,然而这种配置模式再重启 MySQL 服务之后就会失效; +* 另一种方式可通过修改 MySQL 配置文件的方式进行开启,我们需要配置 my.cnf 中的“slow_query_log=1”即可,并且可以通过设置“slow_query_log_file=/tmp/mysql_slow.log”来配置慢查询日志的存储目录,但这种方式配置完成之后需要重启 MySQL 服务器才可生效。 + +需要注意的是,在开启慢日志功能之后,会对 MySQL 的性能造成一定的影响,因此在生产环境中要慎用此功能。 + +explain 执行计划的使用示例 SQL 如下: + +```sql +explain select * from person where uname = 'Java'; +``` + +它的执行结果如下图所示: + +![](https://cdn.jsdelivr.net/gh/krislinzhao/IMGcloud/img/20200615205146.png) + +摘要说明如下表所示: + +![](https://cdn.jsdelivr.net/gh/krislinzhao/IMGcloud/img/20200615205237.png) + +以上字段中最重要的就是 type 字段,它的所有值如下所示: + +![](https://cdn.jsdelivr.net/gh/krislinzhao/IMGcloud/img/20200615205327.png) + +当 type 为 all 时,则表示全表扫描,因此效率会比较低,此时需要查看一下为什么会造成此种原因,是没有创建索引还是索引创建的有问题?以此来优化整个 MySQL 运行的速度。 + +# 小结 +本课时我们从三个维度讲了 MySQL 的优化手段:SQL 和索引优化、数据库结构优化以及系统硬件优化等;同时深入到每个维度中,详细地介绍了 MySQL 具体的优化细节;最后我们讲了联合索引的最左匹配原则,以及慢查询的具体解决方案。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/18.关系型数据和文档型数据库有什么区别?.md b/Java基础教程/Java面试原理/18.关系型数据和文档型数据库有什么区别?.md new file mode 100644 index 00000000..3fcff1bf --- /dev/null +++ b/Java基础教程/Java面试原理/18.关系型数据和文档型数据库有什么区别?.md @@ -0,0 +1,104 @@ +# 关系型数据和文档型数据库有什么区别? + +关系数据库(Relational Database)是建立在关系模型基础上的数据库,借助于几何代数等数学概念和方法来处理数据库中的数据。所谓关系模型是一对一、一对多或者多对多等关系,常见的关系型数据库有 Oracle、SQL Server、DB2、MySQL 等。 + +而文档型数据库是一种非关系型数据库,非关系型数据库(Not Only SQL,NoSQL)正好与关系型数据库相反,它不是建立在“关系模型”上的数据库。文档型数据库的典型代表是 MongoDB。 + +我们本课时的面试题是,关系型数据库和文档型数据库有什么区别? + +## 典型回答 + +关系型数据库属于早期的传统型数据库,它有着标准化的数据模型,以及事务和持久化的支持、例如,关系型数据库都会支持的 ACID 特性,也就是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),具体含义如下。 + +* **原子性(Atomicity)**:是指一个事务中的所有操作,要么全部完成、要么全部不完成,不会存在中间的状态。也就是说事务在正常的情况下会执行完成;异常的情况下,比如在执行的过程中如果出现问题,会回滚成最初的状态,而非中间状态。 +* **一致性(Consistency)**:是指事务从开始执行到结束执行之间的中间状态不会被其他事务看到。 +* **隔离性(Isolation)**:是指数据库允许多个事务同时对数据进行读写或修改的能力,并且整个过程对各个事务来说是相互隔离的。 +* **持久性(Durability)**:是指每次事务提交之后都不会丢失。 + +关系型数据库一般遵循三范式设计思想,具体内容如下。 + +**第一范式(The First Normal Form,1NF)**:要求对属性的原子性,也就是说要求数据库中的字段需要具备原子性,不能再被拆分。 + +比如,用户表中有字段:用户 ID、用户名、电话;而其中电话又可以分为:家庭电话和移动电话等。因此,此表不符合第一范式,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200615205857.png) + +**第二范式(The Second Normal Form,2NF)**:例如订单详情表有这些字段:订单 ID、产品 ID、产品名称、产品单价、折扣。其中,订单 ID 和产品 ID 为联合主键,但这个表中的产品名称和产品单价两个字段只依赖产品 ID,和订单 ID 就没有任何关系了,因此这个表也不符合第二范式。 + +我们可以把原来的订单表拆分为订单表和产品表,其中订单表包含:订单 ID、产品 ID、折扣等字段;而产品表包含:产品 ID、产品名称、产品单价等字段。这样就消除了产品名称和产品单价多次重复出现的情况了,从而避免了冗余数据的产生。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200615210023.png) + +**第三范式(The Third Normal Form,3NF)**:想要满足第三范式必须先满足第二范式,第三范式要求所有的非主键字段必须直接依赖主键,且不存在传递依赖的情况。 + +例如,有一个学生表中包含了:学生 ID、姓名、所在学院 ID、学院电话、学院地址等字段。这个表的所有字段(除去主键字段)都完全依赖唯一的主键字段(学生 ID),所以符合第二范式。但它存在一个问题,学院电话、学院地址依赖非主键字段学院 ID,而不是直接依赖于主键,它是通过传递才依赖于主键,所以不符合第三范式。 + +我们可以把学生表分为两张表,一张是学生表包含了:学生 ID、姓名、所在学院 ID 等字段;另一张为学院表包含了:学院 ID、学院电话、学院地址等字段,这样就满足第三范式的要求了。 + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200615210155.png) + +可以看出,使用三范式可以避免数据的冗余,而且在更新表操作时,只需要更新单张表就可以了。 + +但随着互联网应用的快速发展,我们需要应对日益复杂且快速迭代的数据库,以应对互联网快速发展的趋势,于是诞生了以 MongoDB 为代表的文档型数据库。它提供了更高效的读/写性能以及可自动容灾的数据库集群,还有灵活的数据库结构,从而给系统的数据库存储带来了更多可能 性。 + +当然 MongoDB 的诞生并不是为了替代关系型数据库,而是为系统的快速开发提供一种可能性,它和关系型数据库是一种互补的关系,可供开发者在不同的业务场景下选择相对应的数据库类型。 + +## 考点分析 + +本课时的面试题考察的是面试者对数据库整体概念的理解与区分,这个问题看似简单,但包含着众多小的知识点,面试者需要真正的理解关系型数据库和非关系型数据库以及文档型数据库之间的区别才能灵活应对。与之相关的面试题还有: + +* 非关系型数据库和文档型数据库有什么区别? +* MongoDB 支持事务吗? + +# 知识扩展 + +## 非关系型数据库 VS 文档型数据库 +非关系型数据和文档型数据库属于包含关系,非关系型数据包含了文档型数据库,文档型数据库属于非关系型数据。 + +非关系型数据通常包含 3 种数据库类型:文档型数据库、键值型数据库和全文搜索型数据库,下面分别来看每种类型的具体用途。 + +### 1.文档型数据库 + +文档型数据库以 MongoDB 和 Apache CouchDB 为代表,文档型数据库通常以 JSON 或者 XML 为格式进行数据存储。 + +以 MongoDB 为例,它是由 C++ 编写的一种面向文档的数据库管理系统,在 2007 年 10 月 由 10gen 团队所开发,并在 2009 年 2 月首度推出。MongoDB 是以二进制 JSON 格式存储数据的,MongoDB 对 JSON 做了一些优化,它支持了更多的数据类型,这种二进制存储的 JSON 我们也可以称之为 BSON(Binary JSON)。 + +BSON 具备三个特点:轻量、可遍历以及高效,它的缺点是空间利用率不是很理想。MongoDB 使用 BSON 进行存储的另一个重要原因是 BSON 具备可遍历性。 + +MongoDB 存储结构示例如下: + +```json +{"_id":ObjectId(“57ce2d4cce8685a6fd9df3a3"),"name":"老王","email":['java@qq.com','java@163.com']} +``` + +其中,“_id”为 MongoDB 默认的主键字段,它会为我们生成一起全局唯一的 id 值,并且这个值在做数据分片时非常有用。 + +文档型数据库的使用场景如下。 + +* **敏捷开发**,因为 MongoDB 拥有比关系型数据库更快的开发速度,因此很多敏捷开发组织,包括纽约时报等都采用了 MongoDB 数据库。使用它可以有效地避免在增加和修改数据库带来的沟通成本,以及维护和创建数据库模型成本,使用 MongoDB 只需要在程序层面严格把关就行,程序提交的数据结构可以直接更新到数据库中,并不需要繁杂的设计数据库模型再生成修改语句等过程。 +* **日志系统**,使用 MongoDB 数据库非常适合存储日志,日志对应到数据库中就是很多个文件,而 MongoDB 更擅长存储和查询文档,它提供了更简单的存储和更方便的查询功能。 +* **社交系统**,使用 MongoDB 可以很方便的存储用户的位置信息,可以方便的实现查询附近的人以及附近的地点等功能。 + +### 2. 键值型数据库 + +键值数据库也就是 Key-Value 数据库,它的典型代表数据库是 Redis 和 Memcached,而它们通常被当做非持久化的内存型数据库缓存来使用。当然 Redis 数据库是具备可持久化得能力的,但是开启持久化会降低系统的运行效率,因此在使用时需要根据实际的情况,选择开启或者关闭持久化的功能。 + +键值型数据库以极高的性能著称,且除了 Key-Value 字符串类型之外,还包含一些其他的数据类型。以 Redis 为例,它提供了字符串类型(String)、列表类型(List)、哈希表类型(Hash)、集合类型(Set)、有序集合类型(ZSet)等五种最常用的基础数据类型,还有管道类型(Pipeline)、地理位置类型(GEO)、基数统计类型(HyperLogLog)和流类型(Stream),并且还提供了消息队列的功能。 + +此数据库的优点是性能比较高,缺点是对事务的支持不是很好。 + +### 3. 全文搜索型数据库 + +传统的关系型数据库主要是依赖索引来实现快速查询功能的,而在全文搜索的业务下,索引很难满足查询的需求。因为全文搜索需要支持模糊匹配的,当数据量比较大的情况下,传递的关系型数据库的查询效率是非常低的;另一个原因是全文搜索需要支持多条件随意组合排序,如果要通过索引来实现的话,则需要创建大量的索引,而传统型数据库也很难实现,因此需要专门全文搜索引擎和相关的数据库才能实现此功能。 + +全文搜索型数据库以 ElasticSearch 和 Solr 为代表,它们的出现解决了关系型数据库全文搜索功能较弱的问题。 + +## MongoDB 事务 +MongoDB 在 4.0 之前是不支持事务的,不支持的原因也很简单,因为文档型数据库和传统的关系型数据库不一样,不需要满足三范式。文档型数据库之所以性能比较高的另一个主要原因,就是使用文档型数据库不用进行多表关联性查询,因为文档型数据库会把相关的信息存放到一张表中。因此,无需关联多表查询的 MongoDB,在这种情况下的查询性能是比较高的。 + +把所有相关的数据都放入一个表中,这也是 MongoDB 之前很长一段时间内不支持事务的原因,它可以保证单表操作的原子性,一条记录要么成功插入,要么插入失败,不会存在插入了一半的数据。因此,在这种设计思路下,MongoDB 官方认为“事务功能”的实现没有那么紧迫。 + +但在 MongoDB 4.0 之中正式添加了事务的功能,并且在 MongoDB 4.2 中实现了分布式事务的功能,至此 MongoDB 开启了支持事务之旅。 + +# 小结 +本课时我们首先讲了关系型数据库的 ACID 特性以及设计时需要遵循的三范式设计思想;然后介绍了以 MongoDB 为代表的文档型数据库与关系型数据库的不同;最后还讲了 MongoDB 的事务功能,以及文档性数据库与非关系型数据库的关系,希望本课时的内容对你有帮助。 diff --git a/Java基础教程/Java面试原理/19.Redis的过期策略和内存淘汰机制有什么区别?.md b/Java基础教程/Java面试原理/19.Redis的过期策略和内存淘汰机制有什么区别?.md new file mode 100644 index 00000000..45a3a9a4 --- /dev/null +++ b/Java基础教程/Java面试原理/19.Redis的过期策略和内存淘汰机制有什么区别?.md @@ -0,0 +1,217 @@ +# Redis 的过期策略和内存淘汰机制有什么区别? + +Redis 和 MySQL 是面试绕不过的两座大山,他们一个是关系型数据库的代表(MySQL),一个是键值数据库以及缓存中间件的一哥。尤其 Redis 几乎是所有互联网公司都在用的技术,比如国内的 BATJ、新浪、360、小米等公司;国外的微软、Twitter、Stack Overflow、GitHub、暴雪等公司。有的公司用 MySQL、有的用 SQL Server、甚至还有的用 Oracle 和 DB2,但缓存无一例外使用的都是 Redis,从某种程度上来讲 Redis 是普及率最高的技术,没有之一。 + +我们本课时的面试题是,Redis 是如何处理过期数据的?当内存不够用时 Redis 又是如何处理的? + +## 典型回答 + +我们在新增 Redis 缓存时可以设置缓存的过期时间,该时间保证了数据在规定的时间内失效,可以借助这个特性来实现很多功能。比如,存储一定天数的用户(登录)会话信息,这样在一定范围内用户不用重复登录了,但为了安全性,需要在一定时间之后重新验证用户的信息。因此,我们可以使用 Redis 设置过期时间来存储用户的会话信息。 + +对于已经过期的数据,Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除和定期删除。 + +### 惰性删除 + +惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。 + +它的优点是不会浪费太多的系统资源,只是在每次访问时才检查键值是否过期。缺点是删除过期键不及时,造成了一定的空间浪费。 + +惰性删除的源码位于 src/db.c 文件的 expireIfNeeded 方法中,如下所示: + +```c +int expireIfNeeded(redisDb *db, robj *key) { + // 判断键是否过期 + if (!keyIsExpired(db,key)) return 0; + if (server.masterhost != NULL) return 1; + /* 删除过期键 */ + // 增加过期键个数 + server.stat_expiredkeys++; + // 传播键过期的消息 + propagateExpire(db,key,server.lazyfree_lazy_expire); + notifyKeyspaceEvent(NOTIFY_EXPIRED, + "expired",key,db->id); + // server.lazyfree_lazy_expire 为 1 表示异步删除,否则则为同步删除 + return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : + dbSyncDelete(db,key); +} +// 判断键是否过期 +int keyIsExpired(redisDb *db, robj *key) { + mstime_t when = getExpire(db,key); + if (when < 0) return 0; + if (server.loading) return 0; + mstime_t now = server.lua_caller ? server.lua_time_start : mstime(); + return now > when; +} +// 获取键的过期时间 +long long getExpire(redisDb *db, robj *key) { + dictEntry *de; + if (dictSize(db->expires) == 0 || + (de = dictFind(db->expires,key->ptr)) == NULL) return -1; + serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL); + return dictGetSignedIntegerVal(de); +} +``` + +惰性删除的执行流程如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200616182536.png) + +### 定期删除 + +除了惰性删除之外,Redis 还提供了定期删除功能以弥补惰性删除的不足。 + +定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。 + +默认情况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期清除过期键。当然此值还可以通过配置文件进行设置,在 redis.conf 中修改配置“hz”即可,默认的值为“hz 10”。 + +> 小贴士:定期删除的扫描并不是遍历所有的键值对,这样的话比较费时且太消耗系统资源。Redis 服务器采用的是随机抽取形式,每次从过期字典中,取出 20 个键进行过期检测,过期字典中存储的是所有设置了过期时间的键值对。如果这批随机检查的数据中有 25% 的比例过期,那么会再抽取 20 个随机键值进行检测和删除,并且会循环执行这个流程,直到抽取的这批数据中过期键值小于 25%,此次检测才算完成。 + +定期删除的源码在 expire.c 文件的 activeExpireCycle 方法中,如下所示: + +```c +void activeExpireCycle(int type) { + static unsigned int current_db = 0; /* 上次定期删除遍历到的数据库ID */ + static int timelimit_exit = 0; + static long long last_fast_cycle = 0; /* 上次执行定期删除的时间点 */ + int j, iteration = 0; + int dbs_per_call = CRON_DBS_PER_CALL; // 需要遍历数据库的数量 + long long start = ustime(), timelimit, elapsed; + if (clientsArePaused()) return; + if (type == ACTIVE_EXPIRE_CYCLE_FAST) { + if (!timelimit_exit) return; + // ACTIVE_EXPIRE_CYCLE_FAST_DURATION 快速定期删除的执行时长 + if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return; + last_fast_cycle = start; + } + if (dbs_per_call > server.dbnum || timelimit_exit) + dbs_per_call = server.dbnum; + // 慢速定期删除的执行时长 + timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; + timelimit_exit = 0; + if (timelimit <= 0) timelimit = 1; + if (type == ACTIVE_EXPIRE_CYCLE_FAST) + timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* 删除操作花费的时间 */ + long total_sampled = 0; + long total_expired = 0; + for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { + int expired; + redisDb *db = server.db+(current_db % server.dbnum); + current_db++; + do { + // ....... + expired = 0; + ttl_sum = 0; + ttl_samples = 0; + // 每个数据库中检查的键的数量 + if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) + num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; + // 从数据库中随机选取 num 个键进行检查 + while (num--) { + dictEntry *de; + long long ttl; + if ((de = dictGetRandomKey(db->expires)) == NULL) break; + ttl = dictGetSignedInteger + // 过期检查,并对过期键进行删除 + if (activeExpireCycleTryExpire(db,de,now)) expired++; + if (ttl > 0) { + ttl_sum += ttl; + ttl_samples++; + } + total_sampled++; + } + total_expired += expired; + if (ttl_samples) { + long long avg_ttl = ttl_sum/ttl_samples; + if (db->avg_ttl == 0) db->avg_ttl = avg_ttl; + db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50); + } + if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */ + elapsed = ustime()-start; + if (elapsed > timelimit) { + timelimit_exit = 1; + server.stat_expired_time_cap_reached_count++; + break; + } + } + /* 判断过期键删除数量是否超过 25% */ + } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); + } + // ....... +} +``` + +定期删除的执行流程,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200616182843.png) + +> 小贴士:Redis 服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为 25ms。 + +以上是 Redis 服务器对待过期键的处理方案,当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,这和过期策略是完全不同的两个概念,经常有人把二者搞混,这两者一个是在正常情况下清除过期键,一个是在非正常情况下为了保证 Redis 顺利运行的保护策略。 + +当 Redis 内存不够用时,Redis 服务器会根据服务器设置的淘汰策略,删除一些不常用的数据,以保证 Redis 服务器的顺利运行。 + +## 考点分析 +本课时的面试题并非 Redis 的入门级面试题,需要面试者对 Redis 有一定的了解才能对答如流,并且 Redis 的过期淘汰策略和内存淘汰策略的概念比较类似,都是用于淘汰数据的。因此很多人会把二者当成一回事,但其实并不是,这个面试者特别注意一下,和此知识点相关的面试题还有以下这些: + +* Redis 内存淘汰策略有哪些? +* Redis 有哪些内存淘汰算法? + +# 知识扩展 + +## Redis 内存淘汰策略 +我们可以使用 `config get maxmemory-policy`命令,来查看当前 Redis 的内存淘汰策略,示例代码如下: + +```shell +127.0.0.1:6379> config get maxmemory-policy +1) "maxmemory-policy" +2) "noeviction" +``` + +从上面的结果可以看出,当前 Redis 服务器设置的是“noeviction”类型的内存淘汰策略,那么这表示什么含义呢?Redis 又有几种内存淘汰策略呢? + +在 4.0 版本之前 Redis 的内存淘汰策略有以下 6 种。 + +* noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是 Redis 默认内存淘汰策略。 +* allkeys-lru:淘汰整个键值中最久未使用的键值。 +* allkeys-random:随机淘汰任意键值。 +* volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值。 +* volatile-random:随机淘汰设置了过期时间的任意键值。 +* volatile-ttl:优先淘汰更早过期的键值。 + +可以看出我们上面示例使用的是 Redis 默认的内存淘汰策略“noeviction”。 + +而在 Redis 4.0 版本中又新增了 2 种淘汰策略: + +* volatile-lfu,淘汰所有设置了过期时间的键值中最少使用的键值; +* allkeys-lfu,淘汰整个键值中最少使用的键值。 + +> 小贴士:从以上内存淘汰策略中可以看出,allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。 + +这个内存淘汰策略我们可以通过配置文件来修改,redis.conf 对应的配置项是“maxmemory-policy noeviction”,只需要把它修改成我们需要设置的类型即可。 + +需要注意的是,如果使用修改 redis.conf 的方式,当设置完成之后需要重启 Redis 服务器才能生效。 + +还有另一种简单的修改内存淘汰策略的方式,我们可以使用命令行工具输入“config set maxmemory-policy noeviction”来修改内存淘汰的策略,这种修改方式的好处是执行成功之后就会生效,无需重启 Redis 服务器。但它的坏处是不能持久化内存淘汰策略,每次重启 Redis 服务器之后设置的内存淘汰策略就会丢失。 + +## Redis 内存淘汰算法 +内存淘汰算法主要包含两种:LRU 淘汰算法和 LFU 淘汰算法。 + +### LRU 淘汰算法 + +LRU( Least Recently Used,最近最少使用)淘汰算法:是一种常用的页面置换算法,也就是说最久没有使用的缓存将会被淘汰。 + +LRU 是基于链表结构实现的,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要进行内存淘汰时,只需要删除链表尾部的元素即可。 + +Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间。Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值 (此值可配置) ,然后淘汰最久没有使用的数据。 + +### LFU 淘汰算法 + +LFU(Least Frequently Used,最不常用的)淘汰算法:最不常用的算法是根据总访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。 + +LFU 相对来说比 LRU 更“智能”,因为它解决了使用频率很低的缓存,只是最近被访问了一次就不会被删除的问题。如果是使用 LRU 类似这种情况数据是不会被删除的,而使用 LFU 的话,这个数据就会被删除。 + +Redis 内存淘汰策略使用了 LFU 和近 LRU 的淘汰算法,具体使用哪种淘汰算法,要看服务器是如何设置内存淘汰策略的,也就是要看“maxmemory-policy”的值是如何设置的。 + +# 小结 + +本课时我们讲了 Redis 的过期删除策略:惰性删除 + 定期删除;还讲了 Redis 的内存淘汰策略,它和过期策略是完全不同的两个概念,内存淘汰策略是当内存不够用时才会触发的一种机制,它在 Redis 4.0 之后提供了 8 种内存淘汰策略,这些淘汰策略主要使用了近 LRU 淘汰算法和 LFU 淘汰算法。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/20.Redis怎样实现的分布式锁?.md b/Java基础教程/Java面试原理/20.Redis怎样实现的分布式锁?.md new file mode 100644 index 00000000..6dce66a6 --- /dev/null +++ b/Java基础教程/Java面试原理/20.Redis怎样实现的分布式锁?.md @@ -0,0 +1,130 @@ +# Redis 怎样实现的分布式锁? + +“锁”是我们实际工作和面试中无法避开的话题之一,正确使用锁可以保证高并发环境下程序的正确执行,也就是说只有使用锁才能保证多人同时访问时程序不会出现问题。 + +我们本课时的面试题是,什么是分布式锁?如何实现分布式锁? + +## 典型回答 + +第06篇时讲了单机锁的一些知识,包括悲观锁、乐观锁、可重入锁、共享锁和独占锁等内容,但它们都属于单机锁也就是程序级别的锁,如果在分布式环境下使用就会出现锁不生效的问题,因此我们需要使用分布式锁来解决这个问题。 + +分布式锁是控制分布式系统之间同步访问共享资源的一种方式。是为了解决分布式系统中,不同的系统或是同一个系统的不同主机共享同一个资源的问题,它通常会采用互斥来保证程序的一致性,这就是分布式锁的用途以及执行原理。 + +分布式锁示意图,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200616185240.png) + +分布式锁的常见实现方式有四种: + +* 基于 MySQL 的悲观锁来实现分布式锁,这种方式使用的最少,因为这种实现方式的性能不好,且容易造成死锁; +* 基于 Memcached 实现分布式锁,可使用 add 方法来实现,如果添加成功了则表示分布式锁创建成功; +* 基于 Redis 实现分布式锁,这也是本课时要介绍的重点,可以使用 setnx 方法来实现; +* 基于 ZooKeeper 实现分布式锁,利用 ZooKeeper 顺序临时节点来实现。 + +由于 MySQL 的执行效率问题和死锁问题,所以这种实现方式会被我们先排除掉,而 Memcached 和 Redis 的实现方式比较类似,但因为 Redis 技术比较普及,所以会优先使用 Redis 来实现分布式锁,而 ZooKeeper 确实可以很好的实现分布式锁。但此技术在中小型公司的普及率不高,尤其是非 Java 技术栈的公司使用的较少,如果只是为了实现分布式锁而重新搭建一套 ZooKeeper 集群,显然实现成本和维护成本太高,所以综合以上因素,我们本文会采用 Redis 来实现分布式锁。 + +之所以可以使用以上四种方式来实现分布式锁,是因为以上四种方式都属于程序调用的“外部系统”,而分布式的程序是需要共享“外部系统”的,这就是分布式锁得以实现的基本前提。 + +## 考点分析 +分布式锁的问题看似简单,但却有很多细节需要注意,比如,需要考虑分布式锁的超时问题,如果不设置超时时间的话,可能会导致死锁的产生,所以在对待这个“锁”的问题上,一定不能马虎。和此知识点相关的面试还有以下这些: + +* 单机锁有哪些?它为什么不能在分布式环境下使用? +* Redis 是如何实现分布式锁的?可能会遇到什么问题? +* 分布式锁超时的话会有什么问题?如何解决? + +# 知识扩展 + +## 单机锁 +程序中使用的锁叫单机锁,我们日常中所说的“锁”都泛指单机锁,其分类有很多,大体可分为以下几类: + +* **悲观锁**,是数据对外界的修改采取保守策略,它认为线程很容易把数据修改掉,因此在整个数据被修改的过程中都会采取锁定状态,直到一个线程使用完,其他线程才可以继续使用,典型应用是 synchronized; +* **乐观锁**,和悲观锁的概念恰好相反,乐观锁认为一般情况下数据在修改时不会出现冲突,所以在数据访问之前不会加锁,只是在数据提交更改时,才会对数据进行检测,典型应用是 ReadWriteLock 读写锁; +* **可重入锁**,也叫递归锁,指的是同一个线程在外面的函数获取了锁之后,那么内层的函数也可以继续获得此锁,在 Java 语言中 ReentrantLock 和 synchronized 都是可重入锁; +* **独占锁和共享锁**,只能被单线程持有的锁叫做独占锁,可以被多线程持有的锁叫共享锁,独占锁指的是在任何时候最多只能有一个线程持有该锁,比如 ReentrantLock 就是独占锁;而 ReadWriteLock 读写锁允许同一时间内有多个线程进行读操作,它就属于共享锁。 + +单机锁之所以不能应用在分布式系统中是因为,在分布式系统中,每次请求可能会被分配在不同的服务器上,而单机锁是在单台服务器上生效的。如果是多台服务器就会导致请求分发到不同的服务器,从而导致锁代码不能生效,因此会造成很多异常的问题,那么单机锁就不能应用在分布式系统中了。 + +## 使用 Redis 实现分布式锁 +使用 Redis 实现分布式锁主要需要使用 setnx 方法,也就是 set if not exists(不存在则创建),具体的实现代码如下: + +```shell +127.0.0.1:6379> setnx lock true +(integer) 1 #创建锁成功 +#逻辑业务处理... +127.0.0.1:6379> del lock +(integer) 1 #释放锁 +``` + +当执行 setnx 命令之后返回值为 1 的话,则表示创建锁成功,否则就是失败。释放锁使用 del 删除即可,当其他程序 setnx 失败时,则表示此锁正在使用中,这样就可以实现简单的分布式锁了。 + +但是以上代码有一个问题,就是没有设置锁的超时时间,因此如果出现异常情况,会导致锁未被释放,而其他线程又在排队等待此锁就会导致程序不可用。 + +有人可能会想到使用 expire 来设置键值的过期时间来解决这个问题,例如以下代码: + +```shell +127.0.0.1:6379> setnx lock true +(integer) 1 #创建锁成功 +127.0.0.1:6379> expire lock 30 #设置锁的(过期)超时时间为 30s +(integer) 1 +#逻辑业务处理... +127.0.0.1:6379> del lock +(integer) 1 #释放锁 +``` + +但这样执行仍然会有问题,因为 setnx lock true 和 expire lock 30 命令是非原子的,也就是一个执行完另一个才能执行。但如果在 setnx 命令执行完之后,发生了异常情况,那么就会导致 expire 命令不会执行,因此依然没有解决死锁的问题。 + +这个问题在 Redis 2.6.12 之前一直没有得到有效的处理,当时的解决方案是在客户端进行原子合并操作,于是就诞生了很多客户端类库来解决此原子问题,不过这样就增加了使用的成本。因为你不但要添加 Redis 的客户端,还要为了解决锁的超时问题,需额外的增加新的类库,这样就增加了使用成本,但这个问题在 Redis 2.6.12 版本中得到了有效的处理。 + +在 Redis 2.6.12 中我们可以使用一条 set 命令来执行键值存储,并且可以判断键是否存在以及设置超时时间了,如下代码所示: + +```shell +127.0.0.1:6379> set lock true ex 30 nx +OK #创建锁成功 +``` + +其中,ex 是用来设置超时时间的,而 nx 是 not exists 的意思,用来判断键是否存在。如果返回的结果为“OK”则表示创建锁成功,否则表示此锁有人在使用。 + +## 锁超时 +从上面的内容可以看出,使用 set 命令之后好像一切问题都解决了,但在这里我要告诉你,其实并没有。例如,我们给锁设置了超时时间为 10s,但程序的执行需要使用 15s,那么在第 10s 时此锁因为超时就会被释放,这时候线程二在执行 set 命令时正常获取到了锁,于是在很短的时间内 2s 之后删除了此锁,这就造成了锁被误删的情况,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200616190418.png) + +锁被误删的解决方案是在使用 set 命令创建锁时,给 value 值设置一个归属标识。例如,在 value 中插入一个 UUID,每次在删除之前先要判断 UUID 是不是属于当前的线程,如果属于再删除,这样就避免了锁被误删的问题。 + +注意:在锁的归属判断和删除的过程中,不能先判断锁再删除锁,如下代码所示: + +```java +if(uuid.equals(uuid)){ // 判断是否是自己的锁 + del(luck); // 删除锁 +} +``` + +应该把判断和删除放到一个原子单元中去执行,因此需要借助 Lua 脚本来执行,在 Redis 中执行 Lua 脚本可以保证这批命令的原子性,它的实现代码如下: + +```java +/** + * 释放分布式锁 + * @param jedis Redis客户端 + * @param lockKey 锁的 key + * @param flagId 锁归属标识 + * @return 是否释放成功 + */ +public static boolean unLock(Jedis jedis, String lockKey, String flagId) { + String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; + Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(flagId)); + if ("1L".equals(result)) { // 判断执行结果 + return true; + } + return false; +} +``` + +其中,Collections.singletonList() 方法是将 String 转成 List,因为 jedis.eval() 最后两个参数要求必须是 List 类型。 + +锁超时可以通过两种方案来解决: + +* 把执行耗时的方法从锁中剔除,减少锁中代码的执行时间,保证锁在超时之前,代码一定可以执行完; +* 把锁的超时时间设置的长一些,正常情况下我们在使用完锁之后,会调用删除的方法手动删除锁,因此可以把超时时间设置的稍微长一些。 + +# 小结 +本课时我们讲了分布式锁的四种实现方式,即 MySQL、Memcached、Redis 和 ZooKeeper,因为 Redis 的普及率比较高,因此对于很多公司来说使用 Redis 实现分布式锁是最优的选择。本课时我们还讲了使用 Redis 实现分布式锁的具体步骤以及实现代码,还讲了在实现过程中可能会遇到的一些问题以及解决方案。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/21.Redis中如何实现的消息队列?实现的方式有几种?.md b/Java基础教程/Java面试原理/21.Redis中如何实现的消息队列?实现的方式有几种?.md new file mode 100644 index 00000000..0cbafe5e --- /dev/null +++ b/Java基础教程/Java面试原理/21.Redis中如何实现的消息队列?实现的方式有几种?.md @@ -0,0 +1,313 @@ +# Redis 中如何实现的消息队列?实现的方式有几种? + +细心的你可能发现了,本系列课程中竟然出现了三篇都是在说消息队列,第10篇时讲了程序级别的消息队列以及延迟消息队列的实现,而第15篇讲了常见的消息队列中间件 RabbitMQ、Kafka 等,由此可见消息队列在整个 Java 技术体系中的重要程度。本课时我们将重点来看一下 Redis 是如何实现消息队列的。 + +我们本课时的面试题是,在 Redis 中实现消息队列的方式有几种? + +## 典型回答 +早在 Redis 2.0 版本之前使用 Redis 实现消息队列的方式有两种: + +使用 List 类型实现 +使用 ZSet 类型实现 +其中使用**List 类型实现的方式最为简单和直接**,它主要是通过 lpush、rpop 存入和读取实现消息队列的,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617150400.png) + +lpush 可以把最新的消息存储到消息队列(List 集合)的首部,而 rpop 可以读取消息队列的尾部,这样就实现了先进先出,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617150614.png) + +命令行的实现命令如下: + +```shell +127.0.0.1:6379> lpush mq "java" #推送消息 java +(integer) 1 +127.0.0.1:6379> lpush mq "msg" #推送消息 msg +(integer) 2 +127.0.0.1:6379> rpop mq #接收到消息 java +"java" +127.0.0.1:6379> rpop mq #接收到消息 msg +"mq" +``` + +其中,mq 相当于消息队列的名称,而 lpush 用于生产并添加消息,而 rpop 用于拉取并消费消息。 +使用 List 实现消息队列的优点是消息可以被持久化,List 可以借助 Redis 本身的持久化功能,AOF 或者是 RDB 或混合持久化的方式,用于把数据保存至磁盘,这样当 Redis 重启之后,消息不会丢失。 + +但使用 List 同样存在一定的问题,比如消息不支持重复消费、没有按照主题订阅的功能、不支持消费消息确认等。 + +ZSet 实现消息队列的方式和 List 类似,它是利用 zadd 和 zrangebyscore 来实现存入和读取消息的,这里就不重复叙述了。但 ZSet 的实现方式更为复杂一些,因为 ZSet 多了一个分值(score)属性,我们可以使用它来实现更多的功能,比如用它来存储时间戳,以此来实现延迟消息队列等。 + +ZSet 同样具备持久化的功能,List 存在的问题它也同样存在,不但如此,使用 ZSet 还不能存储相同元素的值。因为它是有序集合,有序集合的存储元素值是不能重复的,但分值可以重复,也就是说当消息值重复时,只能存储一条信息在 ZSet 中。 + +在 Redis 2.0 之后 Redis 就新增了专门的发布和订阅的类型,Publisher(发布者)和 Subscriber(订阅者)来实现消息队列了,它们对应的执行命令如下: + +* 发布消息,publish channel "message" +* 订阅消息,subscribe channel + +使用发布和订阅的类型,我们可以实现主题订阅的功能,也就是 Pattern Subscribe 的功能。因此我们可以使用一个消费者“queue_*”来订阅所有以“queue_”开头的消息队列,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617151011.png) + +发布订阅模式的优点很明显,但同样存在以下 3 个问题: + +* 无法持久化保存消息,如果 Redis 服务器宕机或重启,那么所有的消息将会丢失; +* 发布订阅模式是“发后既忘”的工作模式,如果有订阅者离线重连之后就不能消费之前的历史消息; +* 不支持消费者确认机制,稳定性不能得到保证,例如当消费者获取到消息之后,还没来得及执行就宕机了。因为没有消费者确认机制,Redis 就会误以为消费者已经执行了,因此就不会重复发送未被正常消费的消息了,这样整体的 Redis 稳定性就被没有办法得到保障了。 + +然而在 Redis 5.0 之后新增了 Stream 类型,我们就可以使用 Stream 的 xadd 和 xrange 来实现消息的存入和读取了,并且 Stream 提供了 xack 手动确认消息消费的命令,用它我们就可以实现消费者确认的功能了,使用命令如下: + +```shell +127.0.0.1:6379> xack mq group1 1580959593553-0 +(integer) 1 +``` + +相关语法如下: + +``` +xack key group-key ID [ID ...] +``` + +消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 xack 确认消息已经被消费完成,整个流程的执行如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617151524.png) + +其中“Group”为群组,消费者也就是接收者需要订阅到群组才能正常获取到消息。 + +以上就 Redis 实现消息队列的四种方式,他们分别是: + +* 使用 List 实现消息队列; +* 使用 ZSet 实现消息队列; +* 使用发布订阅者模式实现消息队列; +* 使用 Stream 实现消息队列。 + +## 考点分析 +本课时的题目比较全面的考察了面试者对于 Redis 整体知识框架和新版本特性的理解和领悟。早期版本中比较常用的实现消息队列的方式是 List、ZSet 和发布订阅者模式,使用 Stream 来实现消息队列是近两年才流行起来的方案,并且很多企业也没有使用到 Redis 5.0 这么新的版本。因此只需回答出前三种就算及格了,而 Stream 方式实现消息队列属于附加题,如果面试中能回答上来的话就更好了,它体现了你对新技术的敏感度与对技术的热爱程度,属于面试中的加分项。 + +和此知识点相关的面试题还有以下几个: + +* 在 Java 代码中使用 List 实现消息队列会有什么问题?应该如何解决? +* 在程序中如何使用 Stream 来实现消息队列? + +# 知识扩展 +## 使用 List 实现消息队列 + +在 Java 程序中我们需要使用 Redis 客户端框架来辅助程序操作 Redis,比如 Jedis 框架。 + +使用 Jedis 框架首先需要在 pom.xml 文件中添加 Jedis 依赖,配置如下: + +```xml + +redis.clientsjedis${version} + +``` + +List 实现消息队列的完整代码如下: + +```java +import redis.clients.jedis.Jedis; +publicclass ListMQTest { + public static void main(String[] args){ + // 启动一个线程作为消费者 + new Thread(() -> consumer()).start(); + // 生产者 + producer(); + } + /** + * 生产者 + */ + public static void producer() { + Jedis jedis = new Jedis("127.0.0.1", 6379); + // 推送消息 + jedis.lpush("mq", "Hello, List."); + } + /** + * 消费者 + */ + public static void consumer() { + Jedis jedis = new Jedis("127.0.0.1", 6379); + // 消费消息 + while (true) { + // 获取消息 + String msg = jedis.rpop("mq"); + if (msg != null) { + // 接收到了消息 + System.out.println("接收到消息:" + msg); + } + } + } +} +``` + +以上程序的运行结果是: + +``` +接收到消息:Hello, Java. +``` + +但是以上的代码存在一个问题,可以看出以上消费者的实现是通过 while 无限循环来获取消息,但如果消息的空闲时间比较长,一直没有新任务,而 while 循环不会因此停止,它会一直执行循环的动作,这样就会白白浪费了系统的资源。 + +此时我们可以借助 Redis 中的阻塞读来替代 rpop 的方法就可以解决此问题,具体实现代码如下: + +```java +import redis.clients.jedis.Jedis; +public class ListMQExample { + public static void main(String[] args) throws InterruptedException { + // 消费者 + new Thread(() -> bConsumer()).start(); + // 生产者 + producer(); + } + /** + * 生产者 + */ + public static void producer() throws InterruptedException { + Jedis jedis = new Jedis("127.0.0.1", 6379); + // 推送消息 + jedis.lpush("mq", "Hello, Java."); + Thread.sleep(1000); + jedis.lpush("mq", "message 2."); + Thread.sleep(2000); + jedis.lpush("mq", "message 3."); + } + /** + * 消费者(阻塞版) + */ + public static void bConsumer() { + Jedis jedis = new Jedis("127.0.0.1", 6379); + while (true) { + // 阻塞读 + for (String item : jedis.brpop(0,"mq")) { + // 读取到相关数据,进行业务处理 + System.out.println(item); + } + } + } +} +``` + +以上程序的运行结果是: + +``` +接收到消息:Hello, Java. +``` + +以上代码是经过改良的,我们使用 brpop 替代 rpop 来读取最后一条消息,就可以解决 while 循环在没有数据的情况下,一直循环消耗系统资源的情况了。brpop 中的 b 是 blocking 的意思,表示阻塞读,也就是当队列没有数据时,它会进入休眠状态,当有数据进入队列之后,它才会“苏醒”过来执行读取任务,这样就可以解决 while 循环一直执行消耗系统资源的问题了。 + +## 使用 Stream 实现消息队列 +在开始实现消息队列之前,我们必须先创建分组才行,因为消费者需要关联分组信息才能正常运行,具体实现代码如下: + +```java +import com.google.gson.Gson; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.StreamEntry; +import redis.clients.jedis.StreamEntryID; +import utils.JedisUtils; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +public class StreamGroupExample { + private static final String _STREAM_KEY = "mq"; // 流 key + private static final String _GROUP_NAME = "g1"; // 分组名称 + private static final String _CONSUMER_NAME = "c1"; // 消费者 1 的名称 + private static final String _CONSUMER2_NAME = "c2"; // 消费者 2 的名称 + public static void main(String[] args) { + // 生产者 + producer(); + // 创建消费组 + createGroup(_STREAM_KEY, _GROUP_NAME); + // 消费者 1 + new Thread(() -> consumer()).start(); + // 消费者 2 + new Thread(() -> consumer2()).start(); + } + /** + * 创建消费分组 + * @param stream 流 key + * @param groupName 分组名称 + */ + public static void createGroup(String stream, String groupName) { + Jedis jedis = JedisUtils.getJedis(); + jedis.xgroupCreate(stream, groupName, new StreamEntryID(), true); + } + /** + * 生产者 + */ + public static void producer() { + Jedis jedis = JedisUtils.getJedis(); + // 添加消息 1 + Map map = new HashMap<>(); + map.put("data", "redis"); + StreamEntryID id = jedis.xadd(_STREAM_KEY, null, map); + System.out.println("消息添加成功 ID:" + id); + // 添加消息 2 + Map map2 = new HashMap<>(); + map2.put("data", "java"); + StreamEntryID id2 = jedis.xadd(_STREAM_KEY, null, map2); + System.out.println("消息添加成功 ID:" + id2); + } + /** + * 消费者 1 + */ + public static void consumer() { + Jedis jedis = JedisUtils.getJedis(); + // 消费消息 + while (true) { + // 读取消息 + Map.Entry entry = new AbstractMap.SimpleImmutableEntry<>(_STREAM_KEY, + new StreamEntryID().UNRECEIVED_ENTRY); + // 阻塞读取一条消息(最大阻塞时间120s) + List>> list = jedis.xreadGroup(_GROUP_NAME, _CONSUMER_NAME, 1, + 120 * 1000, true, entry); + if (list != null && list.size() == 1) { + // 读取到消息 + Map content = list.get(0).getValue().get(0).getFields(); // 消息内容 + System.out.println("Consumer 1 读取到消息 ID:" + list.get(0).getValue().get(0).getID() + + " 内容:" + new Gson().toJson(content)); + } + } + } + /** + * 消费者 2 + */ + public static void consumer2() { + Jedis jedis = JedisUtils.getJedis(); + // 消费消息 + while (true) { + // 读取消息 + Map.Entry entry = new AbstractMap.SimpleImmutableEntry<>(_STREAM_KEY, + new StreamEntryID().UNRECEIVED_ENTRY); + // 阻塞读取一条消息(最大阻塞时间120s) + List>> list = jedis.xreadGroup(_GROUP_NAME, _CONSUMER2_NAME, 1, + 120 * 1000, true, entry); + if (list != null && list.size() == 1) { + // 读取到消息 + Map content = list.get(0).getValue().get(0).getFields(); // 消息内容 + System.out.println("Consumer 2 读取到消息 ID:" + list.get(0).getValue().get(0).getID() + + " 内容:" + new Gson().toJson(content)); + } + } + } +} +``` + +以上代码运行结果如下: + +```` +消息添加成功 ID:1580971482344-0 +消息添加成功 ID:1580971482415-0 +Consumer 1 读取到消息 ID:1580971482344-0 内容:{"data":"redis"} +Consumer 2 读取到消息 ID:1580971482415-0 内容:{"data":"java"} +```` + +其中,jedis.xreadGroup() 方法的第五个参数 noAck 表示是否自动确认消息,如果设置 true 收到消息会自动确认 (ack) 消息,否则需要手动确认。 + +可以看出,同一个分组内的多个 consumer 会读取到不同消息,不同的 consumer 不会读取到分组内的同一条消息。 + +> 小贴士:Jedis 框架要使用最新版,低版本 block 设置大于 0 时,会出现 bug,抛连接超时异常。 + +# 小结 +本课时我们讲了 Redis 中消息队列的四种实现方式:List 方式、ZSet 方式、发布订阅者模式、Stream 方式,其中发布订阅者模式不支持消息持久化、而其他三种方式支持持久化,并且 Stream 方式支持消费者确认。我们还使用 Jedis 框架完成了 List 和 Stream 的消息队列功能,需要注意的是在 List 中需要使用 brpop 来读取消息,而不是 rpop,这样可以解决没有任务时 ,while 一直循环浪费系统资源的问题。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/22.Redis是如何实现高可用的?.md b/Java基础教程/Java面试原理/22.Redis是如何实现高可用的?.md new file mode 100644 index 00000000..618d0c8b --- /dev/null +++ b/Java基础教程/Java面试原理/22.Redis是如何实现高可用的?.md @@ -0,0 +1,100 @@ +# Redis是如何实现高可用的? + +高可用是通过设计,减少系统不能提供服务的时间,是分布式系统的基础也是保障系统可靠性的重要手段。而 Redis 作为一款普及率最高的内存型中间件,它的高可用技术也非常的成熟。 + +我们本课时的面试题是,Redis 是如何保证系统高可用的?它的实现方式有哪些? + +## 典型回答 +Redis 高可用的手段主要有以下四种: + +* 数据持久化 +* 主从数据同步(主从复制) +* Redis 哨兵模式(Sentinel) +* Redis 集群(Cluster) + +其中数据持久化保证了系统在发生宕机或者重启之后数据不会丢失,增加了系统的可靠性和减少了系统不可用的时间(省去了手动恢复数据的过程);而主从数据同步可以将数据存储至多台服务器,这样当遇到一台服务器宕机之后,可以很快地切换至另一台服务器以继续提供服务;哨兵模式用于发生故障之后自动切换服务器;而 Redis 集群提供了多主多从的 Redis 分布式集群环境,用于提供性能更好的 Redis 服务,并且它自身拥有故障自动切换的能力。 + +## 考点分析 +高可用的问题属于 Redis 中比较大的面试题了,因为很多知识点都和这个面试题有关,同时也属于比较难的面试题了。因为涉及了分布式集群,而分布式集群属于 Redis 中比较难懂的一个知识点。和此问题相关的面试题还有以下几个: + +* 数据持久化有几种方式? +* Redis 主从同步有几种模式? +* 什么是 Redis 哨兵模式?它解决了什么问题? +* Redis 集群的优势是什么? + +# 知识扩展 +## 1.数据持久化 + +持久化功能是 Redis 和 Memcached 的主要区别之一,因为只有 Redis 提供了此功能。 + +在 Redis 4.0 之前数据持久化方式有两种:AOF 方式和 RDB 方式。 + +RDB(Redis DataBase,快照方式)是将某一个时刻的内存数据,以二进制的方式写入磁盘。 +AOF(Append Only File,文件追加方式)是指将所有的操作命令,以文本的形式追加到文件中。 + +RDB 默认的保存文件为 dump.rdb,优点是以二进制存储的,因此占用的空间更小、数据存储更紧凑,并且与 AOF 相比,RDB 具备更快的重启恢复能力。 + +AOF 默认的保存文件为 appendonly.aof,它的优点是存储频率更高,因此丢失数据的风险就越低,并且 AOF 并不是以二进制存储的,所以它的存储信息更易懂。缺点是占用空间大,重启之后的数据恢复速度比较慢。 + +可以看出 RDB 和 AOF 各有利弊,RDB 具备更快速的数据重启恢复能力,并且占用更小的磁盘空间,但有数据丢失的风险;而 AOF 文件的可读性更高,但却占用了更大的空间,且重启之后的恢复速度更慢,于是在 Redis 4.0 就推出了混合持久化的功能。 + +混合持久化的功能指的是 Redis 可以使用 RDB + AOF 两种格式来进行数据持久化,这样就可以做到扬长避短物尽其用了,混合持久化的存储示意图如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617153326.png) + +我们可以使用`config get aof-use-rdb-preamble`的命令来查询 Redis 混合持久化的功能是否开启,执行示例如下: + +```shell +127.0.0.1:6379> config get aof-use-rdb-preamble +1) "aof-use-rdb-preamble" +2) "yes" +``` + +如果执行结果为“no”则表示混合持久化功能关闭,不过我们可以使用`config set aof-use-rdb-preamble yes`的命令打开此功能。 +Redis 混合持久化的存储模式是,开始的数据以 RDB 的格式进行存储,因此只会占用少量的空间,并且之后的命令会以 AOF 的方式进行数据追加,这样就可以减低数据丢失的风险,同时可以提高数据恢复的速度。 + +## 2.Redis 主从同步 +主从同步是 Redis 多机运行中最基础的功能,它是把多个 Redis 节点组成一个 Redis 集群,在这个集群当中有一个主节点用来进行数据的操作,其他从节点用于同步主节点的内容,并且提供给客户端进行数据查询。 + +Redis 主从同步分为:主从模式和从从模式。 + +主从模式就是一个主节点和多个一级从节点,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617153638.png) + +而从从模式是指一级从节点下面还可以拥有更多的从节点,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617153733.png) + +主从模式可以提高 Redis 的整体运行速度,因为使用主从模式就可以实现数据的读写分离,把写操作的请求分发到主节点上,把其他的读操作请求分发到从节点上,这样就减轻了 Redis 主节点的运行压力,并且提高了 Redis 的整体运行速度。 + +不但如此使用主从模式还实现了 Redis 的高可用,当主服务器宕机之后,可以很迅速的把从节点提升为主节点,为 Redis 服务器的宕机恢复节省了宝贵的时间。 + +并且主从复制还降低了数据丢失的风险,因为数据是完整拷贝在多台服务器上的,当一个服务器磁盘坏掉之后,可以从其他服务器拿到完整的备份数据。 + +## 3.Redis 哨兵模式 +Redis 主从复制模式有那么多的优点,但是有一个致命的缺点,就是当 Redis 的主节点宕机之后,必须人工介入手动恢复,那么到特殊时间段,比如公司组织全体团建或者半夜突然发生主节点宕机的问题,此时如果等待人工去处理就会很慢,这个时间是我们不允许的,并且我们还需要招聘专职的人来负责数据恢复的事,同时招聘的人还需要懂得相关的技术才能胜任这份工作。既然如此的麻烦,那有没有简单一点的解决方案,这个时候我们就需要用到 Redis 的哨兵模式了。 + +Redis 哨兵模式就是用来监视 Redis 主从服务器的,当 Redis 的主从服务器发生故障之后,Redis 哨兵提供了自动容灾修复的功能,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617154426.png) + +Redis 哨兵模块存储在 Redis 的 src/redis-sentinel 目录下,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617154611.png) + +我们可以使用命令`./src/redis-sentinel sentinel.conf`来启动哨兵功能。 + +有了哨兵功能之后,就再也不怕 Redis 主从服务器宕机了。哨兵的工作原理是每个哨兵会以每秒钟 1 次的频率,向已知的主服务器和从服务器,发送一个 PING 命令。如果最后一次有效回复 PING 命令的时间,超过了配置的最大下线时间(Down-After-Milliseconds)时,默认是 30s,那么这个实例会被哨兵标记为主观下线。 + +如果一个主服务器被标记为主观下线,那么正在监视这个主服务器的所有哨兵节点,要以每秒 1 次的频率确认主服务器是否进入了主观下线的状态。如果有足够数量(quorum 配置值)的哨兵证实该主服务器为主观下线,那么这个主服务器被标记为客观下线。此时所有的哨兵会按照规则(协商)自动选出新的主节点服务器,并自动完成主服务器的自动切换功能,而整个过程都是无须人工干预的。 + +## 4.Redis 集群 +Redis 集群也就是 Redis Cluster,它是 Redis 3.0 版本推出的 Redis 集群方案,将数据分布在不同的主服务器上,以此来降低系统对单主节点的依赖,并且可以大大提高 Redis 服务的读写性能。Redis 集群除了拥有主从模式 + 哨兵模式的所有功能之外,还提供了多个主从节点的集群功能,实现了真正意义上的分布式集群服务,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200617154826.png) + +Redis 集群可以实现数据分片服务,也就是说在 Redis 集群中有 16384 个槽位用来存储所有的数据,当我们有 N 个主节点时,可以把 16384 个槽位平均分配到 N 台主服务器上。当有键值存储时,Redis 会使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位,再把此键值存储在对应的服务器上,读取操作也是同样的道理,这样我们就实现了数据分片的功能。 + +# 小结 +本课时我们讲了保障 Redis 高可用的 4 种手段:数据持久化保证了数据不丢失;Redis 主从让 Redis 从单机变成了多机。它有两种模式:主从模式和从从模式,但当主节点出现问题时,需要人工手动恢复系统;Redis 哨兵模式用来监控 Redis 主从模式,并提供了自动容灾恢复的功能。最后是 Redis 集群,除了可以提供主从和哨兵的功能之外,还提供了多个主从节点的集群功能,这样就可以把数据均匀的存储各个主机主节点上,实现了系统的横向扩展,大大提高了 Redis 的并发处理能力。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/23.JVM 的内存布局和运行原理.md b/Java基础教程/Java面试原理/23.JVM 的内存布局和运行原理.md new file mode 100644 index 00000000..204ed4ce --- /dev/null +++ b/Java基础教程/Java面试原理/23.JVM 的内存布局和运行原理.md @@ -0,0 +1,121 @@ +# 说一下 JVM 的内存布局和运行原理? + +JVM(Java Virtual Machine,Java 虚拟机)顾名思义就是用来执行 Java 程序的“虚拟主机”,实际的工作是将编译的 class 代码(字节码)翻译成底层操作系统可以运行的机器码并且进行调用执行,这也是 Java 程序能够“一次编写,到处运行”的原因(因为它会根据特定的操作系统生成对应的操作指令)。JVM 的功能很强大,像 Java 对象的创建、使用和销毁,还有垃圾回收以及某些高级的性能优化,例如,热点代码检测等功能都是在 JVM 中进行的。因为 JVM 是 Java 程序能够运行的根本,因此掌握 JVM 也已经成了一个合格 Java 程序员必备的技能。 + +我们本课时的面试题是,说一下 JVM 的内存布局和运行原理? + +## 典型回答 +JVM 的种类有很多,比如 HotSpot 虚拟机,它是 Sun/OracleJDK 和 OpenJDK 中的默认 JVM,也是目前使用范围最广的 JVM。我们常说的 JVM 其实泛指的是 HotSpot 虚拟机,还有曾经与 HotSpot 齐名为“三大商业 JVM”的 JRockit 和 IBM J9 虚拟机。但无论是什么类型的虚拟机都必须遵守 Oracle 官方发布的《Java虚拟机规范》,它是 Java 领域最权威最重要的著作之一,用于规范 JVM 的一些具体“行为”。 + +同样对于 JVM 的内存布局也一样,根据《Java虚拟机规范》的规定,JVM 的内存布局分为以下几个部分: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200618212842.png) + +以上 5 个内存区域的主要用途如下。 + +### 1. 堆 +堆(Java Heap) 也叫 Java 堆或者是 GC 堆,它是一个线程共享的内存区域,也是 JVM 中占用内存最大的一块区域,Java 中所有的对象都存储在这里。 + +《Java虚拟机规范》对 Java 堆的描述是:“所有的对象实例以及数组都应当在堆上分配”。但这在技术日益发展的今天已经有点不那么“准确”了,比如 JIT(Just In Time Compilation,即时编译 )优化中的逃逸分析,使得变量可以直接在栈上被分配。 + +当对象或者是变量在方法中被创建之后,其指针可能被线程所引用,而这个对象就被称作指针逃逸或者是引用逃逸。 + +比如以下代码中的 sb 对象的逃逸: + +```java +public static StringBuffer createString() { + StringBuffer sb = new StringBuffer(); + sb.append("Java"); + return sb; +} +``` + +sb 虽然是一个局部变量,但上述代码可以看出,它被直接 return 出去了,因此可能被赋值给了其他变量,并且被完全修改,于是此 sb 就逃逸到了方法外部。 + +想要 sb 变量不逃逸也很简单,可以改为如下代码: + +```java +public static String createString() { + StringBuffer sb = new StringBuffer(); + sb.append("Java"); + return sb.toString(); +} +``` + +> 小贴士:通过逃逸分析可以让变量或者是对象直接在栈上分配,从而极大地降低了垃圾回收的次数,以及堆分配对象的压力,进而提高了程序的整体运行效率。 + +回到主题,堆大小的值可通过 -Xms 和 -Xmx 来设置(设置最小值和最大值),当堆超过最大值时就会抛出 OOM(OutOfMemoryError)异常。 + +### 2. 方法区 +方法区(Method Area) 也被称为非堆区,用于和“Java 堆”的概念进行区分,它也是线程共享的内存区域,用于存储已经被 JVM 加载的类型信息、常量、静态变量、代码缓存等数据。 + +说到方法区有人可能会联想到“永久代”,但对于《Java虚拟机规范》来说并没有规定这样一个区域,同样它也只是 HotSpot 中特有的一个概念。这是因为 HotSpot 技术团队把垃圾收集器的分代设计扩展到方法区之后才有的一个概念,可以理解为 HotSpot 技术团队只是用永久代来实现方法区而已,但这会导致一个致命的问题,这样设计更容易造成内存溢出。因为永久代有 -XX:MaxPermSize(方法区分配的最大内存)的上限,即使不设置也会有默认的大小。例如,32 位操作系统中的 4GB 内存限制等,并且这样设计导致了部分的方法在不同类型的 Java 虚拟机下的表现也不同,比如 String::intern() 方法。所以在 JDK 1.7 时 HotSpot 虚拟机已经把原本放在永久代的字符串常量池和静态变量等移出了方法区,并且在 JDK 1.8 中完全废弃了永久代的概念。 + +### 3. 程序计数器 +程序计数器(Program Counter Register) 线程独有一块很小的内存区域,保存当前线程所执行字节码的位置,包括正在执行的指令、跳转、分支、循环、异常处理等。 + +### 4.虚拟机栈 + +虚拟机栈也叫 Java 虚拟机栈(Java Virtual Machine Stack),和程序计数器相同它也是线程独享的,用来描述 Java 方法的执行,在每个方法被执行时就会同步创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。当调用方法时执行入栈,而方法返回时执行出栈。 + +### 5.本地方法栈 + +本地方法栈(Native Method Stacks)与虚拟机栈类似,它是线程独享的,并且作用也和虚拟机栈类似。只不过虚拟机栈是为虚拟机中执行的 Java 方法服务的,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。 + +> 小贴士:需要注意的是《Java虚拟机规范》只规定了有这么几个区域,但没有规定 JVM 的具体实现细节,因此对于不同的 JVM 来说,实现也是不同的。例如,“永久代”是 HotSpot 中的一个概念,而对于 JRockit 来说就没有这个概念。所以很多人说的 JDK 1.8 把永久代转移到了元空间,这其实只是 HotSpot 的实现,而非《Java虚拟机规范》的规定。 + +JVM 的执行流程是,首先先把 Java 代码(.java)转化成字节码(.class),然后通过类加载器将字节码加载到内存中,所谓的内存也就是我们上面介绍的运行时数据区,但字节码并不是可以直接交给操作系统执行的机器码,而是一套 JVM 的指令集。这个时候需要使用特定的命令解析器也就是我们俗称的**执行引擎(Execution Engine)**将字节码翻译成可以被底层操作系统执行的指令再去执行,这样就实现了整个 Java 程序的运行,这也是 JVM 的整体执行流程。 + +## 考点分析 +JVM 的内存布局是一道必考的 Java 面试题,一般会作为 JVM 方面的第一道面试题出现,它也是中高级工程师必须掌握的一个知识点。和此知识点相关的面试题还有这些:类的加载分为几个阶段?每个阶段代表什么含义?加载了什么内容? + +# 知识扩展——类加载 +类的生命周期会经历以下 7 个阶段: + +1. 加载阶段(Loading) +2. 验证阶段(Verification) +3. 准备阶段(Preparation) +4. 解析阶段(Resolution) +5. 初始化阶段(Initialization) +6. 使用阶段(Using) +7. 卸载阶段(Unloading) + +其中验证、准备、解析 3 个阶段统称为连接(Linking),如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200618214003.png) + +我们平常所说的 JVM 类加载通常指的就是前五个阶段:加载、验证、准备、解析、初始化等,接下来我们分别来看看。 + +## 1. 加载阶段 +此阶段用于查到相应的类(通过类名进行查找)并将此类的字节流转换为方法区运行时的数据结构,然后再在内存中生成一个能代表此类的 `java.lang.Class` 对象,作为其他数据访问的入口。 + +> 小贴士:需要注意的是加载阶段和连接阶段的部分动作有可能是交叉执行的,比如一部分字节码文件格式的验证,在加载阶段还未完成时就已经开始验证了。 + +## 2.验证阶段 + +此步骤主要是为了验证字节码的安全性,如果不做安全校验的话可能会载入非安全或有错误的字节码,从而导致系统崩溃,它是 JVM 自我保护的一项重要举措。 + +验证的主要动作大概有以下几个: + +* **文件格式校验**包括常量池中的常量类型、Class 文件的各个部分是否被删除或被追加了其他信息等; +* **元数据校验**包括父类正确性校验(检查父类是否有被 final 修饰)、抽象类校验等; +* **字节码校验**,此步骤最为关键和复杂,主要用于校验程序中的语义是否合法且符合逻辑; +* **符号引用校验**,对类自身以外比如常量池中的各种符号引用的信息进行匹配性校验。 + +## 3. 准备阶段 +此阶段是用来初始化并为类中定义的静态变量分配内存的,这些静态变量会被分配到方法区上。 + +HotSpot 虚拟机在 JDK 1.7 之前都在方法区,而 JDK 1.8 之后此变量会随着类对象一起存放到 Java 堆中。 + +## 4. 解析阶段 +此阶段主要是用来解析类、接口、字段及方法的,解析时会把符号引用替换成直接引用。 + +所谓的符号引用是指以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可;而直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。 + +符号引用和直接引用有一个重要的区别:使用符号引用时被引用的目标不一定已经加载到内存中;而使用直接引用时,引用的目标必定已经存在虚拟机的内存中了。 + +## 5. 初始化 +初始化阶段 JVM 就正式开始执行类中编写的 Java 业务代码了。到这一步骤之后,类的加载过程就算正式完成了。 + +# 小结 +本课时讲了 JVM 的内存布局主要分为:堆、方法区、程序计数器、虚拟机栈和本地方法栈,并讲了 JVM 的执行流程,先把 Java 代码编译成字节码,再把字节码加载到运行时数据区;然后交给 JVM 引擎把字节码翻译为操作系统可以执行的指令进行执行;最后还讲了类加载的 5 个阶段:加载、验证、准备、解析和初始化。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/24.垃圾回收算法有哪些?.md b/Java基础教程/Java面试原理/24.垃圾回收算法有哪些?.md new file mode 100644 index 00000000..b64a31f7 --- /dev/null +++ b/Java基础教程/Java面试原理/24.垃圾回收算法有哪些?.md @@ -0,0 +1,146 @@ +# 垃圾回收算法有哪些? + +说到 Java 虚拟机不得不提的一个词就是**“垃圾回收”(GC,Garbage Collection)**,而垃圾回收的执行速度则影响着整个程序的执行效率,所以我们需要知道更多关于垃圾回收的具体执行细节,以便为我们选择合适的垃圾回收器提供理论持。 + +我们本课时的面试题是,如何判断一个对象是否“死亡”?垃圾回收的算法有哪些? + +## 典型回答 +垃圾回收器首先要做的就是,判断一个对象是存活状态还是死亡状态,死亡的对象将会被标识为垃圾数据并等待收集器进行清除。 + +判断一个对象是否为死亡状态的常用算法有两个:引用计数器算法和可达性分析算法。 + +**引用计数算法(Reference Counting)** 属于垃圾收集器最早的实现算法了,它是指在创建对象时关联一个与之相对应的计数器,当此对象被使用时加 1,相反销毁时 -1。当此计数器为 0 时,则表示此对象未使用,可以被垃圾收集器回收。 + +引用计数算法的优缺点很明显,其优点是垃圾回收比较及时,实时性比较高,只要对象计数器为 0,则可以直接进行回收操作;而缺点是无法解决循环引用的问题,比如以下代码: + +```java +class CustomOne { +    private CustomTwo two; +    public CustomTwo getCustomTwo() { +        return two; +    } +    public void setCustomTwo(CustomTwo two) { +        this.two = two; +    } +} +class CustomTwo { +    private CustomOne one; +    public CustomOne getCustomOne() { +        return one; +    } +    public void setCustomOne(CustomOne one) { +        this.one = one; +    } +} +public class RefCountingTest { +    public static void main(String[] args) { +        CustomOne one = new CustomOne(); +        CustomTwo two = new CustomTwo(); +        one.setCustomTwo(two); +        two.setCustomOne(one); +        one = null; +        two = null; + } +} +``` + +即使 one 和 two 都为 null,但因为循环引用的问题,两个对象都不能被垃圾收集器所回收。 + +**可达性分析算法(Reachability Analysis)** 是目前商业系统中所采用的判断对象死亡的常用算法,它是指从对象的起点(GC Roots)开始向下搜索,如果对象到 GC Roots 没有任何引用链相连时,也就是说此对象到 GC Roots 不可达时,则表示此对象可以被垃圾回收器所回收,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619124046.png) + +当确定了对象的状态之后(存活还是死亡)接下来就是进行垃圾回收了,垃圾回收的常见算法有以下几个: + +* 标记-清除算法; +* 标记-复制算法; +* 标记-整理算法。 + +**标记-清除(Mark-Sweep)**算法属于最早的垃圾回收算法,它是由标记阶段和清除阶段构成的。标记阶段会给所有的存活对象做上标记,而清除阶段会把没有被标记的死亡对象进行回收。而标记的判断方法就是前面讲的引用计数算法和可达性分析算法。 + +标记-清除算法的执行流程如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619124458.png) + +从上图可以看出,标记-清除算法有一个最大的问题就是会产生内存空间的碎片化问题,也就是说标记-清除算法执行完成之后会产生大量的不连续内存,这样当程序需要分配一个大对象时,因为没有足够的连续内存而导致需要提前触发一次垃圾回收动作。 + +**标记-复制算法**是标记-清除算法的一个升级,使用它可以有效地解决内存碎片化的问题。它是指将内存分为大小相同的两块区域,每次只使用其中的一块区域,这样在进行垃圾回收时就可以直接将存活的东西复制到新的内存上,然后再把另一块内存全部清理掉。这样就不会产生内存碎片的问题了,其执行流程如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619125111.png) + +标记-复制的算法虽然可以解决内存碎片的问题,但同时也带来了新的问题。因为需要将内存分为大小相同的两块内存,那么内存的实际可用量其实只有原来的一半,这样此算法导致了内存的可用率大幅降低了。 + +**标记-整理算法**的诞生晚于标记-清除算法和标记-复制算法,它也是由两个阶段组成的:标记阶段和整理阶段。其中标记阶段和标记-清除算法的标记阶段一样,不同的是后面的一个阶段,标记-整理算法的后一个阶段不是直接对内存进行清除,而是把所有存活的对象移动到内存的一端,然后把另一端的所有死亡对象全部清除,执行流程图如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619125735.png) + +## 考点分析 +本题目考察的是关于垃圾收集的一些理论算法问题,都属于概念性的问题,只要深入理解之后还是挺容易记忆的。和此知识点相关的面试题还有这些: + +* Java 中可作为 CG Roots 的对象有哪些? +* 说一下死亡对象的判断细节? + +# 知识扩展 +## GC Roots + +在 Java 中可以作为 GC Roots 的对象,主要包含以下几个: + +* 所有被同步锁持有的对象,比如被 synchronize 持有的对象; +* 字符串常量池里的引用(String Table); +* 类型为引用类型的静态变量; +* 虚拟机栈中引用对象; +* 本地方法栈中的引用对象。 + +## 死亡对象判断 +当使用可达性分析判断一个对象不可达时,并不会直接标识这个对象为死亡状态,而是先将它标记为“待死亡”状态再进行一次校验。校验的内容就是此对象是否重写了 finalize() 方法,如果该对象重写了 finalize() 方法,那么这个对象将会被存入到 F-Queue 队列中,等待 JVM 的 Finalizer 线程去执行重写的 finalize() 方法,在这个方法中如果此对象将自己赋值给某个类变量时,则表示此对象已经被引用了。因此不能被标识为死亡状态,其他情况则会被标识为死亡状态。 + +以上流程对应的示例代码如下: + +```java +public class FinalizeTest { +    // 需要状态判断的对象 +    public static FinalizeTest Hook = null; +    @Override +    protected void finalize() throws Throwable { +        super.finalize(); +        System.out.println("执行了 finalize 方法"); +        FinalizeTest.Hook = this; +    } +    public static void main(String[] args) throws InterruptedException { +        Hook = new FinalizeTest(); +        // 卸载对象,第一次执行 finalize() +        Hook = null; +        System.gc(); +        Thread.sleep(500); // 等待 finalize() 执行 +        if (Hook != null) { +            System.out.println("存活状态"); +        } else { +            System.out.println("死亡状态"); +        } +        // 卸载对象,与上一次代码完全相同 +        Hook = null; +        System.gc(); +        Thread.sleep(500); // 等待 finalize() 执行 +        if (Hook != null) { +            System.out.println("存活状态"); +        } else { +            System.out.println("死亡状态"); +        } +    } +} +``` + +上述代码的执行结果为: + +``` +执行了 finalize 方法 +存活状态 +死亡状态 +``` + +从结果可以看出,卸载了两次对象,第一次执行了 finalize() 方法,成功地把自己从待死亡状态拉了回来;而第二次同样的代码却没有执行 finalize() 方法,从而被确认为了死亡状态,这是因为**任何对象的 finalize() 方法都只会被系统调用一次**。 + +虽然可以从 finalize() 方法中把自己从死亡状态“拯救”出来,但是不建议这样做,因为所有对象的 finalize() 方法只会执行一次。因此同样的代码可能产生的结果是不同的,这样就给程序的执行带来了很大的不确定性。 + +# 小结 +本课时讲了对象状态判断的两种算法:引用计数算法和可达性分析算法。其中引用计数算法无法解决循环引用的问题,因此对于绝大多数的商业系统来说使用的都是可达性分析算法;同时还讲了垃圾回收的三种算法:标记-清除算法、标记-复制算法、标记-整理算法,其中,标记-清除算法会带来内存碎片的问题,而标记-复制算法会降低内存的利用率。所以,标记-整理算法算是一个不错的方案。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/25.你用过哪些垃圾回收器?它们有什么区别?.md b/Java基础教程/Java面试原理/25.你用过哪些垃圾回收器?它们有什么区别?.md new file mode 100644 index 00000000..3659e993 --- /dev/null +++ b/Java基础教程/Java面试原理/25.你用过哪些垃圾回收器?它们有什么区别?.md @@ -0,0 +1,81 @@ +# 你用过哪些垃圾回收器?它们有什么区别? + +上一课时我们讲了垃圾回收的理论知识,而本课时将介绍这些理论知识的具体实践。垃圾回收器也叫垃圾收集器,不同的厂商对垃圾收集器的实现也是不同的,这里主要介绍目前使用最广泛的 OracleJDK 中自带的 HotSpot 虚拟机中的几个垃圾收集器。 + +我们本课时的面试题是,你用过哪些垃圾回收器?它们有什么区别? + +## 典型回答 +《Java 虚拟机规范》并没有对垃圾收集器的具体实现做任何的规定,因此每家垃圾收集器的实现方式都不同,但比较常用的垃圾回收器是 OracleJDK 中自带的 HotSpot 虚拟机。HotSpot 中使用的垃圾收集器主要包括 7 个:**Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS 和 G1(Garbage First)收集器**。 + + **Serial 收集器**属于最早期的垃圾收集器,也是 JDK 1.3 版本之前唯一的垃圾收集器。它是单线程运行的垃圾收集器,其单线程是指在进行垃圾回收时所有的工作线程必须暂停,直到垃圾回收结束为止,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619133514.png) + +Serial 收集器的特点是简单和高效,并且本身的运行对内存要求不高,因此它在客户端模式下使用的比较多。 + +**ParNew收集器**实际上是 Serial 收集器的多线程并行版本,运行示意图如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619133550.png) + +**Parallel Scavenge 收集器**和 ParNew 收集器类似,它也是一个并行运行的垃圾回收器;不同的是,该收集器关注的侧重点是实现一个可以控制的吞吐量。而这个吞吐量计算的也很奇怪,它的计算公式是:用户运行代码的时间 / (用户运行代码的时间 + 垃圾回收执行的时间)。比如用户运行的时间是 8 分钟,垃圾回收运行的时间是 2 分钟,那么吞吐量就是 80%。Parallel Scavenge 收集器追求的目标就是将这个吞吐量的值,控制在一定的范围内。 + +Parallel Scavenge 收集器有两个重要的参数: + +* -XX:MaxGCPauseMillis 参数:它是用来控制垃圾回收的最大停顿时间; +* -XX:GCTimeRatio 参数:它是用来直接设置吞吐量的值的。 + +**Serial Old 收集器**为 Serial 收集器的老年代版本,而 Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本。 + +**CMS(Concurrent Mark Sweep)收集器**与以吞吐量为目标的 Parallel Scavenge 收集器不同,它强调的是提供最短的停顿时间,因此可能会牺牲一定的吞吐量。它主要应用在 Java Web 项目中,它满足了系统需要短时间停顿的要求,以此来提高用户的交互体验。 + +**Garbage First(简称 G1)收集器**是历史发展的产物,也是一款更先进的垃圾收集器,主要面向服务端应用的垃圾收集器。它将内存划分为多个 Region 分区,回收时则以分区为单位进行回收,这样它就可以用相对较少的时间优先回收包含垃圾最多区块。从 JDK 9 之后也成了官方默认的垃圾收集器,官方也推荐使用 G1 来代替选择 CMS 收集器。 + +## 考点分析 +JVM 内存布局和垃圾回收算法是面试中常考的题目,也是我们理解并优化 Java 程序的理论基础,而对于垃圾收集器来说除了目前主流版本(JDK 8)常用的 CMS 之外,其他的垃圾收集器都属于面试中的加分项。对于 G1 和 JDK 11 中的 ZGC 的理解代表了你对技术的热爱和新技术的敏感程度,也属于面试中的重要加分项。 + +和此知识点相关的面试题还有以下这些: + +* 讲一下分代收集理论? +* CMS 收集器的具体执行流程是什么? +* 讲一下 JDK 11 中的 ZGC 收集器? + +# 知识扩展 +## 1.分代收集 + +说到垃圾收集器不得不提的一个理论就是“分代收集”,因为目前商用虚拟机的垃圾收集器都是基于分代收集的理论进行设计的,它是指将不同“年龄”的数据分配到不同的内存区域中进行存储,所谓的“年龄”指的是经历过垃圾收集的次数。这样我们就可以把那些朝生暮死的对象集中分配到一起,把不容易消亡的对象分配到一起,对于不容易死亡的对象我们就可以设置较短的垃圾收集频率,这样就能消耗更少的资源来实现更理想的功能了。 + +通常情况下分代收集算法会分为两个区域:新生代(Young Generation)和老年代(Old Generation),其中新生代用于存储刚刚创建的对象,这个区域内的对象存活率不高,而对于经过了一定次数的 GC 之后还存活下来的对象,就可以成功晋级到老生代了。 + +对于上面介绍的 7 个垃圾收集器来说,新生代垃圾收集器有:Serial、ParNew、Parallel Scavenge,老生代的垃圾收集器有:Serial Old、Parallel Old、CMS,而 G1 属于混合型的垃圾收集器,如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619134108.png) + +## 2. CMS 收集器的具体执行流程 +CMS 收集器是基于标记-清除算法实现的,我们之前有讲过关于标记-清除的算法,这里简单地回顾一下。标记-清除的算法是由标记阶段和清除阶段构成的,标记阶段会给所有的存活对象做上标记;而清除阶段会把被标记为死亡的对象进行回收,而死亡对象的判断是通过引用计数法或者是目前主流的可达性分析算法实现的。但是 CMS 的实现稍微复杂一些,它的整个过程可以分为四个阶段: + +* 初始标记(CMS initial mark) +* 并发标记(CMS concurrent mark) +* 重新标记(CMS remark) +* 并发清除(CMS concurrent sweep) + +首先,初始标记阶段的执行时间很短,它只是标记一下 GC Roots 的关联对象;并发阶段是从 GC Roots 关联的对象进行遍历判断并标识死亡对象,这个过程比较慢,但不需要停止用户线程,用户的线程可以和垃圾收集线程并发执行;而重新标记阶段则是为了判断并标记,刚刚并发阶段用户继续运行的那一部分对象,所以此阶段的执行时间也比较短;最后是并发清除阶段,也就是清除上面标记的死亡对象,由于 CMS 使用的是标记-清除算法,而非标记-整理算法,因此无须移动存活的对象,这个阶段垃圾收集线程也可以和用户线程并发执行。 + +CMS 的整个执行过程中只有执行时间很短的初始标记和重新标记需要 Stop The World(全局停顿)的,执行过程如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200619134706.png) + +因为 CMS 是一款基于标记清除算法实现的垃圾收集器,因此会在收集时产生大量的空间碎片,为了解决这个问题,CMS 收集器提供了一个 `-XX:+UseCMS-CompactAtFullCollection` 的参数(默认是开启的,此参数从 JDK9 开始废弃),用于在 CMS 收集器进行 Full GC 时开启内存碎片的合并和整理。 + +但又因为碎片整理的过程必须移动存活的对象,所以它和用户线程是无法并发执行的,为了解决这个问题 CMS 收集器又提供了另外一个参数` -XX:CMSFullGCsBefore-Compaction`,用于规定多少次(根据此参数的值决定)之后再进行一次碎片整理。 + +## 3. ZGC +ZGC 收集器是 JDK 11 中新增的垃圾收集器,它是由 Oracle 官方开发的,并且支持 TB 级别的堆内存管理,而且 ZGC 收集器也非常高效,可以做到 10ms 以内完成垃圾收集。 + +在 ZGC 收集器中没有新生代和老生代的概念,它只有一代。ZGC 收集器采用的着色指针技术,利用指针中多余的信息位来实现着色标记,并且 ZGC 使用了读屏障来解决 GC 线程和应用线程可能存在的并发(修改对象状态的)问题,从而避免了Stop The World(全局停顿),因此使得 GC 的性能大幅提升。 + +ZGC 的执行流程和 CMS 比较相似,首先是进行 GC Roots 标记,然后再通过指针进行并发着色标记,之后便是对标记为死亡的对象进行回收(被标记为橘色的对象),最后是重定位,将 GC 之后存活的对象进行移动,以解决内存碎片的问题。 + +# 小结 +本课时我们介绍了 JDK 11 之前的 7 种垃圾收集器:Serial、Serial Old、ParNew、Parallel Scavenge、Parallel Old、CMS、G1,其中 CMS 收集器是 JDK 8 之前的主流收集器,而 JDK 9 之后的默认收集器为 G1,并且在文章的最后,介绍了性能更加强悍、综合表现更好的 ZGC 收集器,希望本课时的内容可以切实的帮助到你。 + +OK,这节课就讲到这里啦,下一课时我将分享“生产环境如何排查和优化 JVM?”,记得按时来听课哈。 \ No newline at end of file diff --git a/Java基础教程/Java面试原理/26.生产环境如何排除和优化JVM?.md b/Java基础教程/Java面试原理/26.生产环境如何排除和优化JVM?.md new file mode 100644 index 00000000..6a72a3c0 --- /dev/null +++ b/Java基础教程/Java面试原理/26.生产环境如何排除和优化JVM?.md @@ -0,0 +1,372 @@ +# 生产环境如何排除和优化 JVM? + +通过前面几个课时的学习,相信你对 JVM 的理论及实践等相关知识有了一个大体的印象。而本课时将重点讲解 JVM 的排查与优化,这样就会对 JVM 的知识点有一个完整的认识,从而可以更好地应用于实际工作或者面试了。 + +我们本课时的面试题是,生产环境如何排查问题? + +## 典型回答 +如果是在生产环境中直接排查 JVM 的话,最简单的做法就是使用 JDK 自带的 6 个非常实用的命令行工具来排查。它们分别是:jps、jstat、jinfo、jmap、jhat 和 jstack,它们都位于 JDK 的 bin 目录下,可以使用命令行工具直接运行,其目录如下图所示: + +![](https://gitee.com/krislin_zhao/IMGcloud/raw/master/img/20200620130932.png) + +### 1. jps(虚拟机进程状况工具) +jps(JVM Process Status tool,虚拟机进程状况工具)它的功能和 Linux 中的 ps 命令比较类似,用于列出正在运行的 JVM 的 LVMID(Local Virtual Machine IDentifier,本地虚拟机唯一 ID),以及 JVM 的执行主类、JVM 启动参数等信息。语法如下: + +```bash +jps [options] [hostid] +``` + +常用的 options 选项: + +* -l:用于输出运行主类的全名,如果是 jar 包,则输出 jar 包的路径; +* -q:用于输出 LVMID(Local Virtual Machine Identifier,虚拟机唯一 ID); +* -m:用于输出虚拟机启动时传递给主类 main() 方法的参数; +* -v:用于输出启动时的 JVM 参数。 + +### 2. jstat(虚拟机统计信息监视工具) +jstat(JVM Statistics Monitoring Tool,虚拟机统计信息监视工具)用于监控虚拟机的运行状态信息。 + +例如,我们用它来查询某个 Java 进程的垃圾收集情况,示例如下: + +```bash +➜  jstat -gc 43704 + S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT    CGC    CGCT     GCT +10752.0 10752.0  0.0    0.0   65536.0   5243.4   175104.0     0.0     4480.0 774.0  384.0   75.8       0    0.000   0      0.000   -          -    0.000 +``` + +参数说明如下表所示: + +| 参数 | 说明 | +| :--: | :-----------------------------------------------: | +| S0C | 年轻代中第一个存活区的大小 | +| S1C | 年轻代中第二个存活区的大小 | +| S0U | 年轻代中第一个存活区已使用的空间(字节) | +| S1U | 年轻代中第二个存活区已使用的空间(字节) | +| EC | Edem 区大小 | +| EU | 年轻代中 Edem 区已使用的空间(字节) | +| OC | 老年代大小 | +| OU | 老年代已使用的空间(字节) | +| YGC | 从应用程序启动到采样时 young gc 的次数 | +| YGCT | 从应用程序启动到采样时 young gc 的所用的时间(s) | +| FGC | 从应用程序启动到采样时 full gc 的次数 | +| FGCT | 从应用程序启动到采样时 full gc 的所用的时间 | +| GCT | 从应用程序启动到采样时整个 gc 所用的时间 | + +> 注意:年轻代的 Edem 区满了会触发 young gc,老年代满了会触发 old gc。full gc 指的是清除整个堆,包括 young 区 和 old 区。 + +jstat 常用的查询参数有: + +* -class,查询类加载器信息; +* -compiler,JIT 相关信息; +* -gc,GC 堆状态; +* -gcnew,新生代统计信息; +* -gcutil,GC 堆统计汇总信息。 + +### 3. jinfo(查询虚拟机参数配置工具) +jinfo(Configuration Info for Java)用于查看和调整虚拟机各项参数。语法如下: + +```bash +jinfo