开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

什么是泛型? 一、泛型的概念 二、泛型的意义 三、泛型的表示

发表于 2020-01-18

一、泛型的概念

泛型是 Java SE5 出现的新特性,泛型的本质是类型参数化或参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。

二、泛型的意义

一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大。

Java 在引入泛型之前,表示可变对象,通常使用 Object 来实现,但是在进行类型强制转换时存在安全风险。有了泛型后:

  • 编译期间确定类型,保证类型安全,放的是什么,取的也是什么,不用担心抛出 ClassCastException 异常。
  • 提升可读性,从编码阶段就显式地知道泛型集合、泛型方法等处理的对象类型是什么。
  • 泛型合并了同类型的处理代码提高代码的重用率,增加程序的通用灵活性。

举个例子:

1
2
3
4
5
6
7
8
9
10
java复制代码public static void method1() {
List list = new ArrayList();
List.add(22);
List.add("hncboy");
List.add(new Object());

for (Object o : list) {
System.out.println(o.getClass());
}
}

未使用泛型前,我们对集合可以进行任意类型的 add 操作,遍历结果都被转换成 Object 类型,因为不确定集合里存放的具体类型,输出结果如下所示。

1
2
3
kotlin复制代码class java.lang.Integer
class java.lang.String
class java.lang.Object

采用泛型之后,创建集合对象可以明确的指定类型,在编译期间就确定了该集合存储的类型,存储其他类型的对象编译器会报错。这时遍历集合就可以直接采用明确的 String 类型输出。

1
2
3
4
5
6
7
8
9
10
java复制代码public static void method2() {
List<String> list = new ArrayList();
list.add("22");
list.add("hncboy");
//list.add(new Object()); 报错

for (String s : arrayList) {
System.out.println(s);
}
}

三、泛型的表示

泛型可以定义在类、接口、方法中,分别表示为泛型类、泛型接口、泛型方法。泛型的使用需要先声明,声明通过**<符号>的方式,符号可以任意,编译器通过识别尖括号和尖括号内的字母来解析泛型。泛型的类型只能为类,不能为基本数据类型。尖括号的位置也是固定的,只能在类名之后或方法返回值之前**。

一般泛型有约定的符号:E 代表 Element, 通常在集合中使用;T 代表 Type,通常用于表示类;K 代表 Key,V 代表 Value,<K, V> 通常用于键值对的表示;? 代表泛型通配符。

泛型的表达式有如下几种:

  • 普通符号
  • 无边界通配符 <?>
  • 上界通配符 <? extends E> 父类是 E
  • 下界通配符 <? super E> 是 E 的父类

四、泛型的使用

4.1 泛型类

将泛型定义在类名后,使得用户在使用该类时,根据不同情况传入不同类型。在类上定义的泛型,在实例方法中可以直接使用,不需要定义,但是静态方法上的泛型需要在静态方法上声明,不能直接使用。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Test<T> {

private T data;

public T getData() {
return data;
}

/** 这种写法是错误的,提示 T 未定义 */
/*public static T get() {
return null;
}*/
/** 正确写法,该方法上的 T 和类上的 T 虽然一样,但是是两个指代,可以完全相同,互不影响 */
public static <T> T get() {
return null;
}

public void setData(T data) {
this.data = data;
}
}

4.2 泛型方法

泛型方法,是在调用方法时指明的具体的泛型类型。虽然类上定义的泛型,实例方法中可以直接使用,但是该方法不属于泛型方法。举个例子:get 方法为泛型方法,而且该程序能编译通过运行,因为尖括号里的每个元素都指代一种未知类型,可以为任何符号,尖括号里的 String 并非 java.lang.String 类型,而是作为泛型标识 ,传入的 first 为 Integer 类型,所以该 String 标识符也指代 Integer 类型,返回值自然也是 Integer 类型。不过,应该也不会用这种泛型符号定义在实际情况中。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Test {

public static <String, T, Hncboy> String get(String string, Hncboy hncboy) {
return string;
}

public static void main(String[] args) {
Integer first = 666;
Double second = 888.0;
Integer result = get(first, second);
System.out.println(result);
}
}

4.3 泛型通配符

? 为泛型非限定通配符,表示类型未知,不用声明,可以匹配任意的类。该通配符只能读,不能写,且不对返回值进行操作。也可以将非限定通配符出现的地方用普通泛型标识,不过使用通配符更简洁。举个例子:

test1() 是通过通配符来输出集合的每一个元素的,test2() 和 test1() 的作用一样,只不过将通配符用 来代替了;test3() 用来演示集合在通配符的情况下写操作,发现编译器报错,int 和 String 都不属于 ? 类型,当然放不进集合,因为所有类都有 null 元素,所以可以放进集合。比如主函数传的是 List,而想要在集合里添加一个 String,这是不可能的;test4() 的写法也是错的,? 是不确定,返回值返回不了;test5() 的用法使用来比较 List 和 List<?> 的,在主函数里调用 test5(list) 报错的,显示 java: 不兼容的类型: java.util.List<java.lang.Integer>无法转换为java.util.List<java.lang.Object>,因为 List 不是 List 的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码public class Test {

public static void test1(List<?> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static <T> void test2(List<T> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static void test3(List<?> list) {
//list.add(1); capture of ?
//list.add("1"); capture of ?
list.add(null);
}

/*public static ? test4(List<?> list) {
return null;
}*/

public static void test5(List<Object> list) {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}

public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
test1(list);
test2(list);
//test5(list);
}
}

通过使用泛型通配符可以实现泛型的上下边界 <? extend T> 和 <? super T>,下面将使用 Number 类以及该类的子类来演示这两种上下型边界,Number 类的关系图如下。

<? extends Number> 表示类型为 Number 或 Number 的子类,<? super Integer> 表示类型为 Integer 或 Integer 的父类,举个例子,method1 方法测试是上边界 Number,由于 arrayList1 和 arrayList2 的泛型都为 Number 或其子类,所以可以插入成功,而 arrayList3 的类型 String 和 Number 无关,因此编译报错。method2 方法测试的是下边界 Integer,由于 arrayList4,arrayList5 和 arrayList7 种的类型 Integer、Object 和 Number 都为 Integer 的父类,所以插入成功,而 arrayList7 的类型 Double,因此插入失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码public class Generic {

public static void main(String[] args) {
ArrayList<Integer> arrayList1 = new ArrayList<>();
ArrayList<Number> arrayList2 = new ArrayList<>();
ArrayList<String> arrayList3 = new ArrayList<>();
method1(arrayList1);
method1(arrayList2);
//method1(arrayList3);

ArrayList<Integer> arrayList4 = new ArrayList<>();
ArrayList<Object> arrayList5 = new ArrayList<>();
ArrayList<Number> arrayList6 = new ArrayList<>();
ArrayList<Double> arrayList7 = new ArrayList<>();
method2(arrayList4);
method2(arrayList5);
method2(arrayList6);
//method2(arrayList7)
}

public static void method1(ArrayList<? extends Number> arrayList) {
}

public static void method2(ArrayList<? super Integer> arrayList) {
}
}

4.4 泛型接口

泛型接口就是在接口上定义的泛型,当一个类型未确定的类实现接口时,需要声明该类型。举个例子:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public interface CalcGeneric<T> {
T add(T num1, T num2);
}

public class CalculatorGeneric<T> implements CalcGeneric<T> {

@Override
public T add(T num1, T num2) {
return null;
}
}

4.5 泛型数组

数组是支持协变的,什么是数组的协变呢?举个例子:这段代码中,数组支持以 1 的方式定义数组,因为 Integer 是 Number 的子类,一个 Integer 对象也是一个 Number 对象,所以一个 Integer 的数组也是一个 Number 的数组,这就是数组的协变。虽然这种写法编译时能通过,但是数组实际上存储的是 Integer 对象,如果加入 Double 对象,那么在运行时就会抛出 ArrayStoreException 异常,该种设计存在缺陷。3 方式所示的定义数组方式编译错误,4 所指示的代码才是正确的。泛型是不变的,没有内建的协变类型,使用泛型的时候,类型信息在编译期会被类型擦除,所以泛型将这种错误检测移到了编译器。泛型的设计目的之一就是保证了类型安全,让这种运行时期的错误在编译期就能发现,所以泛型是不支持协变的,如 5 所示的该行代码会有编译错误,

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class Test {

public static void main(String[] args) {
Number[] numbers = new Integer[10]; // 1
// java.lang.ArrayStoreException: java.lang.Double
numbers[0] = new Double(1); // 2
//List<String>[] list = new ArrayList<String>[10]; // 3
List<String>[] list2 = new ArrayList[10]; // 4
//List<Number> list3 = new ArrayList<Integer>(); // 5
}
}

4.6 泛型擦除

在泛型内部,无法获得任何有关泛型参数类型的信息,泛型只在编译阶段有效,泛型类型在逻辑上可看成是多个不同的类型,但是其实质都是同一个类型。因为泛型是在JDK5之后才出现的,需要处理 JDK5之前的非泛型类库。擦除的核心动机是它使得泛化的客户端可以用非泛化的类库实现,反之亦然,这经常被称为”迁移兼容性”。

代价:泛型不能用于显式地引用运行时类型地操作之中,例如转型、instanceof 操作和 new 表达式,因为所有关于参数地类型信息都丢失了。无论何时,当你在编写这个类的代码的时候,提醒自己,他只是个Object。catch 语句不能捕获泛型类型的异常。

举个例子:这串代码的运行输出是,因此可见泛型在运行期间对类型进行了擦除。

1
2
3
kotlin复制代码class java.util.ArrayList
class java.util.ArrayList
true
1
2
3
4
5
6
7
8
java复制代码public static void method1() {
List<Integer> integerArrayList = new ArrayList();
List<String> stringArrayList = new ArrayList();

System.out.println(integerArrayList.getClass());
System.out.println(stringArrayList.getClass());
System.out.println(integerArrayList.getClass() == stringArrayList.getClass());
}

将上面的 Java 代码编译成字节码后查看也可看见两个集合都是 java/util/ArrayList

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static method1()V
L0
LINENUMBER 14 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 0
L1
LINENUMBER 15 L1
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList.<init> ()V
ASTORE 1

因为在运行期间类型擦除的关系,可以通过反射在运行期间修改集合能添加的类,不过添加后查询该集合会抛出 ClassCastException 异常,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public static void method4() throws Exception {
ArrayList<String> stringArrayList = new ArrayList<>();
stringArrayList.add("hnc");
stringArrayList.add("boy");
System.out.println("之前长度:" + stringArrayList.size());

// 通过反射增加元素
Class<?> clazz = stringArrayList.getClass();
Method method = clazz.getDeclaredMethod("add", Object.class);
method.invoke(stringArrayList, 60);

System.out.println("之后长度:" + stringArrayList.size());
// 存的还是 Integer 类型
// java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
for (int i = 0; i < stringArrayList.size(); i++) {
System.out.println(stringArrayList.get(i).getClass());
}
}

五、总结

泛型在平时的学习中用到的还是挺多的。

  • 数组不支持泛型
  • 泛型的类型不能为基础数据类型
  • 泛型只在编译阶段有效

Java 编程思想

码出高效 Java 开发手册

java 泛型详解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一致性Hash算法的实现原理

发表于 2020-01-17

Hash环

我们把232次方想成一个环,比如钟表上有60个分针点组成一个圆,那么hash环就是由232个点组成的圆。第一个点是0,最后一个点是232-1,我们把这232个点组成的环称之为HASH环。

​

一致性Hash算法

将memcached物理机节点通过Hash算法虚拟到一个虚拟闭环上(由0到232构成),key请求的时候通过Hash算法计算出Hash值然后对232取模,定位到环上顺时针方向最接近的虚拟物理节点就是要找到的缓存服务器。

假设有ABC三台缓存服务器:

我们使用这三台服务器各自的IP进行hash计算然后对2~32取模即:

1
复制代码***Hash(服务器IP)%2~32***

计算出来的结果是0到2~32-1的一个整数,那么Hash环上必有一个点与之对应。比如:

图片

图片

现在缓存服务器已经落到了Hash环上,接下来我们就看我们的数据是怎么放到缓存服务器的?

我们可以同样对Object取Hash值然后对2~32取模,比如落到了接近A的一个点上:

图片

那么这个数据理应存到A这个缓存服务器节点上

图片

所以,在缓存服务器节点数量不变的情况下,缓存的落点是不会变的。

图片

但是如果B挂掉了呢?

按照hash且取模的算法,图中3这个Object理应就分配到了C这个节点上去了,所以就会到C上找缓存数据,结果当然是找不到,进而从DB读取数据重新放到了C上。

图片

但是对于编号为1,2的Object还是落到A,编号为4的Object还是落到C,B宕机所影响的仅仅是3这个Object。这就是一致性Hash算法的优点。

​

Hash环的倾斜

前面我们理想化的把三台memcache机器均匀分到了Hash环上:

图片

但是现实情况可能是:

图片

如果Hash环倾斜,即缓存服务器过于集中将会导致大量缓存数据被分配到了同一个服务器上。比如编号1,2,3,4,6的Object都被存到了A,5被存到B,而C上竟然一个数据都没有,这将造成内存空间的浪费。

为了解决这个问题,一致性Hash算法中使用“虚拟节点”解决。

图片

虚拟节点解决Hash环倾斜

图片

“虚拟节点”是“实际节点”在hash环上的复制品,一个实际节点可能对应多个虚拟节点。这样就可以将ABC三台服务器相对均匀分配到Hash环上,以减少Hash环倾斜的影响,使得缓存被均匀分配到hash环上。

Hash算法平衡性

平衡性指的是hash的结果尽可能分布到所有的缓存中去,这样可以使得所有的缓存空间都可以得到利用。但是hash算法不保证绝对的平衡性,为了解决这个问题一致性hash引入了“虚拟节点”的概念。虚拟节点”( virtual node )是实际节点在 hash 空间的复制品( replica ),一实际个节点对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以 hash 值排列。“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。

1
复制代码例如假设 cache A 的 IP 地址为202.168.14.241 。

引入“虚拟节点”前,计算 cache A 的 hash 值:

1
复制代码Hash(“202.168.14.241”);

引入“虚拟节点”后,计算“虚拟节”点 cache A1 和 cache A2 的 hash 值:

1
复制代码Hash(“202.168.14.241#1”);  // cache A1
1
复制代码Hash(“202.168.14.241#2”);  // cache A2

这样只要是命中cacheA1和cacheA2节点,就相当于命中了cacheA的缓存。这样平衡性就得到了提高。

各位老铁,觉得可以麻烦点个赞谢谢了!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DDD 模式从天书到实践

发表于 2020-01-16

背景

正所谓有人的地方就有江湖,有设计的地方也一定会有架构。如果你是一位软件行业的老鸟,你一定会有这样的经历:一个业务的初期,普通的 CRUD 就能满足,业务线也很短,此时系统的一切都看起来很 nice,但随着迭代的不断演化,以及业务逻辑越来越复杂,我们的系统也越来越冗杂,模块彼此关联,甚至没有人能描述清楚每个细节。当新需求需要修改一个功能时,往往光回顾该功能涉及的流程就需要很长时间,更别提修改带来的不可预知的影响面。于是 RD 就加开关,小心翼翼地切流量上线,一有问题赶紧关闭开关。面对此般场景,你要么跑路,要么重构。重构是克服演进式设计中大杂烩问题的主力,通过在单独的类及方法级别上做一系列小步重构来完成,我们可以很容易重构出一个独立的类来放某些通用的逻辑,但是,你会发现你很难给它一个业务上的含义,只能给予一个技术维度描绘的含义。你正在一边重构一边给后人挖坑。在互联网开发“小步快跑,迭代试错”的大环境下,DDD 似乎是一种比较“古老而缓慢”的思想。然而,由于互联网公司也逐渐深入实体经济,业务日益复杂,我们在开发中也越来越多地遇到传统行业软件开发中所面临的问题。怎么解决这个问题呢?其实法宝就是今天的主题,领域驱动设计!!相信你读完本文一定会有所启发。### DDD 介绍

DDD 全程是 Domain-Driven Design,中文叫领域驱动设计,是一套应对复杂软件系统分析和设计的面向对象建模方法论。以前的系统分析和设计是分开的,导致需求和成品非常容易出现偏差,两者相对独立,还会导致沟通困难,DDD 则打破了这种隔阂,提出了领域模型概念,统一了分析和设计编程,使得软件能够更灵活快速跟随需求变化。
( 公众号:架构精进 )### DDD 的发展史

相信之前或多或少一定听说过领域驱动(DDD),繁多的概念会不会让你眼花缭乱?抽象的逻辑是不是感觉缺少落地实践?可能这也是 DDD 一直没得到盛行的原因吧。话说 1967 年有了 OOP,1982 年有了 OOAD(面向对象分析和设计),它是成熟版的 OOP,目标就是解决复杂业务场景,这个过程中逐渐形成了一个领域驱动的思潮,一转眼到 2003 年的时候,Eric Evans 发表了一篇著作 Domain-driven Design: Tackling Complexity in the Heart of Software,正式定义了领域的概念,开始了 DDD 的时代。算下来也有接近 20 年的时间了,但是,事实并不像 Eric Evans 设想的那样容易,DDD 似乎一直不温不火,没有能“风靡全球”。2013 年,Vaughn Vernon 写了一本 Implementing Domain-Driven Design 进一步定义了 DDD 的领域方向,并且给出了很多落地指导,它让人们离 DDD 又进了一步。同时期,随着互联网的兴起,Rod Johnson 这大哥以轻量级极简风格的 Spring Cloud 抢占了所有风头,虽然 Spring 推崇的失血模式并非 OOP 的皇家血统,但是谁用关心这些呢?毕竟简化开发的成本才是硬道理。就在我们用这张口闭口 Spring 的时候,我们意识到了一个严重的问题,我们应对复杂业务场景的时候,Spring 似乎并不能给出更合理的解决方案,于是分而治之的思想下应生了微服务,一改以往单体应用为多个子应用,一下子让人眼前一亮,于是我们没日没夜地拆分服务,加之微服务提供的注册中心、熔断、限流等解决方案,我们用得不亦乐乎。人们在踩过诸多拆分服务的坑(拆分过细导致服务爆炸、拆分不合理导致频分重构等)之后,开始死锁原因了,到底有没有一种方法论可以指导人们更加合理地拆分服务呢?众里寻他千百度,DDD 却在灯火阑珊处,有了 DDD 的指导,加之微服务的事件,才是完美的架构。### DDD 与微服务的关系

背景中我们说到,有 DDD 的指导,加之微服务的事件,才是完美的架构,这里就详细说下它们的关系。系统的复杂度越来越来高是必然趋势,原因可能来自自身业务的演进,也有可能是技术的创新,然而一个人和团队对复杂性的认知是有极限的,就像一个服务器的性能极限一样,解决的办法只有分而治之,将大问题拆解为小问题,最终突破这种极限。微服务在这方面都给出来了理论指导和最佳实践,诸如注册中心、熔断、限流等解决方案,但微服务并没有对“应对复杂业务场景”这个问题给出合理的解决方案,这是因为微服务的侧重点是治理,而不是分。我们都知道,架构一个系统的时候,应该从以下几方面考虑:1. 功能维度
2. 质量维度(包括性能和可用性)
3. 工程维度

微服务在第二个做得很好,但第一个维度和第三个维度做的不够。这就给 DDD 了一个“可乘之机”,DDD 给出了微服务在功能划分上没有给出的很好指导这个缺陷。所以说它们在面对复杂问题和构建系统时是一种互补的关系。从架构角度看,微服务中的服务所关注的范围,正是 DDD 所推崇的六边形架构中的领域层,和整洁架构中的 entity 和 use cases 层。如下图所示:
DDD 与微服务如何协作
知道了 DDD 与微服务还不够,我们还需要知道他们是怎么协作的。一个系统(或者一个公司)的业务范围和在这个范围里进行的活动,被称之为领域,领域是现实生活中面对的问题域,和软件系统无关,领域可以划分为子域,比如电商领域可以划分为商品子域、订单子域、发票子域、库存子域 等,在不同子域里,不同概念会有不同的含义,所以我们在建模的时候必须要有一个明确的边界,这个边界在 DDD 中被称之为限界上下文,它是系统架构内部的一个边界,《整洁之道》这本书里提到:

系统架构是由系统内部的架构边界,以及边界之间的依赖关系所定义的,与系统中组件之间的调用方式无关。
所谓的服务本身只是一种比函数调用方式成本稍高的,分割应用程序行为的一种形式,与系统架构无关。

所以复杂系统划分的第一要素就是划分系统内部架构边界,也就是划分上下文,以及明确之间的关系,这对应之前说的第一维度(功能维度),这就是 DDD 的用武之处。其次,我们才考虑基于非功能的维度如何划分,这才是微服务发挥优势的地方。假如我们把服务划分成 ABC 三个上下文:

我们可以在一个进程内部署单体应用,也可以通过远程调用来完成功能调用,这就是目前的微服务方式,更多的时候我们是两种方式的混合,比如 A 和 B 在一个部署单元内,C 单独部署,这是因为 C 非常重要,或并发量比较大,或需求变更比较频繁,这时候 C 独立部署有几个好处:

  1. C 独立部署资源:资源更合理的倾斜,独立扩容缩容。
  2. 弹力服务:重试、熔断、降级等,已达到故障隔离。
  3. 技术栈独立:C 可以使用其他语言编写,更合适个性化团队技术栈。
  4. 团队独立:可以由不同团队负责。

架构是可以演进的,所以拆分需要考虑架构的阶段,早期更注重业务逻辑边界,后期需要考虑更多方面,比如数据量、复杂性等,但即使有这个方针,也常会见仁见智,没有人能一下子将边界定义正确,其实这里根本就没有明确的对错。即使边界定义的不太合适,通过聚合根可以保障我们能够演进出更合适的上下文,在上下文内部通过实体和值对象来对领域概念进行建模,一组实体和值对象归属于一个聚合根。按照 DDD 的约束要求:* 第一,聚合根来保证内部实体规则的正确性和数据一致性;

  • 第二,外部对象只能通过 id 来引用聚合根,不能引用聚合根内部的实体;
  • 第三,聚合根之间不能共享一个数据库事务,他们之间的数据一致性需要通过最终一致性来保证。

有了聚合根,再基于这些约束,未来可以根据需要,把聚合根升级为上下文,甚至拆分成微服务,都是比较容易的。### DDD 的相关术语与基本概念

讨论完宏观概念以后,让我们来认识一下 DDD 的一些概念吧,每个概念我都为你找了一个 Spring 模式开发的映射概念,方便你理解,但要仅仅作为理解用,不要过于依赖。另外,这里你可能需要结合后面的代码反复结合理解,才能融汇贯通到实际工作中。### 领域

映射概念:切分的服务。领域就是范围。范围的重点是边界。领域的核心思想是将问题逐级细分来减低业务和系统的复杂度,这也是 DDD 讨论的核心。### 子域

映射概念:子服务。领域可以进一步划分成子领域,即子域。这是处理高度复杂领域的设计思想,它试图分离技术实现的复杂性。这个拆分的里面在很多架构里都有,比如 C4。### 核心域

映射概念:核心服务。在领域划分过程中,会不断划分子域,子域按重要程度会被划分成三类:核心域、通用域、支撑域。决定产品核心竞争力的子域就是核心域,没有太多个性化诉求。桃树的例子,有根、茎、叶、花、果、种子等六个子域,不同人理解的核心域不同,比如在果园里,核心域就是果是核心域,在公园里,核心域则是花。有时为了核心域的营养供应,还会剪掉通用域和支撑域(茎、叶等)。### 通用域

映射概念:中间件服务或第三方服务。被多个子域使用的通用功能就是通用域,没有太多企业特征,比如权限认证。### 支撑域

映射概念:企业公共服务。对于功能来讲是必须存在的,但它不对产品核心竞争力产生影响,也不包含通用功能,有企业特征,不具有通用性,比如数据代码类的数字字典系统。### 统一语言

映射概念:统一概念。定义上下文的含义。它的价值是可以解决交流障碍,不管你是 RD、PM、QA 等什么角色,让每个团队使用统一的语言(概念)来交流,甚至可读性更好的代码。通用语言包含属于和用例场景,并且能直接反应在代码中。可以在事件风暴(开会)中来统一语言,甚至是中英文的映射、业务与代码模型的映射等。可以使用一个表格来记录。### 限界上下文

映射概念:服务职责划分的边界。定义上下文的边界。领域模型存在边界之内。对于同一个概念,不同上下文会有不同的理解,比如商品,在销售阶段叫商品,在运输阶段就叫货品。

理论上,限界上下文的边界就是微服务的边界,因此,理解限界上下文在设计中非常重要。

聚合

映射概念:包。聚合概念类似于你理解的包的概念,每个包里包含一类实体或者行为,它有助于分散系统复杂性,也是一种高层次的抽象,可以简化对领域模型的理解。拆分的实体不能都放在一个服务里,这就涉及到了拆分,那么有拆分就有聚合。聚合是为了保证领域内对象之间的一致性问题。在定义聚合的时候,应该遵守不变形约束法则:1. 聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;
2. 聚合内的某些对象的状态必须满足某个业务规则:

  • 一个聚合只有一个聚合根,聚合根是可以独立存在的,聚合中其他实体或值对象依赖与聚合根。
  • 只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性。

聚合根

映射概念:包。一个上下文内可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根,一个聚合只有一个聚合根。### 实体

映射概念:Domain 或 entity。《领域驱动设计模式、原理与实践》一书中讲到,实体是具有身份和连贯性的领域概念,可以看出,实体其实也是一种特殊的领域,这里我们需要注意两点:唯一标示(身份)、连续性。两者缺一不可。你可以想象,文章可以是实体,作者也可以是,因为它们有 id 作为唯一标示。### 值对象

映射概念:Domain 或 entity。为了更好地展示领域模型之间的关系,制定的一个对象,本质上也是一种实体,但相对实体而言,它没有状态和身份标识,它存在的目的就是为了表示一个值,通常使用值对象来传达数量的形式来表示。比如 money,让它具有 id 显然是不合理的,你也不可能通过 id 查询一个 money。定义值对象要依照具体场景的区分来看,你甚至可以把 Article 中的 Author 当成一个值对象,但一定要清楚,Author 独立存在的时候是实体,或者要拿 Author 做复杂的业务逻辑,那么 Author 也会升级为聚合根。最后,给出摘自网络的一张图,比较全,索性就直接 copy 过来了,便于你宏观回顾 DDD 的相关概念:
四种 Domain 模式

除了晦涩难懂的概念外,让我们最难接受的可能就是模型的运用了,Spring 思想中,Domain 只是数据的载体,所有行为都在 Service 中使用 Domain 封装后流转,而 OOP 讲究一对象维度来执行业务,所以,DDD 中的对象是用行为的(理解这点非常重要哦)。这里我为你总结了全部的四种领域模式,供你区分和理解:1. 失血模型
2. 贫血模型
3. 充血模型
4. 胀血模型

背景

先说明一下示例背景,由于公司项目不能外泄的原因,我这里模拟一个文章管理系统(这个系统相对简单,理论上可以不使用 DDD,在这里仅做举例),业务需求有:发布文章、修改文章、文章分类搜索和展示等。使用 Spring 开发的话,你脑海中一定浮现的是如下代码。文章类:Article

1
2
3
4
5
6
7
8
9
10
复制代码public class Article implements Serializable {
private Integer id;
private String title;
private Integer classId;
private Integer authorId;
private String authorName;
private String content;
private Date pubDate;
//getter/setter/toString
}

DAO 类:ArticleDao/ArticleImpl

1
2
3
4
5
6
7
8
复制代码public interface ArticleDao extends BaseDao<Article>{
//...
}

Repository("articleDao")
public class ArticleDaoImpl implements ArticleDao{
//...
}

Service 类:ArticleService

1
2
3
4
5
6
7
8
复制代码public interface ArticleService extends BaseService<Article>{
//...
}

@Service(value="articleService")
public class ArticleServiceImpl implements ArticleService {
//...
}

Controller 类:略。### 四种模式示例

失血模型

Domain Object 只有属性的 getter/setter 方法的纯数据类,所有的业务逻辑完全由 business object 来完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class Article implements Serializable {
private Integer id;
private String title;
private Integer classId;
private Integer authorId;
private String authorName;
private String content;
private Date pubDate;
//getter/setter/toString
}

public interface ArticleDao {
public Article getArticleById(Integer id);
public Article findAll();
public void updateArticle(Article article);
}

贫血模型

简单来说,就是 Domain Object 包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到 Service 层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码public class Article implements Serializable {
private Integer id;
private String title;
private Integer classId;
private Integer authorId;
private String authorName;
private String content;
private Date pubDate;
//getter/setter/toString
//判断是否是热门分类(假设等于57或102的类别的文章就是热门分类的文章)
public boolean isHotClass(Article article){
return Stream.of(57,102)
.anyMatch(classId -> classId.equals(article.getClassId()));
}
//更新分类,但未持久化,这里不能依赖Dao去操作实体化
public Article changeClass(Article article, ArticleClass ac){
return article.setClassId(ac.getId());
}
}

@Repository("articleDao")
public class ArticleDaoImpl implements ArticleDao{
@Resource
private ArticleDao articleDao;
public void changeClass(Article article, ArticleClass ac){
article.changeClass(article, ac);
articleDao.update(article)
}
}

注意这个模式不在 Domain 层里依赖 DAO。持久化的工作还需要在 DAO 或者 Service 中进行。这样做的优缺点优点:各层单向依赖,结构清晰。缺点:* Domain Object 的部分比较紧密依赖的持久化 Domain Logic 被分离到 Service 层,显得不够 OO

  • Service 层过于厚重

充血模型

充血模型和第二种模型差不多,区别在于业务逻辑划分,将绝大多数业务逻辑放到 Domain 中,Service 是很薄的一层,封装少量业务逻辑,并且不和 DAO 打交道:

Service (事务封装) —> Domain Object <—> DAO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码public class Article implements Serializable {
@Resource
private static ArticleDao articleDao;
private Integer id;
private String title;
private Integer classId;
private Integer authorId;
private String authorName;
private String content;
private Date pubDate;
//getter/setter/toString
//使用articleDao进行持久化交互
public List<Article> findAll(){
return articleDao.findAll();
}
//判断是否是热门分类(假设等于57或102的类别的文章就是热门分类的文章)
public boolean isHotClass(Article article){
return Stream.of(57,102)
.anyMatch(classId -> classId.equals(article.getClassId()));
}
//更新分类,但未持久化,这里不能依赖Dao去操作实体化
public Article changeClass(Article article, ArticleClass ac){
return article.setClassId(ac.getId());
}
}

所有业务逻辑都在 Domain 中,事务管理也在 Item 中实现。这样做的优缺点如下。优点:* 更加符合 OO 的原则;

  • Service 层很薄,只充当 Facade 的角色,不和 DAO 打交道。

缺点:* DAO 和 Domain Object 形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。

  • 如何划分 Service 层逻辑和 Domain 层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能 导致整个结构的混乱无序。

胀血模型

基于充血模型的第三个缺点,有同学提出,干脆取消 Service 层,只剩下 Domain Object 和 DAO 两层,在 Domain Object 的 Domain Logic 上面封装事务。

Domain Object (事务封装,业务逻辑) <—> DAO

似乎 Ruby on rails 就是这种模型,它甚至把 Domain Object 和 DAO 都合并了。这样做的优缺点:* 简化了分层

  • 也算符合 OO

该模型缺点:* 很多不是 Domain Logic 的 Service 逻辑也被强行放入 Domain Object ,引起了 Domain Object 模型的不稳定;

  • Domain Object 暴露给 Web 层过多的信息,可能引起意想不到的副作用。

运用 DDD 改造现有旧系统实践

假如你是一个团队 Leader 或者架构师,当你接手一个旧系统维护及重构的任务时,你该如何改造呢?是否觉得哪里都不对但由于业务认知的不熟悉而无从下手呢?其实这里我可以教你一套方法来应对这种窘境。你要做的大概以下几点:1. 通过公共平台大概梳理出系统之间的调用关系(一般中等以上公司都具备 RPC 和 HTTP 调用关系,无脑的挨个系统查询即可),画出来的可能会很乱,也可能会比较清晰,但这就是现状。
2. 分配组员每个人认领几个项目,来梳理项目维度关系,这些关系包括:对外接口、交互、用例、MQ 等的详细说明。个别核心系统可以画出内部实体或者聚合根。
3. 小组开会,挨个 review 每个系统的业务概念,达到组内统一语言。
4. 根据以上资料,即可看出哪些不合理的调用关系(比如循环调用、不规范的调用等),甚至不合理的分层。
5. 根据主线业务自顶向下细分领域,以及限界上下文。此过程可能会颠覆之前的系统划分。6. 根据业务复杂性,指定领域模型,选择贫血或者充血模型。团队内部最好实行统一习惯,以免出现交接成本过大。7. 分工进行开发,并设置 deadline,注意,不要单一的设置一个 deadline,要设置中间 check 时间,比如 dealline 是 1 月 20 日,还要设置两个 check 时间,分别沟通代码风格及边界职责,以免 deadline 时延期。### DDD 与 Spring 家族的完美结合

还用前面提到的文章管理系统,我为你说明一下 DDD 开发的关注点。### 模块(Module)

模块(Module)是 DDD 中明确提到的一种控制限界上下文的手段,在我们的工程中,一般尽量用一个模块来表示一个领域的限界上下文。如代码中所示,一般的工程中包的组织方式为 {com.公司名.组织架构.业务.上下文.*},这样的组织结构能够明确地将一个上下文限定在包的内部。

1
2
3
复制代码import com.company.team.bussiness.counter.*;//计数上下文
import com.company.team.bussiness.category.*;//分类上下文
import com.company.team.bussiness.comment.*;//评论上下文

对于模块内的组织结构,一般情况下我们是按照领域对象、领域服务、领域资源库、防腐层等组织方式定义的。

1
2
3
4
5
6
复制代码import com.company.team.bussiness.cms.domain.valobj.*;//领域对象-值对象
import com.company.team.bussiness.cms.domain.entity.*;//领域对象-实体
import com.company.team.bussiness.cms.domain.aggregate.*;//领域对象-聚合根
import com.company.team.bussiness.cms.service.*;//领域服务
import com.company.team.bussiness.cms.repo.*;//领域资源库
import com.company.team.bussiness.cms.facade.*;//领域防腐层

领域对象

领域驱动要解决的一个重要的问题,就是解决对象的贫血问题,而领域对象则最直接的反应了这个能力。我们可以定义聚合根(文章)和值对象(计数器),来举例说明。聚合根持有文章的 id 和文章的计数数据,这里计数器之所以被列为值对象,而非实体的一个属性,是因为计数器是由多部分组成的,比如真实阅读量、推广阅读量等。在文章领域对象中,我们需要定义个一个方法,来获取文章的计数量,用于页面上显示,这个逻辑可能会很复杂,涉及到爆文、专栏作者级别、发布时间等因素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码package com.company.team.bussiness.domain.aggregate;
import ...;

public class Article {

@Resource
private CategoryRepository categoryRepository;

private int articleId; //文章id
...
private ArticleCount articleCount; //文章计数器
//getter & setter

//查询计数显示数量,这里简化一些逻辑,甚至是不符合实际业务场景,这不重要,这里只为直观表达意思
public Integer getShowArticleCount() {
if(this.articleCount == null){
return 0;
}
return this.articleCount.realCount + categoryRepository.getCategoryWeight(this.category) + (this.articleCount.adCount * DayUtils.calDaysByNow(this.articleCount.deadDays));
}
}

与以往的仅有 getter、setter 的业务对象不同,领域对象具有了行为,对象更加丰满。同时,比起将这些逻辑写在服务内(例如 Service),领域功能的内聚性更强,职责更加明确。### 资源库

领域对象需要资源存储,资源库可以理解成 DAO,但它比 DAO 更宽泛,存储的手段可以是多样化的,常见的无非是数据库、分布式缓存、本地缓存等。资源库(Repository)的作用,就是对领域的存储和访问进行统一管理的对象。在系统中,我们是通过如下的方式组织资源库的。

1
2
3
4
5
复制代码import com.company.team.bussiness.repo.dao.ArticleDao;//数据库访问对象-文章
import com.company.team.bussiness.repo.dao.CommentDao;//数据库访问对象-评论
import com.company.team.bussiness.repo.dao.po.ArticlePO;//数据库持久化对象-文章
import com.company.team.bussiness.repo.dao.po.CommentPO;//数据库持久化对象-评论
import com.company.team.bussiness.repo.cache.ArticleObj;//分布式缓存访问对象-文章缓存访问

资源库对外的整体访问由 Repository 提供,它聚合了各个资源库的数据信息,同时也承担了资源存储的逻辑(例如缓存更新机制等)。在资源库中,我们屏蔽了对底层奖池和奖品的直接访问,而是仅对文章的聚合根进行资源管理。代码示例中展示了资源获取的方法(最常见的 Cache Aside Pattern)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package com.company.team.bussiness.repo;
import ...;

@Repository
public class ArticleRepository {
@Autowired
private ArticleDao articleDao;
@AutoWired
private articleDaoCacheAccessObj articleCacheAccessObj;

public Article getArticleById(int articleId) {
Article article = articleCacheAccessObj.get(articleId);
if(article!=null){
return article;
}
article = getArticleFromDB(articleId);
articleCacheAccessObj.add(articleId, article);
return article;
}

private Article getArticleFromDB(int articleId) {...}
}

比起以往将资源管理放在服务中的做法,由资源库对资源进行管理,职责更加明确,代码的可读性和可维护性也更强。### 防腐层

亦称适配层。在一个上下文中,有时需要对外部上下文进行访问,通常会引入防腐层的概念来对外部上下文的访问进行一次转义。有以下几种情况会考虑引入防腐层:* 需要将外部上下文中的模型翻译成本上下文理解的模型。

  • 不同上下文之间的团队协作关系,如果是供奉者关系,建议引入防腐层,避免外部上下文变化对本上下文的侵蚀。
  • 该访问本上下文使用广泛,为了避免改动影响范围过大。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码package com.company.team.bussiness.facade;
import ...;

@Component
public class ArticleFacade {

@Resource
private ArticleService articleService;

public Article getArticle(ArticleContext context) {
ArticleResponse resp = articleService.getArticle(context.getArticleId());
return buildArticle(resp);
}

private Article buildArticle(ArticleResponse resp) {...}
}

如果内部多个上下文对外部上下文需要访问,那么可以考虑将其放到通用上下文中。### 领域服务

上文中,我们将领域行为封装到领域对象中,将资源管理行为封装到资源库中,将外部上下文的交互行为封装到防腐层中。此时,我们再回过头来看领域服务时,能够发现领域服务本身所承载的职责也就更加清晰了,即就是通过串联领域对象、资源库和防腐层等一系列领域内的对象的行为,对其他上下文提供交互的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码package com.company.team.bussiness.service.impl
import ...;
@Service
public class CommentServiceImpl implements CommentService {
@Resource
private CommentFacade commentFacade;
@Resource
private ArticleRepository articleRepo;
@Resource
private ArticleService articleService;

@Override
public CommentResponse commentArticle(CommentContext commentContext) {
Article article = articleRepo.getArticleById(commentContext.getArticleId());//获取文章聚合根
commentFacade.doComment(commentContext);//增加计数信息
return buildCommentResponse(commentContext,article);//组装评论后的文章信息
}

private CommentResponse buildCommentResponse(CommentContext commentContext, Article article) {...}
}

可以看到在省略了一些防御性逻辑(异常处理、空值判断等)后,领域服务的逻辑已经足够清晰明了。### 示范包结构

反思思考
DDD 将领域层进行了细分,是 DDD 比较 MVC 框架的最大亮点。DDD 能做到这一点,主要是因为 DDD 将领域层进行了细分,比如说领域对象有实体、聚合,动作和操作叫做领域服务,能力叫做领域能力等,而 MVC 架构并没有对业务元素进行细分,所有的业务都是 Service,从而导致 Controller 层和 Service 层很难定义出技术约束,因为都是 Service,你不会知道这个 Service 是用来描述对象的还是来描述一个业务操作的。针对未来业务扩展方面,聚合根升级为上下文,甚至拆分成微服务,也是应对复杂问题的重要手段。实体和值对象是对现有编程习惯最大的变化,但不要过度关注而忽略了领域对象之间的关系。DDD 本身是方法论,是提供理论指导的,所以不要奢求像 Spring 那样给你一个 Demo 照着写,希望读者看完后多多反思。( 公众号:架构精进 )

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

LRU算法及其优化策略——算法篇

发表于 2020-01-16

LRU算法全称是最近最少使用算法(Least Recently Use),广泛的应用于缓存机制中。当缓存使用的空间达到上限后,就需要从已有的数据中淘汰一部分以维持缓存的可用性,而淘汰数据的选择就是通过LRU算法完成的。

LRU算法的基本思想是基于局部性原理的时间局部性:

如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。

所以顾名思义,LRU算法会选出最近最少使用的数据进行淘汰。

原理

一般来讲,LRU将访问数据的顺序或时间和数据本身维护在一个容器当中。当访问一个数据时:

  1. 该数据不在容器当中,则设置该数据的优先级为最高并放入容器中。
  2. 该数据在容器当中,则更新该数据的优先级至最高。

当数据的总量达到上限后,则移除容器中优先级最低的数据。下图是一个简单的LRU原理示意图:

LRU原理示意图.jpg

如果我们按照7 0 1 2 0 3 0 4的顺序来访问数据,且数据的总量上限为3,则如上图所示,LRU算法会依次淘汰7 1 2这三个数据。

朴素的LRU算法

那么我们现在就按照上面的原理,实现一个朴素的LRU算法。下面有三种方案:

  1. 基于数组

方案:为每一个数据附加一个额外的属性——时间戳,当每一次访问数据时,更新该数据的时间戳至当前时间。当数据空间已满后,则扫描整个数组,淘汰时间戳最小的数据。

不足:维护时间戳需要耗费额外的空间,淘汰数据时需要扫描整个数组。
2. 基于长度有限的双向链表

方案:访问一个数据时,当数据不在链表中,则将数据插入至链表头部,如果在链表中,则将该数据移至链表头部。当数据空间已满后,则淘汰链表最末尾的数据。

不足:插入数据或取数据时,需要扫描整个链表。
3. 基于双向链表和哈希表

方案:为了改进上面需要扫描链表的缺陷,配合哈希表,将数据和链表中的节点形成映射,将插入操作和读取操作的时间复杂度从O(N)降至O(1)

基于双向链表 + 哈希表实现LRU

下面我们就基于双向链表和哈希表实现一个LRU算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
复制代码public class LRUCache {
private int size; // 当前容量
private int capacity; // 限制大小
private Map<Integer, DoubleQueueNode> map; // 数据和链表中节点的映射
private DoubleQueueNode head; // 头结点 避免null检查
private DoubleQueueNode tail; // 尾结点 避免null检查

public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>(capacity);
this.head = new DoubleQueueNode(0, 0);
this.tail = new DoubleQueueNode(0, 0);
this.head.next = tail;
}

public Integer get(Integer key) {

DoubleQueueNode node = map.get(key);
if (node == null) {
return null;
}

// 数据在链表中,则移至链表头部
moveToHead(node);

return node.val;
}

public Integer put(Integer key, Integer value) {

Integer oldValue;
DoubleQueueNode node = map.get(key);
if (node == null) {
// 淘汰数据
eliminate();
// 数据不在链表中,插入数据至头部
DoubleQueueNode newNode = new DoubleQueueNode(key, value);
DoubleQueueNode temp = head.next;
head.next = newNode;
newNode.next = temp;
newNode.pre = head;
temp.pre = newNode;
map.put(key, newNode);
size++;
oldValue = null;
} else {
// 数据在链表中,则移至链表头部
moveToHead(node);
oldValue = node.val;
node.val = value;
}
return oldValue;
}

public Integer remove(Integer key) {
DoubleQueueNode deletedNode = map.get(key);
if (deletedNode == null) {
return null;
}
deletedNode.pre.next = deletedNode.next;
deletedNode.next.pre = deletedNode.pre;
map.remove(key);
return deletedNode.val;
}

// 将节点插入至头部节点
private void moveToHead(DoubleQueueNode node) {
node.pre.next = node.next;
node.next.pre = node.pre;
DoubleQueueNode temp = head.next;
head.next = node;
node.next = temp;
node.pre = head;
temp.pre = node;
}

private void eliminate() {
if (size < capacity) {
return;
}

// 将链表中最后一个节点去除
DoubleQueueNode last = tail.pre;
map.remove(last.key);
last.pre.next = tail;
tail.pre = last.pre;
size--;
last = null;
}
}

// 双向链表节点
class DoubleQueueNode {
int key;
int val;
DoubleQueueNode pre;
DoubleQueueNode next;
public DoubleQueueNode(int key, int val) {
this.key = key;
this.val = val;
}
}

基本上就是把上述LRU算法思路用代码实现了一遍,比较简单,只需要注意一下pre和next两个指针的指向和同步更新哈希表,put()和get()操作的时间复杂度都是O(1),空间复杂度为O(N)。

基于LinkedHashMap实现的LRU

其实我们可以直接根据JDK给我们提供的LinkedHashMap直接实现LRU。因为LinkedHashMap的底层即为双向链表和哈希表的组合,所以可以直接拿来使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码public class LRUCache extends LinkedHashMap {

private int capacity;

public LRUCache(int capacity) {
// 注意这里将LinkedHashMap的accessOrder设为true
super(16, 0.75f, true);
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return super.size() >= capacity;
}
}

默认LinkedHashMap并不会淘汰数据,所以我们重写了它的removeEldestEntry()方法,当数据数量达到预设上限后,淘汰数据,accessOrder设为true意为按照访问的顺序排序。整个实现的代码量并不大,主要都是应用LinkedHashMap的特性。

正因为LinkedHashMap这么好用,所以我们可以看到Dubbo的LRU缓存LRUCache也是基于它实现的。

LRU算法优化

朴素的LRU算法已经能够满足缓存的要求了,但是还是有一些不足。当热点数据较多时,有较高的命中率,但是如果有偶发性的批量操作,会使得热点数据被非热点数据挤出容器,使得缓存受到了“污染”。所以为了消除这种影响,又衍生出了下面这些优化方法。

LRU-K

LRU-K算法是对LRU算法的改进,将原先进入缓存队列的评判标准从访问一次改为访问K次,可以说朴素的LRU算法为LRU-1。

LRU-K算法有两个队列,一个是缓存队列,一个是数据访问历史队列。当访问一个数据时,首先先在访问历史队列中累加访问次数,当历史访问记录超过K次后,才将数据缓存至缓存队列,从而避免缓存队列被污染。同时访问历史队列中的数据可以按照LRU的规则进行淘汰。具体如下图所示:

LRU-K.png

下面我们来实现一个LRU-K缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
复制代码// 直接继承我们前面写好的LRUCache
public class LRUKCache extends LRUCache {

private int k; // 进入缓存队列的评判标准
private LRUCache historyList; // 访问数据历史记录

public LRUKCache(int cacheSize, int historyCapacity, int k) {
super(cacheSize);
this.k = k;
this.historyList = new LRUCache(historyCapacity);
}

@Override
public Integer get(Integer key) {

// 记录数据访问次数
Integer historyCount = historyList.get(key);
historyCount = historyCount == null ? 0 : historyCount;
historyList.put(key, ++historyCount);

return super.get(key);
}

@Override
public Integer put(Integer key, Integer value) {

if (value == null) {
return null;
}

// 如果已经在缓存里则直接返回缓存中的数据
if (super.get(key) != null) {
return super.put(key, value);;
}

// 如果数据历史访问次数达到上限,则加入缓存
Integer historyCount = historyList.get(key);
historyCount = historyCount == null ? 0 : historyCount;
if (historyCount >= k) {
// 移除历史访问记录
historyList.remove(key);
return super.put(key, value);
}
}
}

上面只是个简单的模型,并没有加上必要的并发控制。

一般来讲,当K的值越大,则缓存的命中率越高,但是也会使得缓存难以被淘汰。综合来说,使用LRU-2的性能最优。

Two Queue

Two Queue可以说是LRU-2的一种变种,将数据访问历史改为FIFO队列。好处的明显的,FIFO更简易,耗用资源更少,但是相比LRU-2会降低缓存命中率。

Two Queue.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
复制代码// 直接继承LinkedHashMap
public class TwoQueueCache extends LinkedHashMap<Integer, Integer> {

private int k; // 进入缓存队列的评判标准
private int historyCapacity; // 访问数据历史记录最大大小
private LRUCache lruCache; // 我们前面写好的LRUCache

public TwoQueueCache(int cacheSize, int historyCapacity, int k) {
// 注意这里设置LinkedHashMap的accessOrder为false
super();
this.historyCapacity = historyCapacity;
this.k = k;
this.lruCache = new LRUCache(cacheSize);
}

public Integer get(Integer key) {
// 记录数据访问记录
Integer historyCount = super.get(key);
historyCount = historyCount == null ? 0 : historyCount;
super.put(key, historyCount);
return lruCache.get(key);
}

public Integer put(Integer key, Integer value) {

if (value == null) {
return null;
}

// 如果已经在缓存里则直接返回缓存中的数据
if (lruCache.get(key) != null) {
return lruCache.put(key, value);
}

// 如果数据历史访问次数达到上限,则加入缓存
Integer historyCount = super.get(key);
historyCount = historyCount == null ? 0 : historyCount;
if (historyCount >= k) {
// 移除历史访问记录
super.remove(key);
return lruCache.put(key, value);
}

return null;
}

@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return super.size() >= historyCapacity;
}
}

这里直接继承LinkedHashMap,并且accessOrder默认为false,意为按照插入顺序进行排序,二者结合即为一个FIFO的队列。通过重写removeEldestEntry()方法来自动淘汰最早插入的数据。

Multi Queue

相比于上面两种优化,Multi Queue的实现则复杂的多,顾名思义,Multi Queue是由多个LRU队列组成的。每一个LRU队列都有一个相应的优先级,数据会根据访问次数计算出相应的优先级,并放在该队列中。

Multi Queue.png

  • 数据插入和访问:当数据首次插入时,会放入到优先级最低的Q0队列。当再次访问时,根据LRU的规则,会移至队列头部。当根据访问次数计算的优先级提升后,会将该数据移至更高优先级的队列的头部,并删除原队列的该数据。同样的,当该数据的优先级降低时,会移至低优先级的队列中。
  • 数据淘汰:数据淘汰总是从最低优先级的队列的末尾数据进行,并将它加入到Q-history队列的头部。如果数据在Q-history数据中被访问,则重新计算该数据的优先级,并将它加入到相应优先级的队列中。否则就是按照LRU算法完全淘汰。

Multi Queue也可以看做是LRU-K的变种,将原来两个队列扩展为多个队列,好处就是无论是加入缓存还是淘汰缓存数据都变得更加细腻,但是会带来额外开销。

总结

本文讲解了基本的LRU算法及它的几种优化策略,并比较了他们之间的异同和优劣。以前没有想到LRU还有这么些门道,后续还会有Redis、Mysql对于LRU算法应用的文章。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习java8这篇文章就够了 目录 基础 Java8-进阶

发表于 2020-01-16

目录

本文分3部分

java8-基础

主要讲述java8的一些基础概念及用法。包括:Optional类,Lambda表达式,Stream接口。

java8-进阶

主要讲述java8的一些进阶用法。包括:Function接口,自定义Stream接口。

java8-实践

主要讲述java8的用法的一些比较特别的实践用法。

点一点链接,支持一波吃饭,http://aliyun.guan2ye.com/

基础

(一)optional类

  • 创建一个空Optional对象

输出的是一个空的optional对象

1
2
3
复制代码Optional<String> optional = Optional.empty();
System.out.println(optional);
##:Optional.empty
  • 创建一个非空Optional对象

如果person是null,将会立即抛出,而不是访问person的属性时获得一个潜在的错误

1
2
3
4
5
6
7
8
复制代码Person person = new Person("xu","hua");
Optional<Person> optional2 = Optional.of(person);
System.out.println(optional2);
System.out.println(optional2.get());
System.out.println(optional2.get().firstName);
##:Optional[xuhua]
xuhua
xu
  • 判断对象是否存在
1
2
3
4
复制代码System.out.println(optional.isPresent());
System.out.println(optional2.isPresent());
##:false
true
  • 如果Optional为空返回默认值
1
2
3
4
复制代码System.out.println(optional.orElse("fallback"));
optional.ifPresent(System.out::println);
##:fallback
xuhua

(二)Lambda表达式

  • Lambda表达式的使用

java8以前的字符串排列,创建一个匿名的比较器对象Comparator然后将其传递给sort方法

1
2
3
4
5
6
7
复制代码List<String> names= Arrays.asList("peter", "anna", "mike", "xenia");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});

java8使用lambda表达式就不需要匿名对象了

1
复制代码Collections.sort(names,(String a,String b)->{return b.compareTo(a);});

简化一下:对于函数体只有一行代码的,你可以去掉大括号{}以及return关键字

1
复制代码Collections.sort(names,(String a,String b)->b.compareTo(a));

Java编译器可以自动推导出参数类型,所以你可以不用再写一次类型

1
复制代码Collections.sort(names, (a, b) -> b.compareTo(a));
1
复制代码##:[xenia, peter, mike, anna]

对于null的处理

1
2
3
4
复制代码List<String> names2 = Arrays.asList("peter", null, "anna", "mike", "xenia");
names2.sort(Comparator.nullsLast(String::compareTo));
System.out.println(names2);
##:[anna, mike, peter, xenia, null]
  • 函数式接口,方法,构造器

每一个lambda表达式都对应一个类型,通常是接口类型。

而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的lambda表达式都会被匹配到这个抽象方法。

因为默认方法不算抽象方法,所以你也可以给你的函数式接口添加默认方法。

我们可以将lambda表达式当作任意只包含一个抽象方法的接口类型,确保你的接口一定达到这个要求,

你只需要给你的接口添加 @FunctionalInterface 注解,

编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。

+ 函数式接口



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码@FunctionalInterface
public static interface Converter<F, T> {
T convert(F from);
}
/**原始的接口实现*/
Converter<String, Integer> integerConverter1 = new Converter<String, Integer>() {
@Override
public Integer convert(String from) {
return Integer.valueOf(from);
}
};

/**使用lambda表达式实现接口*/
Converter<String, Integer> integerConverter2 = (from) -> Integer.valueOf(from);

Integer converted1 = integerConverter1.convert("123");
Integer converted2 = integerConverter2.convert("123");
System.out.println(converted1);
System.out.println(converted2);
##:123
123
/**简写的lambda表达式*/
Converter<String, Integer> integerConverter3 = Integer::valueOf;
Integer converted3 = integerConverter3.convert("123");
System.out.println(converted3);
##:123
+ 函数式方法
1
2
3
4
5
6
7
8
9
10
复制代码static class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
Something something = new Something();
Converter<String, String> stringConverter = something::startsWith;
String converted4 = stringConverter.convert("Java");
System.out.println(converted4);
##:j
+ 函数式构造器 Java编译器会自动根据PersonFactory.create方法的签名来选择合适的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class Person {
public String firstName;
public String lastName;

public Person() {
}

public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public String toString(){
return firstName+lastName;
}
}
1
2
3
4
5
6
7
复制代码interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("xu", "hua");
System.out.println(person.toString());
##:xuhua
  • Lambda作用域
    点一点链接,支持一波吃饭,http://aliyun.guan2ye.com/

在lambda表达式中访问外层作用域和老版本的匿名对象中的方式很相似。

你可以直接访问标记了final的外层局部变量,或者实例的字段以及静态变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码static int outerStaticNum = 10;

int outerNum;

void testScopes() {

/**变量num可以不用声明为final*/
int num = 1;

/**可以直接在lambda表达式中访问外层的局部变量*/
Lambda2.Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + outerStaticNum+num);
String convert = stringConverter.convert(2);
System.out.println(convert);
##:13
/**但是num必须不可被后面的代码修改(即隐性的具有final的语义),否则编译出错*/
//num=3;

/**lambda内部对于实例的字段以及静态变量是即可读又可写*/
Lambda2.Converter<Integer, String> stringConverter2 = (from) -> {
outerNum = 13;
return String.valueOf(from + outerNum);
};
System.out.println(stringConverter2.convert(2));
System.out.println("\nBefore:outerNum-->" + outerNum);
outerNum = 15;
System.out.println("After:outerNum-->" + outerNum);
##:Before:outerNum-->13
After:outerNum-->15

String[] array = new String[1];
Lambda2.Converter<Integer, String> stringConverter3 = (from) -> {
array[0] = "Hi here";
return String.valueOf(from);
};
stringConverter3.convert(23);
System.out.println("\nBefore:array[0]-->" + array[0]);
array[0] = "Hi there";
System.out.println("After:array[0]-->" + array[0]);
##:Before:array[0]-->Hi here
After:array[0]-->Hi there
}

(三)Stream类

点一点链接,支持一波吃饭,http://aliyun.guan2ye.com/

java.util.Stream 表示能应用在一组元素上一次执行的操作序列。

Stream 操作分为中间操作或者最终操作两种,最终操作返回一特定类型的计算结果,而中间操作返回Stream本身,这样你就可以将多个操作依次串起来。

Stream 的创建需要指定一个数据源,比如 java.util.Collection的子类,List或者Set,Map不支持。

Stream的操作可以串行执行或者并行执行。

  • Stream的基本接口
1
2
3
4
5
6
7
8
9
复制代码List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");
+ Filter 过滤.


`Filter`通过一个`predicate`接口来过滤并只保留符合条件的元素,该操作属于中间操作,所以我们可以在过滤后的结果来应用其他`Stream`操作(比如`forEach`)。


`forEach`需要一个函数来对过滤后的元素依次执行。


`forEac`h是一个最终操作,所以我们不能在`forEach`之后来执行其他`Stream`操作。



1
2
3
4
复制代码stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
+ Sorted 排序. `Sorted`是一个中间操作,返回的是排序好后的`Stream`。 如果你不指定一个自定义的`Comparator`则会使用默认排序.
1
2
3
4
5
复制代码stringCollection
.stream()
.sorted()
.forEach(System.out::println);
System.out.println(stringCollection);//原数据源不会被改变
+ Map. 中间操作`ma`p会将元素根据指定的`Function`接口来依次将元素转成另外的对象.
1
2
3
4
5
6
复制代码stringCollection
.stream()
.map(String::toUpperCase)
.map((s)->s+" space")
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);
+ Match `Stream`提供了多种匹配操作,允许检测指定的`Predicate`是否匹配整个`Stream`。 所有的匹配操作都是最终操作,并返回一个boolean类型的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码boolean anyStartsWithA = stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));
System.out.println(anyStartsWithA); // true

boolean allStartsWithA = stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));
System.out.println(allStartsWithA); // false

boolean noneStartsWithZ = stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));
System.out.println(noneStartsWithZ); // true
+ Count 计数是一个最终操作,返回`Stream`中元素的个数,返回值类型是`long`。
1
2
3
4
5
复制代码long startsWithB = stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();
System.out.println(startsWithB); // 3
+ Reduce `Reduce`是一个最终操作,允许通过指定的函数来讲`stream`中的多个元素规约为一个元素,规约后的结果是通过`Optional`接口表示的。
1
2
3
4
5
6
7
8
复制代码Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
##:aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2
  • 并行stream和串行stream
+ 串行stream



1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码List<String> values = new ArrayList<>(MAX);
for (int i = 0; i < MAX; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}

long t0 = System.nanoTime();
long count = values.stream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));
+ 并行stream 并行`stream`是在运行时将数据划分成了多个块,然后将数据块分配给合适的处理器去处理。 只有当所有块都处理完成了,才会执行之后的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码List<String> values = new ArrayList<>(MAX);
for (int i = 0; i < MAX; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}

long t0 = System.nanoTime();
long count = values.parallelStream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));
时间结果比较:
1
2
3
4
复制代码1000000
sequential sort took: 717 ms
1000000
parallel sort took: 303 ms
  • IntStream接口

IntStream接口是stream的一种,继承了BaseStream接口。

+ range
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码IntStream.range(0, 10)
.forEach(i -> {
if (i % 2 == 1) System.out.print(i+" ");
});
##:1 3 5 7 9

OptionalInt reduced1 =
IntStream.range(0, 10)
.reduce((a, b) -> a + b);
System.out.println(reduced1.getAsInt());

int reduced2 =
IntStream.range(0, 10)
.reduce(7, (a, b) -> a + b);
System.out.println(reduced2);
##:45
52
+ sum
1
复制代码System.out.println(IntStream.range(0, 10).sum());
  • Stream的应用
1
2
3
4
5
6
7
8
9
10
11
12
复制代码Map<String, Integer> unsortMap = new HashMap<>();
unsortMap.put("z", 10);
unsortMap.put("b", 5);
unsortMap.put("a", 6);
unsortMap.put("c", 20);
unsortMap.put("d", 1);
unsortMap.put("e", 7);
unsortMap.put("y", 8);
unsortMap.put("n", 99);
unsortMap.put("j", 50);
unsortMap.put("m", 2);
unsortMap.put("f", 9);

使用stream类来对map的value排序

1
2
3
4
5
6
7
8
9
复制代码public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
Map<K, V> result = new LinkedHashMap<>();
map.entrySet().parallelStream()
.sorted((o1, o2) -> (o2.getValue()).compareTo(o1.getValue()))
.forEachOrdered(x -> result.put(x.getKey(), x.getValue()));
return result;
}
System.out.println(sortByValue(unsortMap));
##:{n=99, j=50, c=20, z=10, f=9, y=8, e=7, a=6, b=5, m=2, d=1}
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码List<Object> list = new ArrayList<>();
JSONObject data1 = new JSONObject();
data1.put("type", "支出");
data1.put("money", 500);
JSONObject data2 = new JSONObject();
data2.put("type", "收入");
data2.put("money", 1000);
JSONObject data3 = new JSONObject();
data3.put("type", "借贷");
data3.put("money", 100);
list.add(data1);
list.add(data2);
list.add(data3);

使用stream类来处理list``里面的json`数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
复制代码    /**
* 按money的值来排列json
*/
list.stream()
.filter(x -> JSONObject.fromObject(x).containsKey("money"))
.sorted((b, a) -> Integer.valueOf(JSONObject.fromObject(a).getInt("money")).compareTo(JSONObject.fromObject(b)
.getInt("money")))
.forEach(System.out::println);
/**
* 找到最小的money
*/
Integer min = list.stream()
.filter(x -> JSONObject.fromObject(x).containsKey("money"))
.map(x -> JSONObject.fromObject(x).getInt("money"))
.sorted()
.findFirst()
.get();
System.out.println(min);
/**
* 计算type的数目
*/
Map<String, Integer> type_count = new HashMap<>();
list.stream()
.filter(x -> JSONObject.fromObject(x).containsKey("type"))
.map(x -> JSONObject.fromObject(x).getString("type"))
.forEach(x -> {
if (type_count.containsKey(x)) type_count.put(x, type_count.get(x) + 1);
else type_count.put(x, 1);
});
System.out.println(type_count.toString());
##:
{"type":"收入","money":1000}
{"type":"支出","money":500}
{"type":"借贷","money":100}
100
{借贷=1, 收入=1, 支出=1}

Java8-进阶

点一点链接,支持一波吃饭,http://aliyun.guan2ye.com/

函数式接口

只包含一个抽象方法的接口

  • Function<T, R>

接受一个输入参数,返回一个结果。

Function接口包含以下方法:

定义两个比较简单的函数:times2 和 squared

1
2
复制代码Function<Integer, Integer> times2 = e -> e * 2;
Function<Integer, Integer> squared = e -> e * e;
+ `R apply(T t)`


执行函数



1
2
3
复制代码//return 8 - > 4 * 2
Integer a = times2.apply(4);
System.out.println(a);
+ `compose` 先执行参数里面的操作,然后执行调用者
1
2
3
复制代码//return 32 - > 4 ^ 2 * 2
Integer b = times2.compose(squared).apply(4);
System.out.println(b);
+ `andThen` 先执行调用者操作,再执行参数操作
1
2
3
复制代码//return 64 - > (4 * 2) ^ 2
Integer c = times2.andThen(squared).apply(4);
System.out.println(c);
+ `identity` 总是返回传入的参数本身
1
2
3
复制代码//return 4
Integer d = identity.apply(4);
System.out.println(d);
  • BiFunction<T, U, R>

接受输入两个参数,返回一个结果

+ `R apply(T t, U u)`



1
2
3
复制代码BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
//return 30
System.out.println(add.apply(10,20));
  • Supplier<T>

无参数,返回一个结果。

+ `T get()`



1
2
3
复制代码Supplier<Integer> get= () -> 10;
//return 10
Integer a=get.get();
  • Consumer<T>

代表了接受一个输入参数并且无返回的操作

+ `void accept(T t)`



1
2
复制代码//return void
Consumer<Integer> accept=x->{};
  • BiConsumer<T, U>

代表了一个接受两个输入参数的操作,并且不返回任何结果

+ `void accept(T t, U u)`



1
2
复制代码//return void
BiConsumer<Integer,Integer> accept=(x,y)->{};
  • BinaryOperator<T> extends BiFunction<T,T,T>

一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果

定义了两个静态方法:minBy,maxBy

  • Predicate<T>

接受一个输入参数,返回一个布尔值结果。

+ `test`



1
2
3
复制代码Predicate<String> predicate=x->x.startsWith("a");
//return ture
System.out.println(predicate.test("abc"));

Stream接口

  • Collector接口

Collector是Stream的可变减少操作接口

Collector<T, A, R>接受三个泛型参数,对可变减少操作的数据类型作相应限制:

T:输入元素类型

A:缩减操作的可变累积类型(通常隐藏为实现细节)

R:可变减少操作的结果类型

Collector接口声明了4个函数,这四个函数一起协调执行以将元素目累积到可变结果容器中,并且可以选择地对结果进行最终的变换:

+ `Supplier<A> supplier()`: 创建新的结果结
+ `BiConsumer<A, T> accumulator()`: 将元素添加到结果容器
+ `BinaryOperator<A> combiner()`: 将两个结果容器合并为一个结果容器
+ `Function<A, R> finisher()`: 对结果容器作相应的变换在Collector接口的characteristics方法内,可以对Collector声明相关约束:


+ `Set<Characteristics> characteristics()`:


Characteristics是Collector内的一个枚举类,声明了CONCURRENT、UNORDERED、IDENTITY\_FINISH等三个属性,用来约束Collector的属性:


    - CONCURRENT:表示此收集器支持并发,意味着允许在多个线程中,累加器可以调用结果容器
    - UNORDERED:表示收集器并不按照Stream中的元素输入顺序执行
    - IDENTITY\_FINISH:表示finisher实现的是识别功能,可忽略。
> 如果一个容器仅声明CONCURRENT属性,而不是UNORDERED属性,那么该容器仅仅支持无序的Stream在多线程中执行。

定义自己的Stream

  • collect

collect有两个接口:

1
2
3
4
复制代码<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
<R, A> R collect(Collector<? super T, A, R> collector);
+ `<1> <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)`



> Supplier supplier是一个工厂函数,用来生成一个新的容器;



> BiConsumer accumulator也是一个函数,用来把Stream中的元素添加到结果容器中;



> BiConsumer combiner还是一个函数,用来把中间状态的多个结果容器合并成为一个(并发的时候会用到)



1
2
3
4
5
6
7
8
复制代码Supplier<List<String>> supplier = ArrayList::new;
BiConsumer<List<String>, String> accumulator = List::add;
BiConsumer<List<String>, List<String>> combiner = List::addAll;

//return [aaa1, aaa1],实现了Collectors.toCollection
List<String> list1 = stringCollection.stream()
.filter(x -> x.startsWith("a"))
.collect(supplier, accumulator, combiner);
+ `<2> <R, A> R collect(Collector<? super T, A, R> collector)` `Collectors`是Java已经提供好的一些工具方法:
1
2
3
4
5
复制代码List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa1");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
转换成其他集合: - `toList`
1
2
3
复制代码//return [aaa1, aaa1]
stringCollection.stream()
.filter(x -> x.startsWith("a")).collect(Collectors.toList())
- `toSet`
1
2
3
复制代码//return [aaa1]
stringCollection.stream()
.filter(x -> x.startsWith("a")).collect(Collectors.toSet())
- `toCollection` 接口:
1
复制代码public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)
实现:
1
2
3
4
复制代码//return [aaa1, aaa1]
List<String> list = stringCollection.stream()
.filter(x -> x.startsWith("a"))
.collect(Collectors.toCollection(ArrayList::new));
- `toMap` 接口:
1
2
3
复制代码public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper)
实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码//return {aaa1=aaa1_xu}
Function<String, String> xu = x -> x + "_xu";
Map<String, String> map = stringCollection.stream()
.filter(x -> x.startsWith("a"))
.distinct()
.collect(Collectors.toMap(Function.identity(), xu));

```转成值:


- `averagingDouble`:求平均值,Stream的元素类型为double
- `averagingInt`:求平均值,Stream的元素类型为int
- `averagingLong`:求平均值,Stream的元素类型为long
- `counting`:Stream的元素个数
- `maxBy`:在指定条件下的,Stream的最大元素
- `minBy`:在指定条件下的,Stream的最小元素
- `reducing`: reduce操作
- `summarizingDouble`:统计Stream的数据(double)状态,其中包括count,min,max,sum和平均。
- `summarizingInt`:统计Stream的数据(int)状态,其中包括count,min,max,sum和平均。
- `summarizingLong`:统计Stream的数据(long)状态,其中包括count,min,max,sum和平均。
- `summingDouble`:求和,Stream的元素类型为double
- `summingInt`:求和,Stream的元素类型为int
- `summingLong`:求和,Stream的元素类型为long数据分区:


- `partitioningBy`


接口:
复制代码public static <T> Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) public static <T, D, A> Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)
1
实现:
复制代码Predicate<String> startA = x -> x.startsWith("a"); //return {false=[ddd2, bbb1], true=[aaa1, aaa1]} Map<Boolean, List<String>> map2 = stringCollection.stream() .collect(Collectors.partitioningBy(startA)); //return {false={false=[ddd2], true=[bbb1]}, true={false=[], true=[aaa1, aaa1]}} Predicate<String> end1 = x -> x.endsWith("1"); Map<Boolean, Map<Boolean, List<String>>> map3 = stringCollection.stream() .collect(Collectors.partitioningBy(startA, Collectors.partitioningBy(end1)));
1
2
3
4
5
6
	

- `groupingBy`


接口:
复制代码public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier) public static <T, K, A, D> Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) public static <T, K, D, A, M extends Map<K, D>> Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,Supplier<M> mapFactory, Collector<? super T, A, D> downstream)
1
实现:
复制代码//rerurn {a=[aaa1, aaa1], b=[bbb1], d=[ddd2]} Function<String, String> stringStart = x -> String.valueOf(x.charAt(0)); Map<String, List<String>> map4 = stringCollection.stream() .collect(Collectors.groupingBy(stringStart)); //rerurn {ddd2=1, bbb1=1, aaa1=2} Map<String, Long> map5 = stringCollection.stream() .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); //rerurn {d=1, a=2, b=1} Map<String, Long> map6 = stringCollection.stream() .collect(Collectors.groupingBy(stringStart, LinkedHashMap::new, Collectors.counting()));
1
2
3
4
* `reduce`


`reduce`有三个接口:

复制代码Optional reduce(BinaryOperator accumulator);

U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator combiner);

T reduce(T identity, BinaryOperator accumulator);

1
2

+ `<1> Optional<T> reduce(BinaryOperator<T> accumulator)`
复制代码BinaryOperator<String> binaryOperator = (x, y) -> x + y;
//rerurn ddd2aaa1bbb1aaa1
String reduceStr1 = stringCollection.stream().reduce(binaryOperator).orElse("");

1
+ `<2> T reduce(T identity, BinaryOperator<T> accumulator)`
复制代码//return start:ddd2aaa1bbb1aaa1 String reduceStr2=stringCollection.stream().reduce("start:",binaryOperator);
1
2
3
4
5
6
7
8
9
10
11
12
13
+ `<3> <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner)`



> 第一个参数返回实例u,传递你要返回的U类型对象的初始化实例u



> BiFunction accumulator,负责数据的累加



> BinaryOperator combiner,负责在并行情况下最后合并每个reduce线程的结果
复制代码List<Person> personList = new ArrayList<>(); personList.add(new Person(10, 20)); personList.add(new Person(20, 30)); personList.add(new Person(30, 50)); BiFunction<Person, Person, Person> biFunction = (x, y) -> new Person(x.getAge() + y.getAge(), x.getRate() + y.getRate()); BinaryOperator<Person> binaryOperator1 = (x, y) -> new Person(x.getAge() + y.getAge(), x.getRate() + y.getRate()); Person total = personList.stream().reduce(new Person(0, 0), biFunction, binaryOperator1); System.out.println("total:"+total);
1
2
========
* Map的双重循环

复制代码 //对map的entry对象来做stream操作,使用两次forEach
Map<String, Long> map = new HashMap<>();
crowdMap.entrySet().stream()
.map(Map.Entry::getValue)
.forEach(x -> x.entrySet().forEach(y -> {
if (map.containsKey(y.getKey()))
map.put(y.getKey(), map.get(y.getKey()) + y.getValue());
else map.put(y.getKey(), y.getValue());
}));

//对map的entry对象来做stream操作,使用flatMap将stream合并
Map<String, Long> map = new HashMap<>();
crowdMap.entrySet().stream()
        .map(Map.Entry::getValue)
        .flatMap(x -> x.entrySet().stream())
        .forEach(y -> {
            if (map.containsKey(y.getKey()))
                map.put(y.getKey(), map.get(y.getKey()) + y.getValue());
            else map.put(y.getKey(), y.getValue());
        });

//使用map本身的foreach语句            
Map<String, Long> map = new HashMap<>();
crowdMap.forEach((key, value) -> value.forEach((x, y) -> {
    if (map.containsKey(x))
        map.put(x, map.get(x) + y);
    map.put(x, y);
}));
1
* List的多次分组

复制代码 //使用groupingBy将ApproveRuleDetail对象分别按照item和detail分组,并计次
Map<String, Map<String, Long>> detailMap = approveRuleDetailList.stream()
.collect(Collectors
.groupingBy(ApproveRuleDetail::getItem, Collectors.
groupingBy(ApproveRuleDetail::getDetail, Collectors.counting())));

1
* List按照自定义条件分组

复制代码 //使用自定义的Function函数,将年龄按照每10岁分组
Function<Integer, Integer> ageGroup = x -> x / 10;
Map<Integer, List> ageMap = statisticsPipelineList
.stream()
.collect(Collectors.groupingBy(y -> ageGroup.apply(y.getAge())));

1
2


复制代码 //将年龄按不同方式分组
Function<Integer, Integer> ageCredit = x -> {
if (x <= 18)
return 18;
else if (x >= 40)
return 40;
else return x;
};

//将StatisticsPipeline转化为suggestion
ToDoubleFunction<StatisticsPipeline> mapper = StatisticsPipeline::getSuggestion;

//将人群按照不同年龄分组,并计算每个年龄段的suggestion的平均值
Map<Integer, Double> ageCreditMap = statisticsPipelineList
        .stream()
        .collect(Collectors.groupingBy(y -> ageCredit.apply(y.getAge()), Collectors.averagingDouble(mapper)));
1
* 多个数据求集合

复制代码 //合并数据
private BiFunction<Integer[], ApprovePipeline, Integer[]> accumulator = (x, y) -> new Integer[]{
x[0] + y.getAuth(), x[1] + y.getAntiFraud(), x[2] + y.getCreditRule(), x[3] + y.getModelReject(), x[4] + y.getSuggestion()
};

//合并集合
private BinaryOperator<Integer[]> combiner = (x, y) -> new Integer[]{x[0] + y[0], x[1] + y[1], x[2] + y[2], x[3] + y[3], x[4] + y[4]};

//将ApprovePipeline对象的不同数据相加
Integer[] detail = approvePipelineList.stream().reduce(new Integer[]{0, 0, 0, 0, 0}, accumulator, combiner);
1
* 多个数据求集合-多重合并

复制代码 private BiFunction<Integer[], ApprovePipeline, Integer[]> accumulator = (x, y) -> new Integer[]{
x[0] += y.getAuth(), x[1] += y.getAntiFraud(), x[2] += y.getCreditRule(), x[3] += y.getModelReject(), x[4] += y.getSuggestion()
};
//合并数据
BiFunction<Integer[], Map.Entry<String, List>, Integer[]> newAccumulator = (x, y) -> {
List pipelineList = y.getValue();
Integer[] data = pipelineList.stream().reduce(new Integer[]{0, 0, 0, 0, 0}, accumulator, combiner);
return new Integer[]{
x[0] += data[0], x[1] += data[1], x[2] += data[2], x[3] += data[3], x[4] += data[4]
};
};
//最终reduce
Integer[] total = channelMap.entrySet().stream().reduce(new Integer[]{0, 0, 0, 0, 0}, newAccumulator, combiner);

1
* map最大多项和

复制代码Long hourC3 = hourMap.entrySet().stream().mapToLong(Map.Entry::getValue).sorted().limit(3).sum();



![在这里插入图片描述](https://gitee.com/songjianzaina/juejin_p8/raw/master/img/863c3cf19f6defef9144fce9c1ecd234e318d2f0cd109038d8d96cacb083d20d)






**本文转载自:** [掘金](https://juejin.cn/post/6844904049125359623)

*[开发者博客 – 和开发相关的 这里全都有](https://dev.newban.cn/)*

Spring 注解之Import 注入的各种花活

发表于 2020-01-16

今天来分享一下 pig4cloud 中涉及的 @Import 的注入形式。通过不同形式的注入方式,最大程度使得架构简洁。

@Import导入一个组件

来看 EnablePigxDynamicRoute 这个注解,当我们需要开始动态数据源时,只需要在main 方法加上此注解即可。

1
2
3
4
5
6
7
less复制代码@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(DynamicRouteAutoConfiguration.class)
public @interface EnablePigxDynamicRoute {
}

实际核心是引入 DynamicRouteAutoConfiguration 这个配置类,此类并未被Spring 扫描管理

写个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class Dog {
}

@Import({Dog.class})
@SpringBootApplication
public class SpringLearnApplication {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringLearnApplication.class, args);
Assert.isTrue(context.containsBean("com.pig4cloud.spring.learn.demo1.Dog"), "error dog bean");
}
}

注意 Dog 并未增加上文的声明式注解,注入了一个全类型名称的Bean

ImportSelector 接口

顾名思义导入的选择器,当 @Import 引入的类是ImportSelector接口的实现时,会按照此选择器进行匹配注入

1
2
3
4
5
6
7
typescript复制代码public class DogImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
... 匹配逻辑查询出来一堆要注入的全类名
return new String[]{"com.pig4cloud.spring.learn.demo1.Dog"};
}
}
1
2
3
4
5
6
7
8
9
java复制代码@Import({DogImportSelector.class})
@SpringBootApplication
public class SpringLearnApplication {

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(SpringLearnApplication.class, args);
Assert.isTrue(context.containsBean("com.pig4cloud.spring.learn.demo1.Dog"), "error dog bean");
}
}

ImportBeanDefinitionRegistrar

当@Import 引入的是ImportBeanDefinitionRegistrar接口实现类,会自动引入registerBeanDefinitions 定义的Bean

以pig 的资源服务器配置设置,自动引入了一个 PigxResourceServerConfigurerAdapter的类,且bean 名称为resourceServerConfigurerAdapter

1
2
3
4
5
6
7
8
9
10
reasonml复制代码public class PigxSecurityBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(PigxResourceServerConfigurerAdapter.class);
registry.registerBeanDefinition(SecurityConstants.RESOURCE_SERVER_CONFIGURER, beanDefinition);

}
}

这也就意味着,若使用EnablePigxResourceServer 注解即可开启 pig4cloud 封装的oauth 资源客户端操作类,也是源码的入口

1
2
3
4
less复制代码@Import({PigxSecurityBeanDefinitionRegistrar.class})
public @interface EnablePigxResourceServer {

}

image

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

除了闹过腥风血雨的fastjson,你还知道哪些Java解析

发表于 2020-01-16

昨天下午 5 点 10 分左右,我解决掉了最后一个 bug,轻舒一口气,准备关机下班。可这个时候,老板朝我走来,脸上挂着神秘的微笑,我就知道他不怀好意。果不其然,他扔给了我一个新的需求,要我在 Java 中解析 JSON,并且要在半个小时候给出最佳的解决方案。

无奈,提前下班的希望破灭了。不过,按时下班的希望还是有的。于是我撸起袖子开始了研究,结果出乎我的意料,竟然不到 10 分钟就找出了最佳方案。但我假装还没有搞出来,趁着下班前的这段时间把方案整理成了现在你们看到的这篇文章。

01、JSON 是什么

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于阅读和编写,机器解析和生成起来更是轻而易举。JSON 采用了完全独立于编程语言的文本格式,但它的格式非常符合 C 语言家族的习惯(比如 C、C++、C#、Java、JavaScript、Python 等)。 这种特质使得 JSON 成为了最理想的数据交换格式。

JSON 建构于两种常见的数据结构:

  • “键/值”对。
  • 数组。

这使得 JSON 在同样基于这些结构的编程语言之间的交换成为可能。在 Java 中,解析 JSON 的第三方类库有很多,比如说下面这些。

很多,对不对?但日常开发中,最常用的只有四个:Gson、Jackson、org.json 和阿里巴巴的 fastjson。下面我们来简单地对比下。

02、Gson

Gson 是谷歌提供的一个开源库,可以将 Java 对象序列化为 JSON 字符串,同样可以将 JSON 字符串反序列化(解析)为匹配的 Java 对象。

使用 Gson 之前,需要先在项目中引入 Gson 的依赖。

1
2
3
4
5
6
复制代码<dependency>  
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
    <scope>compile</scope>
</dependency>

1)简单示例

1
2
3
java复制代码Gson gson = new Gson();  
gson.toJson(18);            // ==> 18
gson.toJson("沉默王二");       // ==> "沉默王二"

上面这段代码通过 new 关键字创建了一个 Gson 对象,然后调用其 toJson() 方法将整形和字符串转成了 JSON 字符串。

同样,可以调用 fromJson() 方法将简单的 JSON 字符串解析为整形和字符串。

1
2
3
java复制代码int one = gson.fromJson("18", int.class);  
Integer one1 = gson.fromJson("18", Integer.class);
String str = gson.fromJson("\"沉默王二\"", String.class);

2)复杂点的示例

Cmower 类有两个字段:整形 age 和 字符串 name。

1
2
3
4
java复制代码class Cmower {  
    private int age = 18;
    private String name = "沉默王二";
}

将其转成 JSON 字符串。

1
2
3
java复制代码Gson gson = new Gson();  
String json = gson.toJson(new Cmower());
System.out.println(json);

输出结果为:

1
复制代码{"age":18,"name":"沉默王二"}

可以再通过 fromJson() 方法将字符串 json 解析为 Java 对象。

1
java复制代码gson.fromJson(json, Cmower.class);

3)数组示例

1
2
3
4
5
6
7
8
9
10
11
java复制代码Gson gson = new Gson();  
int[] ints = {1, 2, 3, 4, 5};
String[] strings = {"沉", "默", "王二"};

// 转成 JSON 字符串
gson.toJson(ints);     // ==> [1,2,3,4,5]
gson.toJson(strings);  // ==> ["沉", "默", "王二"]

// 解析为数组
int[] ints2 = gson.fromJson("[1,2,3,4,5]", int[].class);
String[] strings2 = gson.fromJson("[\"沉\", \"默\", \"王二\"]", String[].class);

数组的处理仍然非常简单,调用的方法也仍然是 toJson() 和 fromJson() 方法。

4)集合示例

1
2
3
java复制代码Gson gson = new Gson();  
List<String> list = new ArrayList<>(Arrays.asList("沉", "默", "王二"));
String json = gson.toJson(list); // ==> ["沉","默","王二"]

把集合转成 JSON 字符串并没有什么特别之处,不过,把 JSON 字符串解析为集合就和之前的方法有些不同了。

1
2
java复制代码Type collectionType = new TypeToken<ArrayList<String>>(){}.getType();  
List<String> list2 = gson.fromJson(json, collectionType);

我们需要借助 com.google.gson.reflect.TypeToken 和 java.lang.reflect.Type 来获取集合的类型,再将其作为参数传递给 formJson() 方法,才能将 JSON 字符串解析为集合。

Gson 虽然可以将任意的 Java 对象转成 JSON 字符串,但将字符串解析为指定的集合类型时就需要花点心思了,因为涉及到了泛型——TypeToken 是解决这个问题的银弹。

关于 Gson,我们就先说到这吧,以后有机会的时候再和大家细说。

03、Jackson

Jackson 是基于 Stream 构建的一款用来序列化和反序列化 JSON 的 Java 开源库,社区非常活跃,其版本的更新速度也比较快。

  • 截止到目前,GitHub 上已经星标 5.2K 了;
  • Spring MVC 的默认 JSON 解析器;
  • 与 Gson 相比,Jackson 在解析大的 JSON 文件时速度更快。
  • 与 fastjson 相比,Jackson 更稳定。

在使用 Jackson 之前,需要先添加 Jackson 的依赖。

1
2
3
4
5
xml复制代码<dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.1</version>
</dependency>

Jackson 的核心模块由三部分组成。

  • jackson-core,核心包,提供基于”流模式”解析的相关 API,它包括 JsonPaser 和 JsonGenerator。
  • jackson-annotations,注解包,提供标准注解功能。
  • jackson-databind ,数据绑定包, 提供基于”对象绑定” 解析的相关 API ( ObjectMapper ) 和”树模型” 解析的相关 API (JsonNode);基于”对象绑定” 解析的 API 和”树模型”解析的 API 依赖基于”流模式”解析的 API。

当添加 jackson-databind 之后, jackson-core 和 jackson-annotations 也随之添加到 Java 项目工程中。

这里顺带推荐一个 IDEA 插件:JsonFormat,可以将 JSON 字符串生成一个 JavaBean。怎么使用呢?可以新建一个类,然后调出 Generate 菜单。

选择 JsonFormat,输入 JSON 字符串。

1
2
3
4
复制代码{  
  "age" : 18,
  "name" : "沉默王二"
}

确认后生成 JavaBean,生成的内容如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public class Cmower {  
    private Integer age;
    private String name;

    public Cmower() {
    }

    public Cmower(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

那怎么使用 Jackson 呢?上文已经提到,ObjectMapper 是 Jackson 最常用的 API,我们来看一个简单的示例。

1
2
3
4
5
6
7
8
9
10
java复制代码Cmower wanger = new Cmower(18,"沉默王二");  
System.out.println(wanger);

ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(wanger);

System.out.println(jsonString);

Cmower deserialize = mapper.readValue(jsonString,Cmower.class);
System.out.println(deserialize);

ObjectMapper 通过 writeValue() 的系列方法可以将 Java 对象序列化为 JSON,并将 JSON 存储成不同的格式。

  • String(writeValueAsString)
  • Byte Array(writeValueAsBytes)

ObjectMapper 通过 readValue() 系列方法可以从不同的数据源(String、Bytes)将 JSON 反序列化(解析)为 Java 对象。

程序输出结果为:

1
2
3
4
5
6
复制代码com.cmower.java_demo.jackson.Cmower@214c265e  
{
  "age" : 18,
  "name" : "沉默王二"
}
com.cmower.java_demo.jackson.Cmower@612fc6eb

在调用 writeValue() 或者 readValue() 方法之前,往往需要对 JSON 和 JavaBean 之间进行一些定制化配置。

1)在反序列化时忽略在 JSON 中存在但 JavaBean 不存在的字段

1
java复制代码mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

2)在序列化时忽略值为 null 的字段

1
java复制代码apper.setSerializationInclusion(Include.NON_NULL);

有些时候,这些定制化的配置对 JSON 和 JavaBean 之间的转化起着重要的作用。如果需要更多配置信息,查看 DeserializationFeature、SerializationFeature 和 Include 类的 Javadoc 即可。

关于 Jackson,我们就说到这吧,以后有机会的时候再和大家细说。

04、org.json

org.json 是 JSON 官方提供的一个开源库,不过使用起来就略显繁琐了。

使用 org.json 之前,需要先在项目中引入 org.json 的依赖。

1
2
3
4
5
复制代码<dependency>  
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20190722</version>
</dependency>

org.json.JSONObject 类可以通过 new 关键字将 JSON 字符串解析为 Java 对象,然后 get 的系列方法获取对应的键值,代码示例如下所示。

1
2
3
4
java复制代码String str = "{ \"name\": \"沉默王二\", \"age\": 18 }";  
JSONObject obj = new JSONObject(str);
String name = obj.getString("name");
int age = obj.getInt("age");

调用 org.json.JSONObject 类的 getJSONArray() 方法可以返回一个表示数组的org.json.JSONArray 对象,再通过循环的方式可以获取数组中的元素,代码示例如下所示。

1
2
3
4
5
6
java复制代码String str = "{ \"number\": [3, 4, 5, 6] }";  
JSONObject obj = new JSONObject(str);
JSONArray arr = obj.getJSONArray("number");
for (int i = 0; i < arr.length(); i++) {
    System.out.println(arr.getInt(i));
}

如果想获取 JSON 字符串,可以使用 put() 方法将键值对放入 org.json.JSONObject 对象中,再调用 toString() 方法即可,代码示例如下所示。

1
2
3
4
java复制代码JSONObject obj = new JSONObject();  
obj.put("name","沉默王二");
obj.put("age",18);
System.out.println(obj.toString()); // {"name":"沉默王二","age":18}

相比较于 Gson 和 Jackson 来说,org.json 就要逊色多了,不仅不够灵活,API 也不够丰富。

05、fastjson

fastjson 是阿里巴巴开源的 JSON 解析库,它可以解析 JSON 格式的字符串,也支持将 Java Bean 序列化为 JSON 字符串。

fastjson 相对于其他 JSON 库的特点就是快,另外 API 使用起来也非常简单,更是在 2012 年被开源中国评选为最受欢迎的国产开源软件之一。

PS:尽管 fastjson 值得信赖,但也闹过不少腥风血雨,这里就不提了。

在使用 fastjson 之前,需要先添加 fastjson 的依赖。

1
2
3
4
5
复制代码<dependency>  
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.61</version>
</dependency>

那怎么使用 fastjson 呢?我们来创建一个 Java Bean,有三个字段:年龄 age,名字 name,列表 books。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Cmower1 {  
    private Integer age;
    private String name;
    private List<String> books = new ArrayList<>();

    public Cmower1(Integer age, String name) {
        this.age = age;
        this.name = name;
    }
   // getter/setter

    public void putBook(String bookname) {
        this.books.add(bookname);
    }
}

然后我们使用 JSON.toJSONString() 将 Java 对象序列化为 JSON 字符串,代码示例如下:

1
2
3
4
java复制代码Cmower1 cmower = new Cmower1(18,"沉默王二");  
cmower.putBook("《Web全栈开发进阶之路》");
String jsonString = JSON.toJSONString(cmower);
System.out.println(jsonString);

程序输出:

1
复制代码{"age":18,"books":["《Web全栈开发进阶之路》"],"name":"沉默王二"}

那如何解析 JSON 字符串呢?使用 JSON.parseObject() 方法,代码示例如下所示。

1
java复制代码JSON.parseObject(jsonString, Cmower1.class)

06、总结

就我个人而言,我是比较推崇 Gson 的,毕竟是谷歌出品的,品质值得信赖,关键是用起来也确实比较得劲。

Jackson 呢,在解析大的 JSON 文件时速度更快,也比 fastjson 稳定。

fastjson 呢,作为我们国产开源软件中的骄傲,嗯,值得尊敬。

令我意外的是,org.json 在 StackOverflow 上一个 160 万浏览量的提问中,牢牢地占据头名答案。更令我想不到的是,老板竟然也选择了 org.json,说它比较原生,JSON 官方的亲儿子。

我。。。。。。

07、鸣谢

好了,各位读者朋友们,以上就是本文的全部内容了。能看到这里的都是最优秀的程序员,升职加薪就是你了👍。如果觉得不过瘾,还想看到更多,我再推荐一篇给大家。

还有一周就解放了,无心撸码,着急回家

原创不易,如果觉得有点用的话,请不要吝啬你手中点赞的权力;如果想要第一时间看到二哥更新的文章,请扫描下方的二维码,关注沉默王二公众号。我们下篇文章见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring boot 集成 dubbo 企业完整版 一、什

发表于 2020-01-15

一、什么是Spring Boot ?

现阶段的 Spring Boot 可谓是太火了,为什么呢?因为使用方便、配置简洁、上手快速,那么它是什么?从官网上我们可以看到,它是 Spring 开源组织下的一个子项目,主要简化了 Spring 繁重的配置,而且 Spring Boot 内嵌了各种 Servlet 容器,如:Tomcat、Jetty 等

官方网站:http://projects.spring.io/spring-boot/GitHub源码:https://github.com/spring-projects/spring-boot

二、Spring Boot 的优势 ?

1、独立运行:不需要在用 tomcat 等容器运行。2、简化配置:不需要在像 Spring mvc 那样配置很多的xml了;3、自动配置:根据包路径自动配置 bean4、应用监控:Spring Boot 提供监控服务

三、项目创建

1、创建提供者

image.png

image.pngimage.png

image.png

后面点击 finish,创建完毕,然后删掉多余包,使得项目结构如下图:

image.png

右击项目,新建一个提供者对外提供服务的模块 qbs-facade

image.png

image.png

image.png

然后再按照该模式创建一个 qbs-web 模块(这里就不介绍了)最终的项目结构如下图所示:image.png

image.png

修改 主 pom 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<packaging>pom</packaging>
<groupId>com.btd</groupId>
<artifactId>qbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>qbs</name>

<modules>
<module>qbs-facade</module>
<module>qbs-api</module>
</modules>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>

<properties>
<java.version>1.8</java.version>
<dubbo.version>2.7.1</dubbo.version>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

3、编写 facade

image.png

SayFacade.java

1
2
3
4
5
haxe复制代码package com.btd.qbs.facade;

public interface SayFacade {
String say(String context);
}

facade 模块的 xml 文件,它只是对外提供一下接口的,所以不需要其它东西

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>com.btd</groupId>
<artifactId>qbs-facade</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>qbs-facade</name>
<packaging>jar</packaging>

</project>

4、组织api模块,实际接口的实现

image.png

先看 pom.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
dust复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>qbs</artifactId>
<groupId>com.btd</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>qbs-api</artifactId>
<packaging>jar</packaging>


<dependencies>
<!-- spring boot 相关 start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring boot 相关 end -->

<!--dubbo 相关-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</dependency>
<!-- dubbo 相关依赖 end-->

<dependency>
<groupId>com.btd</groupId>
<artifactId>qbs-facade</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>

</project>

application.properties 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码spring.application.name=qbs-provider

server.port=11222

dubbo.application.id=${spring.application.name}
dubbo.application.name=${spring.application.name}
dubbo.protocol.port = 28820
dubbo.protocol.name=${spring.application.name}

# zk注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

# 提供者配置
dubbo.provider.name=dubbo
dubbo.provider.protocol=dubbo
dubbo.provider.version=1.0.0
dubbo.provider.timeout=30000

SayFacadeImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码package com.btd.qbs.service;

import com.btd.qbs.facade.SayFacade;
import org.apache.dubbo.config.annotation.Service;

@Service
public class SyaFacadeImpl implements SayFacade {

@Override
public String say(String context) {
return "小肥羊对你说:"+context;
}
}

启动文件 Application.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
arduino复制代码package com.btd.qbs;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@EnableDubbo
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

OK ,到这里来说,我们的提供者就完成了,启动项目,然后我们看看 dubbo-adminimage.png

这里要注意,因为我们的 facade 是对外需要映入的,所以我们打个 jar 包

image.png

打完包后,我们会得到一个 jar 文件image.png

2、建立消费者

image.png

引入我们打的 jar 包

image.png

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码spring.application.name=qbs-consumer

server.port=11121
dubbo.application.id=${spring.application.name}
dubbo.application.name=${spring.application.name}
dubbo.protocol.port=28820
dubbo.protocol.name=dubbo

# zk注册中心地址
dubbo.registry.address=zookeeper://172.25.37.130:2181

# 消费者配置
dubbo.consumer.version=1.0.0
dubbo.consumer.check=false
dubbo.consumer.timeout=8000

pom.xml 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
dust复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.btd.abs</groupId>
<artifactId>qbs-consumer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>qbs-consumer</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<dubbo.version>2.7.1</dubbo.version>
</properties>

<dependencies>
<!-- spring boot 相关 start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring boot 相关 end -->

<!--dubbo 相关-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-dependencies-zookeeper</artifactId>
<version>${dubbo.version}</version>
<type>pom</type>
</dependency>
<!-- dubbo 相关依赖 end-->

<!-- 基础依赖 start -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- 基础依赖 end -->

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Application.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
arduino复制代码package com.btd.abs;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@EnableDubbo
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

DbsController.java 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码package com.btd.abs.controller;

import com.btd.qbs.facade.SayFacade;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping("/qbs")
public class DbsController {

@Reference
private SayFacade sayFacade;

@GetMapping("/say")
@ResponseBody
public String say(String context) {
return sayFacade.say(context);
}
}

最后调用结果:http://localhost:11121/qbs/say?context=asde3

image.png

image.png

好了,这是spring boot 集成 dubbo的一整套商业使用的代码事例,按着从上到下的操作,是完全OK的;

有兴趣的可以关注下我的公众号哦!公众号

原创文章,转载请声明,感谢!!!!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

老生常谈!数据库如何存储时间?你真的知道吗?

发表于 2020-01-14

我们平时开发中不可避免的就是要存储时间,比如我们要记录操作表中这条记录的时间、记录转账的交易时间、记录出发时间等等。你会发现这个时间这个东西与我们开发的联系还是非常紧密的,用的好与不好会给我们的业务甚至功能带来很大的影响。所以,我们有必要重新出发,好好认识一下这个东西。

这是一篇短小精悍的文章,仔细阅读一定能学到不少东西!

1.切记不要用字符串存储日期

我记得我在大学的时候就这样干过,而且现在很多对数据库不太了解的新手也会这样干,可见,这种存储日期的方式的优点还是有的,就是简单直白,容易上手。

但是,这是不正确的做法,主要会有下面两个问题:

  1. 字符串占用的空间更大!
  2. 字符串存储的日期比较效率比较低(逐个字符进行比对),无法用日期相关的 API 进行计算和比较。

2.Datetime 和 Timestamp 之间抉择

Datetime 和 Timestamp 是 MySQL 提供的两种比较相似的保存时间的数据类型。他们两者究竟该如何选择呢?

通常我们都会首选 Timestamp。 下面说一下为什么这样做!

2.1 DateTime 类型没有时区信息的

DateTime 类型是没有时区信息的(时区无关) ,DateTime 类型保存的时间都是当前会话所设置的时区对应的时间。这样就会有什么问题呢?当你的时区更换之后,比如你的服务器更换地址或者更换客户端连接时区设置的话,就会导致你从数据库中读出的时间错误。不要小看这个问题,很多系统就是因为这个问题闹出了很多笑话。

Timestamp 和时区有关。Timestamp 类型字段的值会随着服务器时区的变化而变化,自动换算成相应的时间,说简单点就是在不同时区,查询到同一个条记录此字段的值会不一样。

下面实际演示一下!

建表 SQL 语句:

1
2
3
4
5
6
pgsql复制代码CREATE TABLE `time_zone_test` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`date_time` datetime DEFAULT NULL,
`time_stamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入数据:

1
scss复制代码INSERT INTO time_zone_test(date_time,time_stamp) VALUES(NOW(),NOW());

查看数据:

1
axapta复制代码select date_time,time_stamp from time_zone_test;

结果:

1
2
3
4
5
asciidoc复制代码+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 09:53:32 |
+---------------------+---------------------+

现在我们运行

修改当前会话的时区:

1
routeros复制代码set time_zone='+8:00';

再次查看数据:

1
2
3
4
5
asciidoc复制代码+---------------------+---------------------+
| date_time | time_stamp |
+---------------------+---------------------+
| 2020-01-11 09:53:32 | 2020-01-11 17:53:32 |
+---------------------+---------------------+

扩展:一些关于 MySQL 时区设置的一个常用 sql 命令

1
2
3
4
5
6
7
8
9
10
nginx复制代码# 查看当前会话时区
SELECT @@session.time_zone;
# 设置当前会话时区
SET time_zone = 'Europe/Helsinki';
SET time_zone = "+00:00";
# 数据库全局时区设置
SELECT @@global.time_zone;
# 设置全局时区
SET GLOBAL time_zone = '+8:00';
SET GLOBAL time_zone = 'Europe/Helsinki';

2.2 DateTime 类型耗费空间更大

Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

  • DateTime :1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
  • Timestamp: 1970-01-01 00:00:01 ~ 2037-12-31 23:59:59

Timestamp 在不同版本的 MySQL 中有细微差别。

3 再看 MySQL 日期类型存储空间

下图是 MySQL 5.6 版本中日期类型所占的存储空间:

可以看出 5.6.4 之后的 MySQL 多出了一个需要 0 ~ 3 字节的小数位。Datatime 和 Timestamp 会有几种不同的存储空间占用。

为了方便,本文我们还是默认 Timestamp 只需要使用 4 个字节的存储空间,但是 DateTime 需要耗费 8 个字节的存储空间。

4.数值型时间戳是更好的选择吗?

很多时候,我们也会使用 int 或者 bigint 类型的数值也就是时间戳来表示时间。

这种存储方式的具有 Timestamp 类型的所具有一些优点,并且使用它的进行日期排序以及对比等操作的效率会更高,跨系统也很方便,毕竟只是存放的数值。缺点也很明显,就是数据的可读性太差了,你无法直观的看到具体时间。

时间戳的定义如下:

时间戳的定义是从一个基准时间开始算起,这个基准时间是「1970-1-1 00:00:00 +0:00」,从这个时间开始,用整数表示,以秒计时,随着时间的流逝这个时间整数不断增加。这样一来,我只需要一个数值,就可以完美地表示时间了,而且这个数值是一个绝对数值,即无论的身处地球的任何角落,这个表示时间的时间戳,都是一样的,生成的数值都是一样的,并且没有时区的概念,所以在系统的中时间的传输中,都不需要进行额外的转换了,只有在显示给用户的时候,才转换为字符串格式的本地时间。

数据库中实际操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
asciidoc复制代码mysql> select UNIX_TIMESTAMP('2020-01-11 09:53:32');
+---------------------------------------+
| UNIX_TIMESTAMP('2020-01-11 09:53:32') |
+---------------------------------------+
| 1578707612 |
+---------------------------------------+
1 row in set (0.00 sec)

mysql> select FROM_UNIXTIME(1578707612);
+---------------------------+
| FROM_UNIXTIME(1578707612) |
+---------------------------+
| 2020-01-11 09:53:32 |
+---------------------------+
1 row in set (0.01 sec)

5.总结

MySQL 中时间到底怎么存储才好?Datetime?Timestamp? 数值保存的时间戳?

好像并没有一个银弹,很多程序员会觉得数值型时间戳是真的好,效率又高还各种兼容,但是很多人又觉得它表现的不够直观。这里插一嘴,《高性能 MySQL 》这本神书的作者就是推荐 Timestamp,原因是数值表示时间不够直观。下面是原文:

每种方式都有各自的优势,根据实际场景才是王道。下面再对这三种方式做一个简单的对比,以供大家实际开发中选择正确的存放时间的数据类型:

如果还有什么问题欢迎给我留言!如果文章有什么问题的话,也劳烦指出,Guide 哥感激不尽!

后面的文章我会介绍:

  • Java8 对日期的支持以及为啥不能用 SimpleDateFormat。
  • SpringBoot 中如何实际使用(JPA 为例)

开源项目推荐

作者的其他开源项目推荐:

  1. JavaGuide:【Java学习+面试指南】 一份涵盖大部分Java程序员所需要掌握的核心知识。
  2. springboot-guide : 适合新手入门以及有经验的开发人员查阅的 Spring Boot 教程(业余时间维护中,欢迎一起维护)。
  3. programmer-advancement : 我觉得技术人员应该有的一些好习惯!
  4. spring-security-jwt-guide :从零入门 !Spring Security With JWT(含权限验证)后端部分代码。

公众号

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

恕我直言,牛逼哄哄的MongoDB你可能只会30% Mong

发表于 2020-01-14

MongoDB闪亮登场

自我介绍

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

MongoDB最大的特点就是无Schema限制,灵活度很高。数据格式是BSON,BSON是一种类似JSON的二进制形式的存储格式,简称Binary JSON 它和JSON一样,支持内嵌的文档对象和数组对象。

跟关系型数据库概念对比

Mysql MongoDB
Database(数据库) Database(数据库)
Table(表) Collection(集合)
Row(行) Document(文档)
Column(列) Field(字段)

数据格式

MongoDB 将数据存储为一个文档,BSON格式。由key 和 value组成。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{ 
"_id" : ObjectId("5e141148473cce6a9ef349c7"),
    "title" : "批量更新", 
    "url" : "http://cxytiandi.com/blog/detail/8", 
    "author" : "yinjihuan", 
    "tags" : [
        "java", 
        "mongodb", 
        "spring"
    ], 
    "visit_count" : NumberLong(10), 
    "add_time" : ISODate("2019-02-11T07:10:32.936+0000")
}

使用场景

  • 大数据量存储场景

MongoDB自带副本集和分片,天生就适用于大数量场景,无需开发人员通过中间件去分库分表,非常方便。

  • 操作日志存储

很多时候,我们需要存储一些操作日志,可能只需要存储比如最近一个月的,一般的做法是定期去清理,在MongoDB中有固定集合的概念,我们在创建集合的时候可以指定大小,当数据量超过大小的时候会自动移除掉老数据。

  • 爬虫数据存储

爬下来的数据有网页,也有Json格式的数据,一般都会按照表的格式去存储,如果我们用了MongoDB就可以将抓下来的Json数据直接存入集合中,无格式限制。

  • 社交数据存储

在社交场景中使用 MongoDB 存储存储用户地址位置信息,通过地理位置索引实现附近的人,附近的地点等。

  • 电商商品存储

不同的商品有不同的属性,常见的做法是抽出公共的属性表,然后和SPU进行关联,如果用MongoDB的话那么SPU中直接就可以内嵌属性。

自我陶醉

MongoDB的功能点很多,但是大部分场景下我们只用了最简单的CRUD操作。下面隆重的介绍下MongoDB的功能点,就像你去相亲一样,不好好介绍自己的优点又怎能让你对面的菇凉心动呢?

CRUD

CRUD也就是增删改查,这是数据库最基本的功能,查询还支持全文检索,GEO地理位置查询等。

  • db.collection.insertOne()

单个文档插入到集合中

  • db.collection.insertMany()

多个文档插入到集合中

  • db.collection.insert()

单个或者多个文件插入到集合中

  • db.collection.find( )

查询数据

  • db.inventory.updateOne()

更新单条

  • db.inventory.updateMany()

更新多条

  • db.inventory.deleteOne( )

删除单条文档

  • db.inventory.deleteMany()

删除多条文档

Aggregation

聚合操作用于数据统计方面,比如Mysql中会有count,sum,group by等功能,在MongoDB中相对应的就是Aggregation聚合操作。

聚合下面有两种方式来实现我们需要对数据进行统计的需求,一个是aggregate,一个是MapReduce。

下图展示了aggregate的执行原理:

图片

聚合内置了很多函数,使用好了这些函数我们就可以统计出我们想要的数据。

$project:修改输入文档的结构。可以用来重命名、增加或删除域,也可以用于创建计算结果以及嵌套文档。

match:用于过滤数据,只输出符合条件的文档。match使用MongoDB的标准查询操作。

$limit:用来限制MongoDB聚合管道返回的文档数。

$skip:在聚合管道中跳过指定数量的文档,并返回余下的文档。

$group:将集合中的文档分组,可用于统计结果。

$sort:将输入文档排序后输出。

$geoNear:输出接近某一地理位置的有序文档。

$unwind:将文档中的某一个数组类型字段拆分成多条,每条包含数组中的一个值。

下图展示了MapReduce的执行原理:

图片

总共4条数据,query指定了查询条件,只处理status=A的数据。

map阶段对数据进行分组聚合,也就是形成了第三部分的效果,根据cust_id去重统计。

reduce中的key也就是cust_id, values也就是汇总的amount集合。然后进行sum操作,最终的结果通过out输出到一个集合中。

Transactions

MongoDB最开始是不支持事务的,在MongoDB中,对单个文档的操作是原子性操作。所以再设计的时候可以使用嵌入的文档和数组来描述数据之间的关系,这样就不用跨多个文档和集合进行操作,也就通过了单文档原子性消除了许多实际用例对多文档事务的需要。

任何事物都是有限制的,某些场景还是不能完全通过内嵌的方式来描述数据的关系,还是会存在多个集合,对于使用MongoDB的用户来说,如果能支持事务就很方便了。

不负众望,MongoDB 4.0 版本的发布,为我们带来了原生的事务操作。

Indexes

索引不用我多说了,作用大家都知道。单索引,组合索引,全文索引,Hash索引等。

1
复制代码db.collection.createIndex({user_id: 1, add_time: 1}, {background: true})

创建索引特别要注意的是将background设置为true,在建索引的过程会阻塞其它数据库操作,background可指定以后台方式创建索引,默认为false。这可是血的教训呀,切记切记。

Security

MongoDB中的安全需要重视,目前启动不知道有没有强制的限制,以前启动的时候可以不指定认证的方式,也就是不需要密码即可访问,然后很多人都直接用的默认端口,暴露在公网上,给不法分子有机可乘,出现了数据被删,需要用比特币来找回数据的案例比比皆是。

还是要开启安全认证,内置了很多角色,不同的角色可操作的内容不一样,控制的比较细。

Replication

副本集是一组相同数据集的MongoDB实例,同时在多个节点存储数据,提高了可用性。主节点负责写入,从节点负责读取,提高整体性能。

副本集由下面的组件构成:

Primary:主节点接收所有的写操作。

Secondaries:从节点会从主节点进行数据的复制,维护跟主节点相同的数据。用于查询操作。

Arbiter:仲裁节点本身不存储数据,只参与选举。

图片

Sharding

分片是MongoDB绝对的亮点,将数据水平拆分到多个节点。MongoDB的分片是全自动的,我们只需要配置好分片的规则,它就能自动维护数据并存储到不同节点。MongoDB使用分片来支持大数据量的存储和高吞吐量的操作。

下图是Mongodb的分片集群架构图:

图片

MongoDB分片集群由以下组件够成:

Shard:每个shard的数据都是独立完整的一份。并且可以作为副本集部署。

mongos:mongos是查询路由器,在客户端和服务端中间的一层,请求会直接到mongos,由mongos路由到具体的Shard。

Config Servers:存储集群所有节点、分片数据路由信息。

GridFS

GridFS是MongoDB的一个子模块,主要用于在MongoDB中存储文件,相当于MongoDB内置的一个分布式文件系统。

本质上还是讲文件的数据分块存储在集合中,默认的文件集合分为fs.files和fs.chunks。

fs.files是存储文件的基本信息,比如文件名,大小,上传时间,md5等。fs.chunks是存储文件真正数据的地方,一个文件会被分割成多个chunk块进行存储,一般为256k/个。

图片

如果你的项目中用到了MongoDB,那么你可以使用GridFS来构建一个文件系统,这样就不用去购买第三方的存储服务了。

GridFS的好处是你不用单独去搭建一个文件系统,直接使用Mongodb自带的即可,备份,分片都依赖MongoDB,维护起来也方便。

知识点总结

下图是我自己总结的一些知识点,作为一个后端开发来说,能掌握下面的内容就已经不错了,毕竟我们又不是要去抢DBA的饭碗,如果大家业余时间要学习的话可以按照下面的点进行学习,几年前我录制了一套视频,在我的网站上,大部分内容都覆盖到了。

图片

工作必用

MongoDB跟Mysql的语法对比

图片

图片

Spring Boot中集成MongoDB

加入MongoDB的依赖:

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

配置MongoDB的信息:

1
2
3
4
复制代码spring.data.mongodb.database=test
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
// 用户名,密码省略.......

直接注入MongoTemplate就可以操作MongoDB:

1
2
复制代码@Autowired
private MongoTemplate mongoTemplate;

使用示列

创建一个实体类,对应MongoDB的集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@Data
@Document(collection = "article_info")
public class Article {
@Id
@GeneratedValue
private Long id;
@Field("title")
private String title;
@Field("url")
private String url;
@Field("author")
private String author;
@Field("tags")
private List<String> tags;
@Field("visit_count")
private Long visitCount;
@Field("add_time")
private Date addTime;
}

最终存储到数据中的格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{ 
"_id" : ObjectId("5e141148473cce6a9ef349c7"),
    "title" : "批量更新", 
    "url" : "http://cxytiandi.com/blog/detail/8", 
    "author" : "yinjihuan", 
    "tags" : [
        "java", 
        "mongodb", 
        "spring"
    ], 
    "visit_count" : NumberLong(10), 
    "add_time" : ISODate("2019-02-11T07:10:32.936+0000")
}

插入数据

1
2
3
4
5
6
7
8
复制代码Article article = new Article();
article.setTitle("MongoTemplate 的基本使用 ");
article.setAuthor("yinjihuan");
article.setUrl("http://cxytiandi.com/blog/detail/1");
article.setTags(Arrays.asList("java", "mongodb", "spring"));
article.setVisitCount(0L);
article.setAddTime(new Date());
mongoTemplate.save(article);

数据库语法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码db.article_info.save({
"title": "批量更新",
"url": "http://cxytiandi.com/blog/detail/8",
"author": "yinjihuan",
"tags": [
"java",
"mongodb",
"spring"
],
"visit_count": NumberLong(10),
"add_time": ISODate("2019-02-11T07:10:32.936+0000")
})

更新数据

1
2
3
4
复制代码Query query = Query.query(Criteria.where("author").is("yinjihuan")); 
Update update = Update.update("title", "MongoTemplate")
.set("visitCount", 10); 
mongoTemplate.updateMulti(query, update, Article.class);

数据库语法

1
2
3
4
5
6
7
8
9
复制代码db.article_info.updateMany(
{"author":"yinjihuan"}, 
{"$set":
  {
    "title":"MongoTemplate", 
    "visit_count": NumberLong(10)
  }
}
)

删除数据

1
2
复制代码Query query = Query.query(Criteria.where("author").is("yinjihuan")); 
mongoTemplate.remove(query, Article.class);

数据库语法

1
复制代码db.article_info.remove({"author":"yinjihuan"})

查询数据

1
2
复制代码Query query = Query.query(Criteria.where("author").is("yinjihuan")); 
List<Article> articles = mongoTemplate.find(query, Article.class);

数据库语法

1
复制代码db.article_info.find({"author":"yinjihuan"})

存储文件

1
2
3
4
5
复制代码File file = new File("/Users/yinjihuan/Downloads/logo.png");
InputStream content = new FileInputStream(file);
// 存储文件的额外信息,比如用户ID,后面要查询某个用户的所有文件时就可以直接查询
DBObject metadata = new BasicDBObject("userId", "1001");
ObjectId fileId = gridFsTemplate.store(content, file.getName(), "image/png", metadata);

源码参考

github.com/yinjihuan/s…

客户端推荐

下载地址:

studio3t.com/download/

图片

spring-boot-starter-mongodb-pool

最后推荐一个我自己写的小框架:Spring Boot中增强Mongodb的配置,多数据源,连接池

github.com/yinjihuan/s…

感兴趣的可以关注下我的微信公众号 猿天地,更多技术文章第一时间阅读。我的GitHub也有一些开源的代码 github.com/yinjihuan

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…836837838…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%