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

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


  • 首页

  • 归档

  • 搜索

CQRS & Event Sourcing — 解决检索应用

发表于 2019-04-20

现在,每个开发人员都很熟悉MVC标准体系结构设计模式。大多数的应用程序都是基于这种体系结构进行创建的。它允许我们创建可扩展的大型企业应用程序,但近期我们还听到了另外的一些有关于CQRS/ES的相关信息。这些方法应该被放在MVC中一起使用吗?他们可以解决什么问题?现在,让我们一起来看看CQRS/ES是什么,以及他们都有哪些优点和缺点。

CQRS — 模式介绍

CQRS(Command Query Responsibility Segregation)是一种简单的设计模式。它衍生与CQS,即命令和查询分离,CQS是由Bertrand Meyer所设计。按照这一设计概念,系统中的方法应该分为两种:改变状态的命令和返回值的查询。Greg young将引入了这个设计概念,并将其应用于对象或者组件当中,这就是今天所要将的CQRS。它背后的主要思想是应用程序更改对象或组件状态(Command)应该与获取对象或者组件信息(Query)分开。

下面,将通一张图来说明应用程序中有关CQRS部分的组成结构:

CQRS模式介绍

Commands(命令)—表示用户的操作意图。它们包含了与用户将要对系统执行操作的所有必要信息。

  • Command Bus(命令总线):是一种接收命令并将命令传递给命令处理程序的队列。
  • Command Handler(命令处理程序):包含实际的业务逻辑,用于验证和处理命令中接收到的数据。Command handler负责生成和传播域事件(Event)到事件总线(Event Bus)。
  • Event Bus(事件总线):将事件发布给订阅特定事件类型的事件处理程序。如果存在连续的事件依赖,事件总线可以使用异步或者同步的方式将事件发布出去。
  • Event Handler(事件处理程序):负责处理特定类型的事件。它们的职责是将应用程序的最新状态保存到读库中,并执行终端的相关操作,如发送电子邮件,存储文件等。

Query(查询):表示用户实际可用的应用程序状态。获取UI的数据应该通过这些对象完成。

下面我们将介绍有关CQRS的诸多优点,它们是:

  • 我们可以给处理业务逻辑部分和处理查询部分的开发人员分别分配任务,但需要小心的是,这种模式可能会破坏信息的完整性。
  • 通过在多个不同的服务器上扩展Commands和Query,我们可以进一步提升应用程序的读/写性能。
  • 使用两个不同的数据库(读库/写库)进行同步,可以实现自动备份,无需额外的干预工作。
  • 读取数据时不会涉及到写库的操作,因此在使用事件源是读数据操作会更快。
  • 我们可以直接为视图层构建数据,而无需考虑域逻辑,这可以简化视图层的工作并提高性能。

尽管使用CQRS模式具有上述诸多的优点,但是在使用前还需要慎重考虑。对于只具有简单域的简单项目,其UI模型与域模型紧密联系的,使用CQRS反而会增加项目的复杂度和冗余度,这无疑是过度的设计项目。此外,对于数据量较少或者性能要求较低的项目实施CQRS模式不会带来显著的性能提升。

Event Sourcing — 案例研究

有这样一个案例,我们想要检索任何一个域对象的历史状态数据,而且在任何时间都可以生成统计数据。我们想要检查上个月、上个季度或者过去任何时间的状态汇总。想要解决这个问题并不容易。我们可以在特定的时间范围内将额外的数据保存在数据库中,但这种方法也存在一些缺点。我们不知道范围应该是什么样子,以及未来统计数据需要哪些数据项。为了避免这些问题,我们可以每天为所有聚合创建快照,但它们同样会产生大量的冗余数据。

Event Sourcing(ES)似乎是目前解决这些问题的最佳方案。Event Sourcing允许我们将Aggregate(聚合)状态的每一个更改事件保存在Event Store的事件存储库中。通过Command Handler将事件写入到事件存储库中,并处理相关的逻辑。要创建Aggregate(聚合)对象的当前状态,我们需要运行创建预期域对象的所有事件并对其执行所有的更改。下面我们将通过一张图来说明这一架构设计方式:

event-sourcing

下面我们将列举一些使用ES的优点:

  • 时间穿梭机:可以及时重建特定聚合的状态。每个事件都包含一个时间戳。根据这些时间戳可以在特定的时间内运行事件或者停止事件。
  • 自动审计:我们不需要额外的工作就可以检查出在特定的时间范围内谁做了什么以及改变了什么。这和可以显示更改历史记录的系统日志不同,事件可以告知我们每次更改背后所对应的操作意图。
  • 易于引入纠正措施:当数据库中的数据发生错误时,我们可以将应用程序的状态回退到特定的时间点上,并重建当时的应用程序状态。
  • 易于调试:如果应用程序出现问题,我们可以将特定事件内的所有事件取出,并逐条的重建应用状态,以检查应用程序可能出现问题的地方。这样我们可以更快的找到问题,缩短调试所需的时间。

Aggregates

**Aggregate(聚合)**一词在本文中多次被提及,那它到底是什么意思?**Aggregate(聚合)**来自于领域驱动设计(DDD)的一个概念,它指的是始终保持一致状态的实体或者相关实体组。我们可以简单的理解为接收和处理Command(包含Command Handler)的一个边界,然后根据当前状态生成事件。在通常情况下,Aggregate root(聚合根)由一个域对象构成,但它可以由多个对象组成。我们还需要注意整个应用程序可以包含多个Aggregate(聚合),并且所有事件都存储在同一个存储库中。

总结

CQRS/ES可以作为特定问题的解决方案。它可以在标准N层架构设计的应用程序的某些层中进行引入,它可以解决非标准问题,常规架构中我们所拿到的是最终状态,在很多情况下,固然当前状态很重要,但我们还需要知道当前状态是如何产生的。CQRS和ES两种概念应该一起使用吗?事实表明,并没有。我们想要统计任何时间范文内的域对象状态,而写库只能存储当前状态。引入CQRS并没能帮助我们解决这一问题。在下一章节中,我们将引入Axon框架,Axon框架时间了CQRS/ES,用于解决某些域对象的一些特定问题,尤其是收集历史统计数据。我们将阐述如何使用Axon框架实现CQRS/ES并实现与Spring Boot应用程的整合。

作者:LukaszKucik ,译:谭朝红,原文:CQRS and Event Sourcing as an antidote for problems with retrieving application states

访问我的博客:罗摩尔-Ramostear 获取更多信息

本文转载自: 掘金

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

搞懂Java线程池

发表于 2019-04-20

身为程序员我们对线程是再熟悉不过了,多线程并发算是Java进阶的知识,用好多线程不容易有太多的坑。创建线程也算是一个”重”操作。创建线程的语句是new Thread()咋一看好像就是new了一个对象。

没错是new了个对象,但是不仅仅是普通对象那样在堆中分配了一块内存,它还需要调用操作系统内核API,然后操作系统再为线程分配一些资源。所以较普通对象,线程就比较“重了”。所以我们要避免频繁的创建和销毁线程,还得控制一下线程的数量。线程池就是用来完成这一项使命的。

所以多线程就离不开线程池,所以要掌握多线程编程,线程池的了解必不可少。
线程池的设计就是采用生产者-消费者模式,线程池里面的线程是消费者,我们塞给线程池的任务是生产者。可以理解成线程池就是火车站售票厅,线程池里面的线程就是火车站售票厅窗口员工,我们去买票或者退票改签就是给窗口员工任务也就是生产,然后窗口员工帮我们办理业务,也就是消费。
一般我们是用ThreadPoolExecutor来创建线程池,我找了里面参数最多的构造器。

1、corePoolSize

按字面翻译过来就是核心池大小,其实就是线程池保有的最小的线程数,这里需要注意一下,初始化线程池的时候,除非调用prestartAllCoreThreads或者prestartCoreThread这两个方法,这两个方法分别是在无任务到来之前预创建所有核心线程或者创建一个线程。否则线程池初始化后没任务进来前是没有线程的。只有当任务来了才会创建线程。

所以这里保有的核心数指的是,当线程池创建了这么多的线程之后,会保留的不会被回收的线程数,超过corePoolSize的线程在一定时间之后就会被回收。

但是java1.6新增了一个allowCoreThreadTimeOut(boolean value)方法,当设为true时候,所有的线程都会超时回收,包括核心线程。

2、maximumPoolSize

最大线程数,也就是池里面能有的最大的线程数量。也就是火车站售票厅窗口所有的窗口都有员工在服务。特别是在节假日的时候,基本上窗口都会开放。

3、keepAliveTime、TimeUnit

keepAliveTime就是存活时间,TimeUnit是时间单位,来表明keepAliveTime的数字是秒啊还是毫秒啊等等。
这两个参数就是当我们线程池存在的线程数量超过corePoolSize时,如果有个线程已经空闲了keepAliveTime这么长的时间,那么这个空闲线程就要被回收了,就类似于出行高峰期过去了,售票厅窗口可以关闭几个了。总不能都没人了还开这么多窗口把,浪费呀。

4、workQueue

工作队列,是阻塞队列。队列存储的也就是线程需要执行的Runnable,也就是任务。对应着就是去售票厅排队的我们。

5、threadFactory

按名字翻译过来就是线程工厂了,也就是我们可以搞个工厂,然后自定义如何创建线程,比如给线程set下名字啊等。然后线程池就会按照工厂定义的方式创建线程。就是如果不设定线程的名字的话,线程名可能就是什么thread-1这样的,对于我们排查问题不太方便,所以给个名字来标识一下比较好。

6、handler

这个是拒绝策略,也就是当线程池中所有的线程都在执行任务,并且工作队列(是有界队列)也排满了,那再有任务提交就会执行拒绝策略。ThreadPoolExecutor提供了四种拒绝策略
1、ThreadPoolExecutor.AbortPolicy()
是默认的拒绝策略,会抛出 RejectedExcecutionException。
2、ThreadPoolExecutor.CallerRunsPolicy()
让提交任务的线程自己去执行这个任务。。好像这样做挺有道理的..我没空你自己搞去
3、ThreadPoolExecutor.DiscardOldestPolicy()
丢弃最老的任务,也就是工作队列里最前面的任务,丢弃了之后把新任务加入到工作队列中…真的不公平啊
4、ThreadPoolExecutor.DiscardPolicy()
直接丢弃任务,并且不抛出任何异常…假装没看到系列

除了这四种还可以自定义拒绝策略,建议自定义拒绝策略。因为更加的友好,可以设置成服务降级啊等操作。

注意

Java并发包还提供了Executors,可以快速创建线程池,但是不推荐使用Executors。因为Executors创建线程池都是默认使用无界队列LinkedBlockingQueue,在高负载的情况下容易OOM。所以建议使用有界队列。

阿里巴巴Java开发手册

总结

所以线程池就是生产者-消费者模型的实现,线程池约束了线程的数量,也避免频繁的创建和销毁线程。工作队列得存在使得任务有序的进行,完美!


如有错误欢迎指正!
个人公众号:yes的练级攻略

本文转载自: 掘金

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

Java 函数式接口 lambda 应用

发表于 2019-04-20

函数式接口

理解Functional Interface(函数式接口,以下简称FI)是学习Java8 Lambda表达式的关键所在,所以放在最开始讨论。FI的定义其实很简单:任何接口,如果只包含唯一一个抽象方法,那么它就是一个FI。为了让编译器帮助我们确保一个接口满足FI的要求(也就是说有且仅有一个抽象方法),Java8提供了@FunctionalInterface注解。举个简单的例子,Runnable接口就是一个FI,下面是它的源代码:

1
2
3
4
5
6
7
8
复制代码@Functional Interface
public interface Runnable{
public abstract void run ();
}

public interface Runnable {
public abstract void run ();
}

ps: 上述两种都是函数式接口,在Java 8 提供新的注解Function Interface 声明一个接口为函数式接口,声明之后这个接口必须符合函数式接口的规范。@FunctionalInterface 对于接口是不是函数式接口没有影响,但该注解知识提醒编译器去检查该接口是否仅包含一个抽象方法。

下面的用法就是错误:

1
2
3
4
复制代码@Function Interface
public interface test{
public void test1();
public void test2();

ps: 接口中声明的方法默认是抽象的,使用注解后,编译器自动检查发现存在两个抽象方法,会报错。

函数式接口的规范

  1. 函数式接口里是可以包含默认方法,因为默认方法不是抽象方法,其有一个默认实现,所以是符合函数式接口的定义的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    @FunctionalInterface
interface GreetingService
{
void sayMessage(String message);

default void doSomeMoreWork1()
{
// Method body
}

default void doSomeMoreWork2()
{
// Method body
}
}
  1. 函数式接口里是可以包含静态方法,因为静态方法不能是抽象方法,是一个已经实现了的方法,所以是符合函数式接口的定义的;
1
2
3
4
5
6
7
8
复制代码    @FunctionalInterface
interface GreetingService
{
public void sayMessage(String message);
public static void printHello(){
System.out.println("Hello");
}
}
  1. 函数式接口里是可以包含Object里的public方法,这些方法对于函数式接口来说,不被当成是抽象方法(虽然它们是抽象方法);因为任何一个函数式接口的实现,默认都继承了Object类,包含了来自java.lang.Object里对这些抽象方法的实现。
1
2
3
4
5
6
7
8
复制代码    @FunctionalInterface
interface GreetingService
{
void sayMessage(String message);

@Override
boolean equals(Object obj);
}

Java 内部类

为什么讲内部类,因为在 lamada 表达式又与 Java 内部类应用存在相似之处,特别是匿名内部类。在学习这部分前专门又复习了一遍内部类的概念。

分类

成员内部类

局部内部类

静态内部类

匿名内部类

成员内部类

  1. 定义成员内部类后在创建该内部类的对象是不同于普通类的,成员内部类是其外部类的属性。因此在创建时必须首先创建其外部类对象,再创建内部类的对象。内部类 对象名 = 外部类对象.new 内部类( );
  2. 外部类是不能直接使用内部类的成员和方法滴,可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法。
  3. 可先创建内部类的对象,然后通过内部类的对象来访问其成员变量和方法。
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
复制代码public class Outer {

private static int i = 1;
private int j = 10;
private int k = 20;


public static void outerF1() {
}

/**
* 外部类的静态方法访问成员内部类,与在外部类外部访问成员内部类一样
*/
public static void outerF4() {
//step1 建立外部类对象
Outer out = new Outer();
//step2 根据外部类对象建立内部类对象
Inner inner = out.new Inner();
//step3 访问内部类的方法
inner.innerF1();
}

public static void main(String[] args) {

/*
* outerF4();该语句的输出结果和下面三条语句的输出结果一样
*如果要直接创建内部类的对象,不能想当然地认为只需加上外围类Outer的名字,
*就可以按照通常的样子生成内部类的对象,而是必须使用此外围类的一个对象来
*创建其内部类的一个对象:
*Outer.Inner outin = out.new Inner()
*因此,除非你已经有了外围类的一个对象,否则不可能生成内部类的对象。因为此
*内部类的对象会悄悄地链接到创建它的外围类的对象。如果你用的是静态的内部类,
*那就不需要对其外围类对象的引用。
*/
Outer out = new Outer();
Outer.Inner outin = out.new Inner();
outin.innerF1();
}

public void outerF2() {
}

/**
* 外部类的非静态方法访问成员内部类
*/
public void outerF3() {
Inner inner = new Inner();
inner.innerF1();
}

/**
* 成员内部类中,不能定义静态成员
* 成员内部类中,可以访问外部类的所有成员
*/
class Inner {
// static int innerI = 100;内部类中不允许定义静态变量
// 内部类和外部类的实例变量可以共存
int j = 100;
int innerI = 1;


void innerF1() {
System.out.println(i);
//在内部类中访问内部类自己的变量直接用变量名
System.out.println(j);
//在内部类中访问内部类自己的变量也可以用this.变量名
System.out.println(this.j);
//在内部类中访问外部类中与内部类同名的实例变量用外部类名.this.变量名
System.out.println(Outer.this.j);
//如果内部类中没有与外部类同名的变量,则可以直接用变量名访问外部类变量
System.out.println(k);
outerF1();
outerF2();
}
}
}
  1. 局部内部类

在方法中定义的内部类称为局部内部类。与局部变量类似,局部内部类不能有访问说明符,因为它不是外围类的一部分,但是它可以访问当前代码块内的常量,和此外围类所有的成员。

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
复制代码public class Outer {

private int s = 100;
private int outI = 1;

public static void main(String[] args) {
// 访问局部内部类必须先有外部类对象
Outer out = new Outer();
out.f(3);
}

public void f(final int k) {
final int s = 200;
int i = 1;
final int j = 10;


/**
* 定义在方法内部
*/
class Inner {
// 可以定义与外部类同名的变量
int s = 300;
int innerI = 100;

// static int m = 20; 不可以定义静态变量
Inner(int k) {
innerF(k);
}
void innerF(int k) {
// java如果内部类没有与外部类同名的变量,在内部类中可以直接访问外部类的实例变量
System.out.println(outI);
// 可以访问外部类的局部变量(即方法内的变量),但是变量必须是final的
System.out.println(j);
//System.out.println(i);
// 如果内部类中有与外部类同名的变量,直接用变量名访问的是内部类的变量
System.out.println(s);
// 用this.变量名访问的也是内部类变量
System.out.println(this.s);
// 用外部类名.this.内部类变量名访问的是外部类变量
System.out.println(Outer.this.s);
}
}
new Inner(k);
}
}
  1. 静态内部类(嵌套类)

如果你不需要内部类对象与其外围类对象之间有联系,那你可以将内部类声明为static。这通常称为嵌套类(nested class)。想要理解static应用于内部类时的含义,你就必须记住,普通的内部类对象隐含地保存了一个引用,指向创建它的外围类对象。然而,当内部类是static的时,就不是这样了。
要创建嵌套类的对象,并不需要其外围类的对象。
不能从嵌套类的对象中访问非静态的外围类对象。

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
复制代码public class Outer {
private static int i = 1;
private int j = 10;

public static void outerF1() {
}

public static void main(String[] args) {
new Outer().outerF3();
}

public void outerF2() {
}

public void outerF3() {
// 外部类访问内部类的静态成员:内部类.静态成员
System.out.println(Inner.inner_i);
Inner.innerF1();
// 外部类访问内部类的非静态成员:实例化内部类即可
Inner inner = new Inner();
inner.innerF2();
}

/**
* 静态内部类可以用public,protected,private修饰
* 静态内部类中可以定义静态或者非静态的成员
*/
static class Inner {
static int inner_i = 100;
int innerJ = 200;

static void innerF1() {
// 静态内部类只能访问外部类的静态成员(包括静态变量和静态方法)
System.out.println("Outer.i" + i);
outerF1();
}


void innerF2() {
// 静态内部类不能访问外部类的非静态成员(包括非静态变量和非静态方法)
// System.out.println("Outer.i"+j);
// outerF2();
}
}
}

匿名内部类

这是我们今天的主角,匿名内部类, 字面意思没有名字的类 {}。匿名内部作为最特殊的内部类,需要讲解的内容。(think in java)

为什么使用匿名内部类:

  1. 只用到类的一个实例
  2. 类在定义后马上用到
  3. 类非常小(SUN推荐是在4行代码以下)
  4. 给类命名并不会导致你的代码更容易被理解

在使用匿名内部类时,要记住以下几个原则:

  1. 匿名内部类一般不能有构造方法。
  2. 匿名内部类不能定义任何静态成员、方法和类。
  3. 匿名内部类不能是public,protected,private,static。
  4. 只能创建匿名内部类的一个实例。
  5. 一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。
  6. 因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。

你可能见过如下的代码:

1
2
3
4
5
6
7
复制代码    List<Integer> var1 = new ArrayList<Integer>()
{
{
add(1);
add(2);
}
};

就是用到匿名类的语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// 在方法中返回一个匿名内部类
public class Parcel6 {
public static void main(String[] args) {
Parcel6 p = new Parcel6();
Contents c = p.cont();
}

public Contents cont() {
return new Contents() {
private int i = 11;


public int value() {
return i;
}
}; // 在这里需要一个分号
}
}

cont()方法将下面两个动作合并在一起:返回值的生成,与表示这个返回值的类的定义。
return new Contents()
但是,在到达语句结束的分号之前,你却说:“等一等,我想在这里插入一个类的定义”:

这种奇怪的语法指的是:“创建一个继承自Contents的匿名类的对象。”通过new 表达式返回的引用被自动向上转型为对Contents的引用。匿名内部类的语法是下面例子的简略形式:

1
2
3
4
5
6
7
8
复制代码class MyContents implements Contents {
private int i = 11;

public int value() {
return i;
}
}
return new MyContents();

上述这类写法是最常见。

在Java中,通常就是编写另外一个类或类库的人规定一个接口,然后你来实现这个接口,然后把这个接口的一个对象作为参数传给别人的程序,别人的程序必要时就会通过那个接口来调用你编写的函数,执行后续的一些方法。

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
复制代码public class CallBack {

public static void main(String[] args) {
CallBack callBack = new CallBack();
callBack.toDoSomethings(100, new CallBackInterface() {
public void execute() {
System.out.println("我的请求处理成功了");
}
});

}

public void toDoSomethings(int a, CallBackInterface callBackInterface) {
long start = System.currentTimeMillis();
if (a > 100) {
callBackInterface.execute();
} else {
System.out.println("a < 100 不需要执行回调方法");
}
long end = System.currentTimeMillis();
System.out.println("该接口回调时间 : " + (end - start));
}
}
public interface CallBackInterface {

void execute();
}

Java里的回调,可以说是匿名内部类精彩表演,优美的编码风格,真是让人陶醉~ this is so amazing 。

经过上述的铺垫引出下面的主角 lamada 表达式实现函数式接口。

Lambda语法糖

为了能够方便、快捷、幽雅的创建出FI的实例,Java8提供了Lambda表达式这颗语法糖。下面我用一个例子来介绍Lambda语法。假设我们想对一个List按字符串长度进行排序,那么在Java8之前,可以借助匿名内部类来实现:

1
2
3
4
5
6
7
8
9
复制代码List<String> words = Arrays.asList("apple", "banana", "pear");
words.sort(new Comparator<String>() {

@Override
public int compare(String w1, String w2) {
return Integer.compare(w1.length(), w2.length());
}

});

上面的匿名内部类简直可以用丑陋来形容,唯一的一行逻辑被五行垃圾代码淹没。根据前面的定义(并查看Java源代码)可知,Comparator是个FI,所以,可以用Lambda表达式来实现:

1
2
3
复制代码words.sort((String w1, String w2) -> {
return Integer.compare(w1.length(), w2.length());
});

ps: 看起来像一个匿名的方法,实际就是一个匿名类对象的引用,代码看起来更加简洁。可以认为 lambda表达式实现了接口的抽象方法,因为函数式接口默认只有一个抽象方法。

参考文献:

函数式接口概念

详解内部类

本文转载自: 掘金

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

Axios 源码解析

发表于 2019-04-16

Axios 是什么?

Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
今天我们来进入 Axios 源码解析

Axios 功能

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

希望通过源码来慢慢理清这些功能的实现原理

Axios 使用

执行 GET 请求

1
2
3
4
javascript复制代码axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})

执行 POST 请求

1
2
3
4
5
6
7
javascript复制代码axios.post('/user', {
name: 'zxm',
age: 18,
})
.then(function (response) {
console.log(response);
})

使用方式不是本次主题的重点,具体使用方式可以参照 Axios 中文说明

源码拉下来直接进入 lib 文件夹开始解读源码

源码解读

lib/ axios.js 开始

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
javascript复制代码'use strict';

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

// 重点 createInstance 方法
// 先眼熟一个代码 下面讲完工具函数会再具体来讲解 createInstance
function createInstance(defaultConfig) {
// 实例化 Axios
var context = new Axios(defaultConfig);
// 自定义 bind 方法 返回一个函数()=> {Axios.prototype.request.apply(context,args)}
var instance = bind(Axios.prototype.request, context);
// Axios 源码的工具类
utils.extend(instance, Axios.prototype, context);

utils.extend(instance, context);

return instance;
}
// 传入一个默认配置 defaults 配置先不管,后面会有具体的细节
var axios = createInstance(defaults);


// 下面都是为 axios 实例化的对象增加不同的方法。
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
module.exports = axios;
module.exports.default = axios;

lib/ util.js 工具方法

有如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
javascript复制代码module.exports = {
isArray: isArray,
isArrayBuffer: isArrayBuffer,
isBuffer: isBuffer,
isFormData: isFormData,
isArrayBufferView: isArrayBufferView,
isString: isString,
isNumber: isNumber,
isObject: isObject,
isUndefined: isUndefined,
isDate: isDate,
isFile: isFile,
isBlob: isBlob,
isFunction: isFunction,
isStream: isStream,
isURLSearchParams: isURLSearchParams,
isStandardBrowserEnv: isStandardBrowserEnv,
forEach: forEach,
merge: merge,
deepMerge: deepMerge,
extend: extend,
trim: trim
};

is开头的isXxx方法名 都是判断是否是 Xxx 类型 ,这里就不做明说 主要是看下 后面几个方法

extend 将 b 里面的属性和方法继承给 a , 并且将 b 里面的方法的执行上个下文都绑定到 thisArg

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// a, b,thisArg 参数都为一个对象
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
// 如果指定了 thisArg 那么绑定执行上下文到 thisArg
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}

抽象的话看个例子

这样是不是就一目了然。fn2 函数没有拿自己对象内的 age = 20 而是被指定到了 thisArg 中的 age

自定义 forEach 方法遍历基本数据,数组,对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
javascript复制代码function forEach(obj, fn) {
if (obj === null || typeof obj === 'undefined') {
return;
}
if (typeof obj !== 'object') {
obj = [obj];
}
if (isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}

merge 合并对象的属性,相同属性后面的替换前的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (typeof result[key] === 'object' && typeof val === 'object') {
result[key] = merge(result[key], val);
} else {
result[key] = val;
}
}

for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}

如下图所示:

bind -> lib/ helpers/ bind.js 这个很清楚,返回一个函数,并且传入的方法执行上下文绑定到 thisArg上。

1
2
3
4
5
6
7
8
9
javascript复制代码module.exports = function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
};

好勒那么 axios/util 方法我们就基本没有问题拉

看完这些工具类方法后我们在回过头看之前的 createInstance 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码function createInstance(defaultConfig) {
// 实例化 Axios, Axios下面会讲到
var context = new Axios(defaultConfig);

// 将 Axios.prototype.request 的执行上下文绑定到 context
// bind 方法返回的是一个函数
var instance = bind(Axios.prototype.request, context);

// 将 Axios.prototype 上的所有方法的执行上下文绑定到 context , 并且继承给 instance
utils.extend(instance, Axios.prototype, context);

// 将 context 继承给 instance
utils.extend(instance, context);

return instance;
}
// 传入一个默认配置
var axios = createInstance(defaults);

总结:createInstance 函数返回了一个函数 instance.

  1. instance 是一个函数 Axios.prototype.request 且执行上下文绑定到 context。
  2. instance 里面还有 Axios.prototype 上面的所有方法,并且这些方法的执行上下文也绑定到 context。
  3. instance 里面还有 context 上的方法。

Axios 实例源码

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
javascript复制代码'use strict';
var utils = require('./../utils');
var buildURL = require('../helpers/buildURL');
var InterceptorManager = require('./InterceptorManager');
var dispatchRequest = require('./dispatchRequest');
var mergeConfig = require('./mergeConfig');

function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}

// 核心方法 request
Axios.prototype.request = function request(config) {
// ... 单独讲
};

// 合并配置将用户的配置 和默认的配置合并
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 这个就是给 Axios.prototype 上面增加 delete,get,head,options 方法
// 这样我们就可以使用 axios.get(), axios.post() 等等方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
Axios.prototype[method] = function(url, config) {
// 都是调用了 this.request 方法
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});

module.exports = Axios;

上面的所有的方法都是通过调用了 this.request 方法

那么我们就来看这个 request 方法,个人认为是源码内的精华也是比较难理解的部分,使用到了 Promise 的链式调用,也使用到了中间件的思想。

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
javascript复制代码function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Axios.prototype.request = function request(config) {
// Allow for axios('example/url'[, config]) a la fetch API
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 合并配置
config = mergeConfig(this.defaults, config);
// 请求方式,没有默认为 get
config.method = config.method ? config.method.toLowerCase() : 'get';

// 重点 这个就是拦截器的中间件
var chain = [dispatchRequest, undefined];
// 生成一个 promise 对象
var promise = Promise.resolve(config);

// 将请求前方法置入 chain 数组的前面 一次置入两个 成功的,失败的
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 将请求后的方法置入 chain 数组的后面 一次置入两个 成功的,失败的
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

// 通过 shift 方法把第一个元素从其中删除,并返回第一个元素。
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

看到这里有点抽象,没关系。我们先讲下拦截器。在请求或响应被 then 或 catch 处理前拦截它们。使用方法参考 Axios 中文说明 ,大致使用如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javascript复制代码// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

通过 promise 链式调用一个一个函数,这个函数就是 chain 数组里面的方法

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// 初始的 chain 数组 dispatchRequest 是发送请求的方法
var chain = [dispatchRequest, undefined];

// 然后 遍历 interceptors
// 注意 这里的 forEach 不是 Array.forEach, 也不是上面讲到的 util.forEach. 具体 拦截器源码 会讲到
// 现在我们只要理解他是遍历给 chain 里面追加两个方法就可以
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

然后添加了请求拦截器和相应拦截器 chain 会是什么样子呢 (重点)

1
ini复制代码chain = [ 请求拦截器的成功方法,请求拦截器的失败方法,dispatchRequest, undefined, 响应拦截器的成功方法,响应拦截器的失败方法 ]。

好了,知道具体使用使用之后是什么样子呢?回过头去看 request 方法

每次请求的时候我们有一个

1
2
3
4
5
6
7
javascript复制代码 while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
意思就是将 chainn 内的方法两两拿出来执行 成如下这样
promise.then(请求拦截器的成功方法, 请求拦截器的失败方法)
.then(dispatchRequest, undefined)
.then(响应拦截器的成功方法, 响应拦截器的失败方法)

现在看是不是清楚了很多,拦截器的原理。现在我们再来看 InterceptorManager 的源码

InterceptorManager 拦截器源码

lib/ core/ InterceptorManager.js

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
javascript复制代码'use strict';
var utils = require('./../utils');

function InterceptorManager() {
// 存放方法的数组
this.handlers = [];
}
// 通过 use 方法来添加拦截方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// 通过 eject 方法来删除拦截方法
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
// 添加一个 forEach 方法,这就是上述说的 forEach
InterceptorManager.prototype.forEach = function forEach(fn) {
// 里面还是依旧使用了 utils 的 forEach, 不要纠结这些 forEach 的具体代码
// 明白他们干了什么就可以
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};

module.exports = InterceptorManager;

dispatchRequest 源码

lib/ core/ dispatchRequest .js

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
javascript复制代码'use strict';
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 请求取消时候的方法,暂不看
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
// 请求没有取消 执行下面的请求
if (config.baseURL && !isAbsoluteURL(config.url)) {
config.url = combineURLs(config.baseURL, config.url);
}
config.headers = config.headers || {};
// 转换数据
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 合并配置
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers || {}
);
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
// 这里是重点, 获取请求的方式,下面会讲到
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// 拿到了请求的数据, 转换 data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
// 失败处理
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}

return Promise.reject(reason);
});
};

看了这么多,我们还没看到是通过什么来发送请求的,现在我们看看在最开始实例化 createInstance 方法中我们传入的 defaults 是什么

var axios = createInstance(defaults);

lib/ defaults.js

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
javascript复制代码'use strict';

var utils = require('./utils');
var normalizeHeaderName = require('./helpers/normalizeHeaderName');

var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
headers['Content-Type'] = value;
}
}
// getDefaultAdapter 方法是来获取请求的方式
function getDefaultAdapter() {
var adapter;
// process 是 node 环境的全局变量
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// 如果是 node 环境那么久通过 node http 的请求方法
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// 如果是浏览器啥的 有 XMLHttpRequest 的就用 XMLHttpRequest
adapter = require('./adapters/xhr');
}
return adapter;
}

var defaults = {
// adapter 就是请求的方法
adapter: getDefaultAdapter(),
// 下面一些请求头,转换数据,请求,详情的数据
// 这也就是为什么我们可以直接拿到请求的数据时一个对象,如果用 ajax 我们拿到的都是 jSON 格式的字符串
// 然后每次都通过 JSON.stringify(data)来处理结果。
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],

transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],

/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,

xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',

maxContentLength: -1,

validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};

defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

module.exports = defaults;

总结

  1. Axios 的源码走读一遍确实可以看到和学习到很多的东西。
  2. Axios 还有一些功能:请求的取消,请求超时的处理。这里我没有全部说明。
  3. Axios 通过在请求中添加 toke 并验证方法,让客户端支持防御 XSRF Django CSRF 原理分析

最后

如果看的还不是很明白,不用担心,这基本上是我表达,书写的不够好。因为在写篇文章时我也曾反复的删除,重写,总觉得表达的不够清楚。为了加强理解和学习大家可以去 github 将代码拉下来对照着来看。这样在对 Axios 源码解析时会更加清晰

git clone https://github.com/axios/axios.git

全文章,如有错误或不严谨的地方,请务必给予指正,谢谢!

参考:

  • Axios 中文说明,很详细的教程
  • Axios 源码 github
  • 推荐 Axios源码深度剖析

本文转载自: 掘金

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

Java 核心知识点整理

发表于 2019-04-16

听说你在面试

又到了求职的金三银四的黄金月份,我相信有不少小伙伴已经摩拳擦掌的准备寻找下一份工作。

就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句,复习准备的是否充分,将直接影响你入职的成功率。

但很多小伙伴却苦于没有合适的资料来回顾整个 Java 知识体系,或者有的小伙伴可能都不知道该从哪里开始复习。

我偶然从一个网友群中发现了整理的这份资料,不论是从整个 Java 知识体系,还是从面试的角度来看,都是一份含技术量很高的资料。

也不知道这位作者是谁,里面的内容也大多整理来自于互联网,但很明显的是这位作者为了整理这份资料用了很多心,在此表示感谢。

注,后来联系上了作者是美团的一位大佬,再次表示感谢!

我随后截了几张图,大家可以仔细查看左边的菜单栏,覆盖的知识面真的很广,而且质量都很不错。

说实话,作为一名 Java 程序员,不论你需不需要面试都应该好好看下这份资料。我大概撸了一边,真的是堪称典范。

那么如何获取这份资料呢?关注下方公众号回复:java,即可获取。

另外关注后,回复:java ,获取超过10000+人领取的 Java 知识体系/面试必看资料。

本文转载自: 掘金

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

SpringBoot + Spring Security 学

发表于 2019-04-14

记住我功能的基本原理

当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的记住我功能,实际是调用了RememberMeService接口,其接口的实现类会将用户的信息生成Token并将它写入 response 的Cookie中,在写入的同时,内部的TokenRepositoryTokenRepository会将这份Token再存入数据库一份。

当用户再次访问服务器资源的时候,首先会经过RememberMeAuthenticationFiler过滤器,在这个过滤器里面会读取当前请求中携带的 Cookie,这里存着上次服务器保存 的Token,然后去数据库中查找是否有相应的 Token,如果有,则再通过UserDetailsService获取用户的信息。

记住我功能的过滤器

从图中可以得知记住我的过滤器在过滤链的中部,注意是在UsernamePasswordAuthenticationFilter之后。

前端页面checkbox设置

在 html 中增加记住我复选框checkbox控件,注意其中复选框的name 一定必须为remember-me

1
复制代码<input type="checkbox" name="remember-me" value="true"/>

配置cookie存储数据库源

本例中使用了 springboot 管理的数据库源,所以注意要配置spring-boot-starter-jdbc的依赖:

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

如果不配置会报编译异常:

1
复制代码The type org.springframework.jdbc.core.support.JdbcDaoSupport cannot be resolved. It is indirectly referenced from required .class files

记住我的安全认证配置:

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
复制代码@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;

@Override
protected void configure(HttpSecurity http) throws Exception {
// 将自定义的验证码过滤器放置在 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login") // 设置登录页面
.loginProcessingUrl("/user/login") // 自定义的登录接口
.successHandler(myAuthenctiationSuccessHandler)
.failureHandler(myAuthenctiationFailureHandler)
.defaultSuccessUrl("/home").permitAll() // 登录成功之后,默认跳转的页面
.and().authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/", "/index", "/user/login", "/code/image").permitAll() // 设置所有人都可以访问登录页面
.anyRequest().authenticated() // 任何请求,登录后可以访问
.and().csrf().disable() // 关闭csrf防护
.rememberMe() // 记住我配置
.tokenRepository(persistentTokenRepository()) // 配置数据库源
.tokenValiditySeconds(3600)
.userDetailsService(userDetailsService);
}

@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl persistentTokenRepository = new JdbcTokenRepositoryImpl();
// 将 DataSource 设置到 PersistentTokenRepository
persistentTokenRepository.setDataSource(dataSource);
// 第一次启动的时候自动建表(可以不用这句话,自己手动建表,源码中有语句的)
// persistentTokenRepository.setCreateTableOnStartup(true);
return persistentTokenRepository;
}
}

注意:在数据库源配置之前,建议手动在数据库中新增一张保存的cookie表,其数据库脚本在JdbcTokenRepositoryImpl的静态属性中配置了:

1
2
3
4
5
6
复制代码public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
PersistentTokenRepository {
/** Default SQL for creating the database table to store the tokens */
public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
+ "token varchar(64) not null, last_used timestamp not null)";
}

因此可以事先执行以下sql 脚本创建表:

1
复制代码create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);

当然,JdbcTokenRepositoryImpl自身还有一个setCreateTableOnStartup()方法进行开启自动建表操作,但是不建议使用。

当成功登录之后,RememberMeService会将成功登录请求的cookie存储到配置的数据库中:

源码分析

首次请求

首先进入到AbstractAuthenticationProcessingFilter过滤器中的doFilter()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码public abstract class AbstractAuthenticationProcessingFilter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

……

try {
authResult = attemptAuthentication(request, response);
……
}
catch (InternalAuthenticationServiceException failed) {
……
}

successfulAuthentication(request, response, chain, authResult);
}
}

其中当用户认证成功之后,会进入successfulAuthentication()方法,在用户信息被保存在了SecurityContextHolder之后,其中就调用了rememberMeServices.loginSuccess():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {

……

SecurityContextHolder.getContext().setAuthentication(authResult);

// 调用记住我服务接口的登录成功方法
rememberMeServices.loginSuccess(request, response, authResult);

// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}

successHandler.onAuthenticationSuccess(request, response, authResult);
}

在这个RememberMeServices有个抽象实现类,在抽象实现类loginSuccess()方法中进行了记住我功能判断,为什么前端的复选框控件的 name 必须为remember-me,原因就在此:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码public abstract class AbstractRememberMeServices implements RememberMeServices,
InitializingBean, LogoutHandler {

public static final String DEFAULT_PARAMETER = "remember-me";

private String parameter = DEFAULT_PARAMETER;

@Override
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {

if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}

onLoginSuccess(request, response, successfulAuthentication);
}
}

当识别到记住我功能开启的时候,就会进入onLoginSuccess()方法,其具体的方法实现在PersistentTokenBasedRememberMeServices类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {

protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();

logger.debug("Creating new persistent login for user " + username);

PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
// 保存cookie到数据库
tokenRepository.createNewToken(persistentToken);
// 将cookie回写一份到响应中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
}

上面的tokenRepository.createNewToken()和addCookie()就将 cookie 保存到数据库并回显到响应中。

第二次请求

当第二次请求传到服务器的时候,请求会被RememberMeAuthenticationFilter过滤器进行过滤:过滤器首先判定之前的过滤器都没有认证通过当前用户,也就是SecurityContextHolder中没有已经认证的信息,所以会调用rememberMeServices.autoLogin()的自动登录接口拿到已通过认证的rememberMeAuth进行用户认证登录:

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
复制代码public class RememberMeAuthenticationFilter extends GenericFilterBean implements
ApplicationEventPublisherAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

// SecurityContextHolder 不存在已经认证的 authentication,表示前面的过滤器没有做过任何身份认证
if (SecurityContextHolder.getContext().getAuthentication() == null) {
// 调用自动登录接口
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);

if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

onSuccessfulAuthentication(request, response, rememberMeAuth);

……

}
catch (AuthenticationException authenticationException) {
……
}
}

chain.doFilter(request, response);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'");
}

chain.doFilter(request, response);
}
}
}

这个自动登录的接口,又由其抽象实现类进行实现:

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
复制代码public abstract class AbstractRememberMeServices implements RememberMeServices,
InitializingBean, LogoutHandler {
@Override
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
// 从请求中获取cookie
String rememberMeCookie = extractRememberMeCookie(request);

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

logger.debug("Remember-me cookie detected");

if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}

UserDetails user = null;

try {
// 解码请求中的cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
// 根据 cookie 找到用户认证
user = processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);

logger.debug("Remember-me cookie accepted");

return createSuccessfulAuthentication(request, user);
}
catch (CookieTheftException cte) {
……
}

cancelCookie(request, response);
return null;
}
}

processAutoLoginCookie()的具体实现还是由PersistentTokenBasedRememberMeServices来实现,总得来说就是一顿判定当前的cookieTokens是不是在数据库中存在tokenRepository.getTokenForSeries(presentedSeries),并判断是不是一样的,如果一样,就是把当前请求的新 token 更新保存到数据库,最后通过当前请求token中的用户名调用UserDetailsService.loadUserByUsername()进行用户认证。

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
复制代码public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
protected UserDetails processAutoLoginCookie(String[] cookieTokens,
HttpServletRequest request, HttpServletResponse response) {

if (cookieTokens.length != 2) {
throw new InvalidCookieException("Cookie token did not contain " + 2
+ " tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
}

final String presentedSeries = cookieTokens[0];
final String presentedToken = cookieTokens[1];

// 从数据库查询上次保存的token
PersistentRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries);

if (token == null) {
// 查询不到抛异常
throw new RememberMeAuthenticationException(……);
}

// token 不匹配抛出异常
// We have a match for this user/series combination
if (!presentedToken.equals(token.getTokenValue())) {
// Token doesn't match series value. Delete all logins for this user and throw
// an exception to warn them.
tokenRepository.removeUserTokens(token.getUsername());

throw new CookieTheftException(……);
}

// 过期判断
if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
throw new RememberMeAuthenticationException("Remember-me login has expired");
}

PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), generateTokenData(), new Date());

try {
tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
addCookie(newToken, request, response);
}
catch (Exception e) {
……
}

return getUserDetailsService().loadUserByUsername(token.getUsername());
}
}

个人博客:woodwhale’s blog

博客园:木鲸鱼的博客

本文转载自: 掘金

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

死磕 java集合之TreeMap源码分析(一)——红黑树全

发表于 2019-04-13

欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

简介

TreeMap使用红黑树存储元素,可以保证元素按key值的大小进行遍历。

继承体系

TreeMap

TreeMap实现了Map、SortedMap、NavigableMap、Cloneable、Serializable等接口。

SortedMap规定了元素可以按key的大小来遍历,它定义了一些返回部分map的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public interface SortedMap<K,V> extends Map<K,V> {
// key的比较器
Comparator<? super K> comparator();
// 返回fromKey(包含)到toKey(不包含)之间的元素组成的子map
SortedMap<K,V> subMap(K fromKey, K toKey);
// 返回小于toKey(不包含)的子map
SortedMap<K,V> headMap(K toKey);
// 返回大于等于fromKey(包含)的子map
SortedMap<K,V> tailMap(K fromKey);
// 返回最小的key
K firstKey();
// 返回最大的key
K lastKey();
// 返回key集合
Set<K> keySet();
// 返回value集合
Collection<V> values();
// 返回节点集合
Set<Map.Entry<K, V>> entrySet();
}

NavigableMap是对SortedMap的增强,定义了一些返回离目标key最近的元素的方法。

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
复制代码public interface NavigableMap<K,V> extends SortedMap<K,V> {
// 小于给定key的最大节点
Map.Entry<K,V> lowerEntry(K key);
// 小于给定key的最大key
K lowerKey(K key);
// 小于等于给定key的最大节点
Map.Entry<K,V> floorEntry(K key);
// 小于等于给定key的最大key
K floorKey(K key);
// 大于等于给定key的最小节点
Map.Entry<K,V> ceilingEntry(K key);
// 大于等于给定key的最小key
K ceilingKey(K key);
// 大于给定key的最小节点
Map.Entry<K,V> higherEntry(K key);
// 大于给定key的最小key
K higherKey(K key);
// 最小的节点
Map.Entry<K,V> firstEntry();
// 最大的节点
Map.Entry<K,V> lastEntry();
// 弹出最小的节点
Map.Entry<K,V> pollFirstEntry();
// 弹出最大的节点
Map.Entry<K,V> pollLastEntry();
// 返回倒序的map
NavigableMap<K,V> descendingMap();
// 返回有序的key集合
NavigableSet<K> navigableKeySet();
// 返回倒序的key集合
NavigableSet<K> descendingKeySet();
// 返回从fromKey到toKey的子map,是否包含起止元素可以自己决定
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
// 返回小于toKey的子map,是否包含toKey自己决定
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
// 返回大于fromKey的子map,是否包含fromKey自己决定
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
// 等价于subMap(fromKey, true, toKey, false)
SortedMap<K,V> subMap(K fromKey, K toKey);
// 等价于headMap(toKey, false)
SortedMap<K,V> headMap(K toKey);
// 等价于tailMap(fromKey, true)
SortedMap<K,V> tailMap(K fromKey);
}

存储结构

TreeMap-structure

TreeMap只使用到了红黑树,所以它的时间复杂度为O(log n),我们再来回顾一下红黑树的特性。

(1)每个节点或者是黑色,或者是红色。

(2)根节点是黑色。

(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)

(4)如果一个节点是红色的,则它的子节点必须是黑色的。

(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

源码解析

属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码/**
* 比较器,如果没传则key要实现Comparable接口
*/
private final Comparator<? super K> comparator;

/**
* 根节点
*/
private transient Entry<K,V> root;

/**
* 元素个数
*/
private transient int size = 0;

/**
* 修改次数
*/
private transient int modCount = 0;

(1)comparator

按key的大小排序有两种方式,一种是key实现Comparable接口,一种方式通过构造方法传入比较器。

(2)root

根节点,TreeMap没有桶的概念,所有的元素都存储在一颗树中。

Entry内部类

存储节点,典型的红黑树结构。

1
2
3
4
5
6
7
8
复制代码static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
}

构造方法

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
复制代码/**
* 默认构造方法,key必须实现Comparable接口
*/
public TreeMap() {
comparator = null;
}

/**
* 使用传入的comparator比较两个key的大小
*/
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}

/**
* key必须实现Comparable接口,把传入map中的所有元素保存到新的TreeMap中
*/
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}

/**
* 使用传入map的比较器,并把传入map中的所有元素保存到新的TreeMap中
*/
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}

构造方法主要分成两类,一类是使用comparator比较器,一类是key必须实现Comparable接口。

其实,笔者认为这两种比较方式可以合并成一种,当没有传comparator的时候,可以用以下方式来给comparator赋值,这样后续所有的比较操作都可以使用一样的逻辑处理了,而不用每次都检查comparator为空的时候又用Comparable来实现一遍逻辑。

1
2
3
复制代码// 如果comparator为空,则key必须实现Comparable接口,所以这里肯定可以强转
// 这样在构造方法中统一替换掉,后续的逻辑就都一致了
comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);

get(Object key)方法

获取元素,典型的二叉查找树的查找方法。

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
复制代码public V get(Object key) {
// 根据key查找元素
Entry<K,V> p = getEntry(key);
// 找到了返回value值,没找到返回null
return (p==null ? null : p.value);
}

final Entry<K,V> getEntry(Object key) {
// 如果comparator不为空,使用comparator的版本获取元素
if (comparator != null)
return getEntryUsingComparator(key);
// 如果key为空返回空指针异常
if (key == null)
throw new NullPointerException();
// 将key强转为Comparable
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
// 从根元素开始遍历
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
// 如果小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 如果大于0从右子树查找
p = p.right;
else
// 如果相等说明找到了直接返回
return p;
}
// 没找到返回null
return null;
}

final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 从根元素开始遍历
Entry<K,V> p = root;
while (p != null) {
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
// 如果小于0从左子树查找
p = p.left;
else if (cmp > 0)
// 如果大于0从右子树查找
p = p.right;
else
// 如果相等说明找到了直接返回
return p;
}
}
// 没找到返回null
return null;
}

(1)从root遍历整个树;

(2)如果待查找的key比当前遍历的key小,则在其左子树中查找;

(3)如果待查找的key比当前遍历的key大,则在其右子树中查找;

(4)如果待查找的key与当前遍历的key相等,则找到了该元素,直接返回;

(5)从这里可以看出是否有comparator分化成了两个方法,但是内部逻辑一模一样,因此可见笔者comparator = (k1, k2) -> ((Comparable<? super K>)k1).compareTo(k2);这种改造的必要性。


我是一条美丽的分割线,前方高能,请做好准备。


特性再回顾

(1)每个节点或者是黑色,或者是红色。

(2)根节点是黑色。

(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)

(4)如果一个节点是红色的,则它的子节点必须是黑色的。

(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

左旋

左旋,就是以某个节点为支点向左旋转。

left-rotation

整个左旋过程如下:

(1)将 y的左节点 设为 x的右节点,即将 β 设为 x的右节点;

(2)将 x 设为 y的左节点的父节点,即将 β的父节点 设为 x;

(3)将 x的父节点 设为 y的父节点;

(4)如果 x的父节点 为空节点,则将y设置为根节点;如果x是它父节点的左(右)节点,则将y设置为x父节点的左(右)节点;

(5)将 x 设为 y的左节点;

(6)将 x的父节点 设为 y;

让我们来看看TreeMap中的实现:

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
复制代码/**
* 以p为支点进行左旋
* 假设p为图中的x
*/
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// p的右节点,即y
Entry<K,V> r = p.right;

// (1)将 y的左节点 设为 x的右节点
p.right = r.left;

// (2)将 x 设为 y的左节点的父节点(如果y的左节点存在的话)
if (r.left != null)
r.left.parent = p;

// (3)将 x的父节点 设为 y的父节点
r.parent = p.parent;

// (4)...
if (p.parent == null)
// 如果 x的父节点 为空,则将y设置为根节点
root = r;
else if (p.parent.left == p)
// 如果x是它父节点的左节点,则将y设置为x父节点的左节点
p.parent.left = r;
else
// 如果x是它父节点的右节点,则将y设置为x父节点的右节点
p.parent.right = r;

// (5)将 x 设为 y的左节点
r.left = p;

// (6)将 x的父节点 设为 y
p.parent = r;
}
}

右旋

右旋,就是以某个节点为支点向右旋转。

right-rotation

整个右旋过程如下:

(1)将 x的右节点 设为 y的左节点,即 将 β 设为 y的左节点;

(2)将 y 设为 x的右节点的父节点,即 将 β的父节点 设为 y;

(3)将 y的父节点 设为 x的父节点;

(4)如果 y的父节点 是 空节点,则将x设为根节点;如果y是它父节点的左(右)节点,则将x设为y的父节点的左(右)节点;

(5)将 y 设为 x的右节点;

(6)将 y的父节点 设为 x;

让我们来看看TreeMap中的实现:

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
复制代码/**
* 以p为支点进行右旋
* 假设p为图中的y
*/
private void rotateRight(Entry<K,V> p) {
if (p != null) {
// p的左节点,即x
Entry<K,V> l = p.left;

// (1)将 x的右节点 设为 y的左节点
p.left = l.right;

// (2)将 y 设为 x的右节点的父节点(如果x有右节点的话)
if (l.right != null) l.right.parent = p;

// (3)将 y的父节点 设为 x的父节点
l.parent = p.parent;

// (4)...
if (p.parent == null)
// 如果 y的父节点 是 空节点,则将x设为根节点
root = l;
else if (p.parent.right == p)
// 如果y是它父节点的右节点,则将x设为y的父节点的右节点
p.parent.right = l;
else
// 如果y是它父节点的左节点,则将x设为y的父节点的左节点
p.parent.left = l;

// (5)将 y 设为 x的右节点
l.right = p;

// (6)将 y的父节点 设为 x
p.parent = l;
}
}

未完待续,下一节我们一起探讨红黑树插入元素的操作。

现在公众号文章没办法留言了,如果有什么疑问或者建议请直接在公众号给我留言。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

qrcode

本文转载自: 掘金

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

SpringBoot21版本的个人应用开发框架 - 日志自

发表于 2019-04-11

本篇作为SpringBoot2.1版本的个人开发框架 子章节,请先阅读SpringBoot2.1版本的个人开发框架再次阅读本篇文章

后端项目地址:SpringBoot2.1版本的个人应用开发框架

前端项目地址:ywh-vue-admin

日志自定义

在之前的章节我们测试的时候,发现控台台输出的日志是默认的,并且有很多的日志没有打印,并且不能自定义设置我们的想要输出的信息,对于一个应用程序来说日志记录是必不可少的一部分。线上问题追踪,基于日志的业务逻辑统计分析等都离不日志。

对于日志的参考资料网上一搜一大堆,更详细的介绍可以轻松的获得,这里贴出几个参考资料:

  • logback 配置详解
  • 日志组件slf4j介绍及配置详解
  • Java常用日志框架介绍

Java有很多常用的日志框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。

Commons Logging和Slf4j是日志门面,提供一个统一的高层接口,为各种loging API提供一个简单统一的接口。log4j和Logback则是具体的日志实现方案。可以简单的理解为接口与接口的实现,调用者只需要关注接口而无需关注具体的实现,做到解耦。

比较常用的组合使用方式是Slf4j与Logback组合使用,Commons Logging与Log4j组合使用,基于下面的一些优点,选用Slf4j+Logback的日志框架:

更快的执行速度,Logback重写了内部的实现,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了

自动清除旧的日志归档文件,通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量

Logback拥有远比log4j更丰富的过滤能力,可以不用降低日志级别而记录低级别中的日志。

Logback必须配合Slf4j使用。由于Logback和Slf4j是同一个作者,其兼容性不言而喻。

默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台。

配置日志

由上可知我们springboot项目默认使用的就是Logback,我们可以通过设置yml文件的方式来设置日志的格式,也可以通过logback.xml的方式来设置日志管理。

  • springboot官方配置 这里有所有的yml文件配置示例

yml方式: 这种方式相对于xml的方式比较简单,因为你不配置,springboot也会有默认的设置,在application-dev.yml开发环境添加以下配置即可生效,path的路径在开发环境时可以是windows下的路径,当你部署到liunx服务器时需要使用application-prod生产环境的配置文件,文件中配置的路径为liunx的路径即可。

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
复制代码logging:
file:
#存放文件的最大天数
max-history: 15
#存放日志最大size
max-size: 100MB
#存放日志文件位置
path: E:\logs
pattern:
#输出到控制台的格式
console: "YWH - %d{yyyy-MM-dd HH:mm:ss} -%-4r [%t] %-5level %logger{36} - %msg%n"
#日志级别映射,可以指定包下的日志级别 也可指定root为info级别
level:
root: info
com.ywh.core: debug
*************************************************
<!-- %d{HH: mm:ss.SSS}——日志输出时间 -->
<!-- %thread [%t] ——输出日志的进程名字,这在Web应用以及异步任务处理中很有用 -->
<!-- %-4r —— "-"代表了左对齐 将输出从程序启动到创建日志记录的时间 进行左对齐 且最小宽度为4 -->
<!-- %-5level——日志级别,并且使用5个字符靠左对齐 -->
<!-- %logger{36}——日志输出者的名字 -->
<!-- %msg——日志消息 -->
<!-- %n——平台的换行符 -->

<!-- 更多的详情可参考 : https://aub.iteye.com/blog/1103685 此博客最下方有解释

以下图片来自于:aub.iteye.com/blog/110368…

xml方式:这种方式需要配置多个标签,相对与yml方式比较麻烦一点,在resources文件下创建logback-spring.xml文件,如果不想把xml文件直接放在resources下的话,需要在yml文件中配置logging.config= 指定位置

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
复制代码<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。
当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="60 seconds">
<contextName>Y-W-H</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
<property name="log.path" value="E:/logs/" />

<!--0. 日志格式和颜色渲染 -->
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
<conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName ) [%thread] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


<!--1. 输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>

。。。。。。省略代码,具体代码可前往github查看

</configuration>

通过以上两种方式的任意一种配置好以后启动项目以后,就会发现我们已经使用了我们自定义的输出格式来输出日志了,在我们指定下的路径下出现了日志文件。

自定义异常以及全局异常类

当日志级别设置到INFO级别后,只会输出INFO以上的日志,如INFO、WARN、ERROR,这没毛病,问题是,程序中抛出的异常堆栈(运行时异常)都没有打印了,不利于排查问题。

而且,在某些情况下,我们在Service中想直接把异常往Controller抛出不做处理,但我们不能直接把异常信息输出到客户端,这是非常不友好的,而且我们想要精准的定位错误的所在,这就要我们自己来定义异常的输出了,并且把错误的异常以我们之前封装的Result的统一格式返回给前端,所以我们需要自定义异常以及定义全局异常类,我们先定义自定义异常类然后再定义全局异常类。

根据菜鸟教程中的异常信息分类,异常分为三种情况

检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。

错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

而我们所要做的就是继承运行时异常,对此类异常进行自定义处理,在common下exception包中创建MyException类继承RuntimeException。

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
复制代码package com.ywh.common.exception;

/**
* CreateTime: 2018-11-21 19:07
* ClassName: MyXiyiException
* Package: com.ywh.common.exception
* Describe:
* 自定义异常,可以throws的时候用自己的异常类
*
* @author YWH
*/
public class MyException extends RuntimeException {

public MyException(String msg) {
super(msg);
}

public MyException(String message, Throwable throwable) {
super(message, throwable);
}

public MyException(Throwable throwable) {
super(throwable);
}
}

在common下的utils包中创建MyExceptionUtil工具类快速创建异常类

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
复制代码package com.ywh.common.utils;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ywh.common.exception.MyException;

/**
* CreateTime: 2018-12-18 22:32
* ClassName: MyExceptionUtil
* Package: com.ywh.common.utils
* Describe:
* 异常工具类
*
* @author YWH
*/
public class MyExceptionUtil {

public MyExceptionUtil() {
}

public static MyException mxe(String msg, Throwable t, Object... params){
return new MyException(StringUtils.format(msg, params),t);
}

public static MyException mxe(String msg, Object... params){
return new MyException(StringUtils.format(msg, params));
}

public static MyException mxe(Throwable t){
return new MyException(t);
}

}

创建完自定义异常以后我们要对自定义异常进行捕获然后处理,这就需要我们定义全局异常类来进行捕获后进行处理了,在common下exception中添加GlobalExceptionHandler类。

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
复制代码package com.ywh.common.exception;

/**
* @Author: YWH
* @Description: 全局异常处理类,拦截controller RestControllerAdvice此注解为ResponseBody和ControllerAdvice混合注解
* @Date: Create in 17:16 2018/11/17
*/
@RestControllerAdvice
public class GlobalExceptionHandler {

/**
*
* 全局异常类中定义的异常都可以被拦截,只是触发条件不一样,如IO异常这种必须抛出异常到
* controller中才可以被拦截,或者在类中用try..catch自己处理
* 绝大部分不需要向上抛出异常即可被拦截,返回前端json数据,如数组下标越界,404 500 400等错误
* 如果自己想要写,按着以下格式增加异常即可
*HttpMessageNotReadableException
*/

private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

/**
* 启动应用后,被 @ExceptionHandler、@InitBinder、@ModelAttribute 注解的方法,
* 都会作用在 被 @RequestMapping 注解的方法上。
* @param binder
*/
@InitBinder
public void initWebBinder(WebDataBinder binder){

}

/**
* 系统错误,未知的错误 已测试
* @param ex 异常信息
* @return 返回前端异常信息
*/
@ExceptionHandler({Exception.class})
public Result exception(Exception ex){
log.error("错误详情:" + ex.getMessage(),ex);
return Result.errorJson(BaseEnum.SYSTEM_ERROR.getMsg(),BaseEnum.SYSTEM_ERROR.getIndex());
}

。。。。。。省略代码,具体代码请前往github查看

/**
* 自定义异常信息拦截
* @param ex 异常信息
* @return 返回前端异常信息
*/
@ExceptionHandler(MyException.class)
public Result myCustomizeException(MyException ex){
log.warn("错误详情:" + ex);
return Result.errorJson(BaseEnum.CUSTOMIZE_EXCEPTION.getMsg(),BaseEnum.CUSTOMIZE_EXCEPTION.getIndex());
}

}

在GlobalExceptionHandler中我们对很多异常进行了拦截后自定义处理,并把我们上边自定义的运行时异常进行拦截,我在类中的方法上都写了注释,并根据网上的资料应该很好理解,我对大部分的异常都做了测试,都是可以进行拦截成功的。

测试示例

我们用postman通过post方式请求一个get的方法,可以看到返回了我们自定义的json格式,并且告诉我们这是因为接口类型所导致的错误,这样我们很快就能定位到错误进行解决。

异常信息拦截

以上错误都是系统替我们捕获并且通过全局异常类进行了拦截之后返回自定义的json格式,而我们的自定义异常如何使用呢,自定义异常需要我们手动捕获异常,并且抛出异常,这样我们的全局异常类才能拦截到。

我们在ExampleServiceImpl中定义一个方法,并在Controller层中调用此方法,用postman调用此接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    /**
* 测试自定义异常
* @return 返回字符串
*/
@Override
public String myException() {
int i = 0;
int a = 10;
if( i > a){
System.out.println("测试!!!");
}else{
throw MyExceptionUtil.mxe("出错了,比他小啊!!");
}
return "没有进行拦截,失败了";
}
1
2
3
4
5
6
7
复制代码    @Autowired
private ExampleService exampleService;

@GetMapping("myExceptionTest")
public Result myExceptionTest(){
return Result.successJson(exampleService.myException());
}

自定义一场拦截

控制台信息

可以看到我们的自定义异常被拦截到并且在控制台中打印了我们想要的信息。

本文转载自: 掘金

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

SpringBoot21版本的个人应用开发框架 - 实现基

发表于 2019-04-11

本篇作为SpringBoot2.1版本的个人开发框架 子章节,请先阅读SpringBoot2.1版本的个人开发框架再次阅读本篇文章

后端项目地址:SpringBoot2.1版本的个人应用开发框架

前端项目地址:ywh-vue-admin

我们实现了代码生成的功能后,对于一个web项目来说,我们还要对返回前端的格式进行一个简单的封装Result,所有返回的类型都是统一的格式,以及我们在自动生成的代码可以继承我们自定义的基础controller等类,便于我们自己扩展。

基础枚举类

在common子模块下的base包下创建基础的BaseEnum枚举类用来定义描述信息维护到枚举里面,尽量不要在代码中直接出现魔法值(如一些编码、中文等,直接常量等),以后的枚举常量类也可以按照这种模式来写。

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
复制代码package com.ywh.common.base;

/**
* CreateTime: 2018-12-15 16:06
* ClassName: BaseEnum
* Package: com.ywh.common.base
* Describe:
* 基础枚举类
*
* @author YWH
*/
public enum BaseEnum {

/**
*后台处理成功
*/
SUCCESS("后台处理成功!",200),

。。。。。。。。省略代码,具体代码请前往github查看

/**
* 404错误拦截
*/
NO_HANDLER("这个页面石沉大海了!接口没找到",404);

private String msg;

private int index;

BaseEnum(String msg, int index) {
this.msg = msg;
this.index = index;
}

public String getMsg() {
return msg;
}

public void setMsg(String msg) {
this.msg = msg;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}
}

封装前端返回json格式

在common下utils包下的创建Result类,作为前端的返回对象,Controller的直接返回对象都是Result。

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
复制代码package com.ywh.common.utils;

import com.ywh.common.base.BaseEnum;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
* CreateTime: 2018-12-15 15:46
* ClassName: Result
* Package: com.ywh.common.utils
* Describe:
* 前端返回json格式封装类
*
* @author YWH
*/
public class Result extends Object implements Serializable{
private static final long serialVersionUID = 1348172152215944560L;

/**
* 返回状态码,200为正确,100为失败
*/
private int code;

/**
* 返回处理信息,成功或者失败
*/
private String msg;

/**
* 成功返回true,失败返回false
*/
private Boolean success;

/**
* 返回给前端的数据
*/
private Map<String, Object> extend = new HashMap<String ,Object>();

/**
* 成功返回的json封装体
* @param value 原始数据
* @return json格式
*/
public static Result successJson(Object value){
Result results = new Result();
results.setCode(BaseEnum.SUCCESS.getIndex());
results.setMsg(BaseEnum.SUCCESS.getMsg());
results.setSuccess(true);
results.getExtend().put("data",value);
return results;
}

/**
* 失败返回的json封装体
* @return json格式
*/
public static Result errorJson(){
Result results = new Result();
results.setCode(BaseEnum.FAIL.getIndex());
results.setSuccess(false);
results.setMsg(BaseEnum.FAIL.getMsg());
return results;
}

。。。。。。省略代码,具体代码请前往github查看。
}

在core的ExampleController中写一个方法,用postman测试一下我们的前端结构体

1
2
3
4
5
6
7
8
9
10
复制代码@RestController
@RequestMapping("ExampleController")
public class ExampleController{

@GetMapping("findAll")
public Result findAll(){
return Result.successJson("成功了");
}

}
1
2
3
4
5
6
7
8
复制代码{
"extend": {
"data": "成功了"
},
"msg": "后台处理成功!",
"code": 200,
"success": true
}

MybatisPlus分页插件

MybatisPlus为我们提供了分页插件,使用也很方便,创建一个配置类即可使用MybatisPlus为我提供的分页查询了,在common下的config包下创建MybatisPlusConfig类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码package com.ywh.common.config;
/**
* CreateTime: 2018-12-18 20:39
* ClassName: MybatisPlusConfig
* Package: com.ywh.common.config
* Describe:
* MybatisPlus的配置类
*
* @author YWH
*/
@EnableTransactionManagement
@Configuration
public class MybatisPlusConfig {

/**
* 分页插件
* @return
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}

}

在common下的Entity中创建BasePage类,用于前端传入参数的接收实体类,我们可以控制当前页和一页中显示几条数据

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
复制代码package com.ywh.common.entity;

import lombok.Data;

/**
* CreateTime: 2018-11-22 10:23
* ClassName: BasePage
* Package: com.ywh.common.entity
* Describe:
* 分页实体类
*
* @author YWH
*/
@Data
public class BasePage {

/**
* 当前每页显示数
*/
private Integer size;

/**
* 当前页数
*/
private Integer current;


}

定义基础Controller、service等

对于我们自动生成的代码我们可以继承我自定义的Controller等等,在common下base包中创建以下类,封装如下:

BaseController、BaseService、BaseServiceImpl、BaseDao: 因为都贴出来太长了,剩下具体代码可前往github查看

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
复制代码package com.ywh.common.base;

/**
* CreateTime: 2018-11-21 9:09
* ClassName: BaseController
* Package: com.ywh.common.base
* Describe:
* 基础Controller 所有的Controller继承这个类,如果有什么通用的方法,可自行扩展
* getList addData addDataBatch updateById updateByColumn deleteByColumn deleteById deleteByIds
* @author YWH
*/
public class BaseController<Service extends IService,T> {

private static final Logger log = LoggerFactory.getLogger(BaseMapper.class);

@Autowired
private Service service;

/**
* 获取所有的数据
* @return 返回前端json数据
*/
@GetMapping("getList")
public Result getList(){
List<T> list = service.list();
return Result.successJson(list);
}

/**
* 分页查询
* @param pn 分页的实体类
* @return 返回前端json数据
*/
@GetMapping("getPageList")
public Result getPageList(@RequestBody BasePage pn){
Page<T> pojo = new Page<T>(pn.getCurrent(),pn.getSize());
IPage<T> page = service.page(pojo);
log.info("总条数 ------> " + page.getTotal());
log.info("当前页数 ------> " + page.getCurrent());
log.info("当前每页显示数 ------> " + page.getSize());
log.info("数据 ------> " + page.getRecords());
return Result.successJson(page);
}


。。。。。。。省略代码,具体代码请前往github查看

}

可以把一些通用的CRUD方法写在这些基础类中,在我们有了基础的这些类以后,我们自动生成的代码并没有在生成的时候继承我们这些定义好的基础类,所以我们要修改一下CodeGenerator工具类以便在生成的时候就继承,在CodeGenerator的策略配置中添加以下代码就可以了

1
2
3
4
5
复制代码//继承自定义的controller,service,impl,dao
strategy.setSuperControllerClass("com.ywh.common.base.BaseController");
strategy.setSuperServiceClass("com.ywh.common.base.BaseService");
strategy.setSuperServiceImplClass("com.ywh.common.base.BaseServiceImpl");
strategy.setSuperMapperClass("com.ywh.common.base.BaseDao");

测试用例

我们上面添加了基础类和分页插件以后,测试一下是不是好用的,把之前生成的Controller等文件删除也好,把myBatisPlus.properties中的是否覆盖文件设置成true也行,重新生成代码然后通过postman测试我们的查询,分页查询,添加数据等等好不好用。

分页查询

可以看到我有三条数据,但是我规定了当前页只显示一条数据,分页查询已经生效了,其他方法我这里就不测试了。

本文转载自: 掘金

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

社招面经总结——算法题篇 面试结果 股票买卖(头条) LRU

发表于 2019-04-08

面试结果

总结下最近的面试:

  • 头条后端:3面技术面挂
  • 蚂蚁支付宝营销-机器学习平台开发: 技术面通过,年后被通知只有P7的hc
  • 蚂蚁中台-机器学习平台开发: 技术面通过, 被蚂蚁HR挂掉(脉脉上好多人遇到这种情况,一个是今年大环境不好,另一个,面试尽量不要赶上阿里财年年底,这算是一点tips吧)
  • 快手后端: 拿到offer
  • 百度后端: 拿到offer

最终拒了百度,去快手了, 一心想去阿里, 个人有点阿里情节吧,缘分差点。
总结下最近的面试情况, 由于面了20多面, 就按照题型分类给大家一个总结。推荐大家每年都要抽出时间去面一下,不一定跳槽,但是需要知道自己的不足,一定要你的工龄匹配上你的能力。比如就我个人来说,通过面试我知道数据库的知识不是很懂,再加上由于所在组对数据库接触较少,这就是短板,作为一个后端工程师对数据库说不太了解是很可耻的,在选择offer的时候就可以适当有偏向性。下面分专题把最近的面试总结和大家总结一下。过分简单的就不说了,比如打印一个图形啥的, 还有的我不太记得清了,比如快手一面好像是一道动态规划的题目。当然,可能有的解决方法不是很好,大家可以在手撕代码群里讨论。最后一篇我再谈一下一些面试的技巧啥的。麻烦大家点赞转发支持下~

股票买卖(头条)

Leetcode 上有三题股票买卖,面试的时候只考了两题,分别是easy 和medium的难度

Leetcode 121. 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

1
2
复制代码输入: [7,1,5,3,6,4]
输出: 5

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
示例 2:

1
2
复制代码输入: [7,6,4,3,1]
输出: 0

解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

题解

纪录两个状态, 一个是最大利润, 另一个是遍历过的子序列的最小值。知道之前的最小值我们就可以算出当前天可能的最大利润是多少

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码class Solution {
public int maxProfit(int[] prices) {
// 7,1,5,3,6,4
int maxProfit = 0;
int minNum = Integer.MAX_VALUE;
for (int i = 0; i < prices.length; i++) {
if (Integer.MAX_VALUE != minNum && prices[i] - minNum > maxProfit) {
maxProfit = prices[i] - minNum;
}

if (prices[i] < minNum) {
minNum = prices[i];
}
}
return maxProfit;
}
}

Leetcode 122. 买卖股票的最佳时机 II

这次改成股票可以买卖多次, 但是你必须要在出售股票之前把持有的股票卖掉。
示例 1:

1
2
3
4
复制代码输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:

1
2
3
4
5
复制代码输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

1
2
3
复制代码输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

题解

由于可以无限次买入和卖出。我们都知道炒股想挣钱当然是低价买入高价抛出,那么这里我们只需要从第二天开始,如果当前价格比之前价格高,则把差值加入利润中,因为我们可以昨天买入,今日卖出,若明日价更高的话,还可以今日买入,明日再抛出。以此类推,遍历完整个数组后即可求得最大利润。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class Solution {
public int maxProfit(int[] prices) {
// 7,1,5,3,6,4
int maxProfit = 0;
for (int i = 0; i < prices.length; i++) {
if (i != 0 && prices[i] - prices[i-1] > 0) {
maxProfit += prices[i] - prices[i-1];
}
}
return maxProfit;
}
}

LRU cache (头条、蚂蚁)

这道题目是头条的高频题目,甚至我怀疑,头条这个面试题是题库里面的必考题。看脉脉也是好多人遇到。第一次我写的时候没写好,可能由于这个挂了。

转自知乎:zhuanlan.zhihu.com/p/34133067

题目

运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制。它应该支持以下操作: 获取数据 get 和 写入数据 put 。

获取数据 get(key) - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 put(key, value) - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。

进阶:

你是否可以在 O(1) 时间复杂度内完成这两种操作?

1
2
3
4
5
6
7
8
9
10
11
复制代码LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );

cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4

题解

这道题在今日头条、快手或者硅谷的公司中是比较常见的,代码要写的还蛮多的,难度也是hard级别。

最重要的是LRU 这个策略怎么去实现,
很容易想到用一个链表去实现最近使用的放在链表的最前面。
比如get一个元素,相当于被使用过了,这个时候它需要放到最前面,再返回值,
set同理。
那如何把一个链表的中间元素,快速的放到链表的开头呢?
很自然的我们想到了双端链表。

基于 HashMap 和 双向链表实现 LRU 的

整体的设计思路是,可以使用 HashMap 存储 key,这样可以做到 save 和 get key的时间都是 O(1),而 HashMap 的 Value 指向双向链表实现的 LRU 的 Node 节点,如图所示。

image.png

LRU 存储是基于双向链表实现的,下面的图演示了它的原理。其中 head 代表双向链表的表头,tail 代表尾部。首先预先设置 LRU 的容量,如果存储满了,可以通过 O(1) 的时间淘汰掉双向链表的尾部,每次新增和访问数据,都可以通过 O(1)的效率把新的节点增加到对头,或者把已经存在的节点移动到队头。

下面展示了,预设大小是 3 的,LRU存储的在存储和访问过程中的变化。为了简化图复杂度,图中没有展示 HashMap部分的变化,仅仅演示了上图 LRU 双向链表的变化。我们对这个LRU缓存的操作序列如下:

1
2
3
4
5
6
7
8
复制代码save("key1", 7)
save("key2", 0)
save("key3", 1)
save("key4", 2)
get("key2")
save("key5", 3)
get("key2")
save("key6", 4)

相应的 LRU 双向链表部分变化如下:

image.png

总结一下核心操作的步骤:

save(key, value),首先在 HashMap 找到 Key 对应的节点,如果节点存在,更新节点的值,并把这个节点移动队头。如果不存在,需要构造新的节点,并且尝试把节点塞到队头,如果LRU空间不足,则通过 tail 淘汰掉队尾的节点,同时在 HashMap 中移除 Key。

get(key),通过 HashMap 找到 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
103
104
105
106
107
复制代码    private static class DLinkedNode {
int key;
int value;
DLinkedNode pre;
DLinkedNode post;
}

/**
* 总是在头节点中插入新节点.
*/
private void addNode(DLinkedNode node) {

node.pre = head;
node.post = head.post;

head.post.pre = node;
head.post = node;
}

/**
* 摘除一个节点.
*/
private void removeNode(DLinkedNode node) {
DLinkedNode pre = node.pre;
DLinkedNode post = node.post;

pre.post = post;
post.pre = pre;
}

/**
* 摘除一个节点,并且将它移动到开头
*/
private void moveToHead(DLinkedNode node) {
this.removeNode(node);
this.addNode(node);
}

/**
* 弹出最尾巴节点
*/
private DLinkedNode popTail() {
DLinkedNode res = tail.pre;
this.removeNode(res);
return res;
}

private HashMap<Integer, DLinkedNode>
cache = new HashMap<Integer, DLinkedNode>();
private int count;
private int capacity;
private DLinkedNode head, tail;

public LRUCache(int capacity) {
this.count = 0;
this.capacity = capacity;

head = new DLinkedNode();
head.pre = null;

tail = new DLinkedNode();
tail.post = null;

head.post = tail;
tail.pre = head;
}

public int get(int key) {

DLinkedNode node = cache.get(key);
if (node == null) {
return -1; // cache里面没有
}

// cache 命中,挪到开头
this.moveToHead(node);

return node.value;
}


public void put(int key, int value) {
DLinkedNode node = cache.get(key);

if (node == null) {

DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;

this.cache.put(key, newNode);
this.addNode(newNode);

++count;

if (count > capacity) {
// 最后一个节点弹出
DLinkedNode tail = this.popTail();
this.cache.remove(tail.key);
count--;
}
} else {
// cache命中,更新cache.
node.value = value;
this.moveToHead(node);
}
}

二叉树层次遍历(头条)

这个题目之前也讲过,Leetcode 102题。

题目

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如:
给定二叉树: [3,9,20,null,null,15,7],

1
2
3
4
5
复制代码    3
/ \
9 20
/ \
15 7

返回其层次遍历结果:

1
2
3
4
5
复制代码[
[3],
[9,20],
[15,7]
]

题解

我们数据结构的书上教的层序遍历,就是利用一个队列,不断的把左子树和右子树入队。但是这个题目还要要求按照层输出。所以关键的问题是: 如何确定是在同一层的。
我们很自然的想到:
如果在入队之前,把上一层所有的节点出队,那么出队的这些节点就是上一层的列表。
由于队列是先进先出的数据结构,所以这个列表是从左到右的。

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
复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if (root == null) {
return res;
}

LinkedList<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
int size = queue.size();
List<Integer> currentRes = new LinkedList<>();
// 当前队列的大小就是上一层的节点个数, 依次出队
while (size > 0) {
TreeNode current = queue.poll();
if (current == null) {
continue;
}
currentRes.add(current.val);
// 左子树和右子树入队.
if (current.left != null) {
queue.add(current.left);
}
if (current.right != null) {
queue.add(current.right);
}
size--;
}
res.add(currentRes);
}
return res;
}
}

这道题可不可以用非递归来解呢?

递归的子问题:遍历当前节点, 对于当前层, 遍历左子树的下一层层,遍历右子树的下一层

递归结束条件: 当前层,当前子树节点是null

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
复制代码/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<>();
if (root == null) {
return res;
}
levelOrderHelper(res, root, 0);
return res;
}

/**
* @param depth 二叉树的深度
*/
private void levelOrderHelper(List<List<Integer>> res, TreeNode root, int depth) {
if (root == null) {
return;
}

if (res.size() <= depth) {
// 当前层的第一个节点,需要new 一个list来存当前层.
res.add(new LinkedList<>());
}
// depth 层,把当前节点加入
res.get(depth).add(root.val);
// 递归的遍历下一层.
levelOrderHelper(res, root.left, depth + 1);
levelOrderHelper(res, root.right, depth + 1);
}
}

二叉树转链表(快手)

这是Leetcode 104题。
给定一个二叉树,原地将它展开为链表。

例如,给定二叉树

1
2
3
4
5
6
复制代码
1
/ \
2 5
/ \ \
3 4 6

将其展开为:

1
2
3
4
5
6
7
8
9
10
11
复制代码1
\
2
\
3
\
4
\
5
\
6

这道题目的关键是转换的时候递归的时候记住是先转换右子树,再转换左子树。
所以需要记录一下右子树转换完之后链表的头结点在哪里。注意没有新定义一个next指针,而是直接将right 当做next指针,那么Left指针我们赋值成null就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码class Solution {
private TreeNode prev = null;

public void flatten(TreeNode root) {
if (root == null) return;
flatten(root.right); // 先转换右子树
flatten(root.left);
root.right = prev; // 右子树指向链表的头
root.left = null; // 把左子树置空
prev = root; // 当前结点为链表头
}
}

用递归解法,用一个stack 记录节点,右子树先入栈,左子树后入栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码class Solution {
public void flatten(TreeNode root) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode current = stack.pop();
if (current.right != null) stack.push(current.right);
if (current.left != null) stack.push(current.left);
if (!stack.isEmpty()) current.right = stack.peek();
current.left = null;
}
}
}

二叉树寻找最近公共父节点(快手)

Leetcode 236 二叉树的最近公共父亲节点

题解

从根节点开始遍历,如果node1和node2中的任一个和root匹配,那么root就是最低公共祖先。 如果都不匹配,则分别递归左、右子树,如果有一个 节点出现在左子树,并且另一个节点出现在右子树,则root就是最低公共祖先. 如果两个节点都出现在左子树,则说明最低公共祖先在左子树中,否则在右子树。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public class Solution {  
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//发现目标节点则通过返回值标记该子树发现了某个目标结点
if(root == null || root == p || root == q) return root;
//查看左子树中是否有目标结点,没有为null
TreeNode left = lowestCommonAncestor(root.left, p, q);
//查看右子树是否有目标节点,没有为null
TreeNode right = lowestCommonAncestor(root.right, p, q);
//都不为空,说明做右子树都有目标结点,则公共祖先就是本身
if(left!=null&&right!=null) return root;
//如果发现了目标节点,则继续向上标记为该目标节点
return left == null ? right : left;
}
}

数据流求中位数(蚂蚁)

面了蚂蚁中台的团队,二面面试官根据汇报层级推测应该是P9级别及以上,在美国面我,面试风格偏硅谷那边。题目是hard难度的,leetcode 295题。
这道题目是Leetcode的hard难度的原题。给定一个数据流,求数据流的中位数,求中位数或者topK的问题我们通常都会想用堆来解决。
但是面试官又进一步加大了难度,他要求内存使用很小,没有磁盘,但是压榨空间的同时可以忍受一定时间的损耗。且面试官不仅要求说出思路,要写出完整可经过大数据检测的production code。

先不考虑内存

不考虑内存的方式就是Leetcode 论坛上的题解。
基本思想是建立两个堆。左边是大根堆,右边是小根堆。
如果是奇数的时候,大根堆的堆顶是中位数。

例如:[1,2,3,4,5]
大根堆建立如下:

1
2
3
复制代码      3
/ \
1 2

小根堆建立如下:

1
2
3
复制代码      4
/
5

偶数的时候则是最大堆和最小堆顶的平均数。

例如: [1, 2, 3, 4]

大根堆建立如下:

1
2
3
复制代码      2
/
1

小根堆建立如下:

1
2
3
复制代码      3
/
4

然后再维护一个奇数偶数的状态即可求中位数。

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
复制代码public class MedianStream {
private PriorityQueue<Integer> leftHeap = new PriorityQueue<>(5, Collections.reverseOrder());
private PriorityQueue<Integer> rightHeap = new PriorityQueue<>(5);

private boolean even = true;

public double getMedian() {
if (even) {
return (leftHeap.peek() + rightHeap.peek()) / 2.0;
} else {
return leftHeap.peek();
}
}

public void addNum(int num) {
if (even) {
rightHeap.offer(num);
int rightMin = rightHeap.poll();
leftHeap.offer(rightMin);
} else {
leftHeap.offer(num);
int leftMax = leftHeap.poll();
rightHeap.offer(leftMax);
}
System.out.println(leftHeap);
System.out.println(rightHeap);
// 奇偶变换.
even = !even;
}
}

压榨内存

但是这样做的问题就是可能内存会爆掉。如果你的流无限大,那么意味着这些数据都要存在内存中,堆必须要能够建无限大。如果内存必须很小的方式,用时间换空间。

  • 流是可以重复去读的, 用时间换空间;
  • 可以用分治的思想,先读一遍流,把流中的数据个数分桶;
  • 分桶之后遍历桶就可以得到中位数落在哪个桶里面,这样就把问题的范围缩小了。
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
复制代码public class Median {
private static int BUCKET_SIZE = 1000;

private int left = 0;
private int right = Integer.MAX_VALUE;

// 流这里用int[] 代替
public double findMedian(int[] nums) {
// 第一遍读取stream 将问题复杂度转化为内存可接受的量级.
int[] bucket = new int[BUCKET_SIZE];
int step = (right - left) / BUCKET_SIZE;
boolean even = true;
int sumCount = 0;
for (int i = 0; i < nums.length; i++) {
int index = nums[i] / step;
bucket[index] = bucket[index] + 1;
sumCount++;
even = !even;
}
// 如果是偶数,那么就需要计算第topK 个数
// 如果是奇数, 那么需要计算第 topK和topK+1的个数.
int topK = even ? sumCount / 2 : sumCount / 2 + 1;

int index = 0;
int indexBucketCount = 0;
for (index = 0; index < bucket.length; index++) {
indexBucketCount = bucket[index];
if (indexBucketCount >= topK) {
// 当前bucket 就是中位数的bucket.
break;
}
topK -= indexBucketCount;
}

// 划分到这里其实转化为一个topK的问题, 再读一遍流.
if (even && indexBucketCount == topK) {
left = index * step;
right = (index + 2) * step;
return helperEven(nums, topK);
// 偶数的时候, 恰好划分到在左右两个子段中.
// 左右两段中 [topIndex-K + (topIndex-K + 1)] / 2.
} else if (even) {
left = index * step;
right = (index + 1) * step;
return helperEven(nums, topK);
// 左边 [topIndex-K + (topIndex-K + 1)] / 2
} else {
left = index * step;
right = (index + 1) * step;
return helperOdd(nums, topK);
// 奇数, 左边topIndex-K
}
}
}

这里边界条件我们处理好之后,关键还是helperOdd 和 helperEven这两个函数怎么去求topK的问题. 我们还是转化为一个topK的问题,那么求top-K和top(K+1)的问题到这里我们是不是可以用堆来解决了呢? 答案是不能,考虑极端情况。
中位数的重复次数非常多

1
2
复制代码eg:
[100,100,100,100,100...] (1000亿个100)

你的划分恰好落到这个桶里面,内存同样会爆掉。

再用时间换空间

假如我们的划分bucket大小是10000,那么最大的时候区间就是20000。(对应上面的偶数且落到两个分桶的情况)
那么既然划分到某一个bucket了,我们直接用数数字的方式来求topK 就可以了,用堆的方式也可以,更高效一点,其实这里问题规模已经划分到很小了,两种方法都可以。
我们选用TreeMap这种数据结构计数。然后分奇数偶数去求解。

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
复制代码    private double helperEven(int[] nums, int topK) {
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] >= left && nums[i] <= right) {
if (!map.containsKey(nums[i])) {
map.put(nums[i], 1);
} else {
map.put(nums[i], map.get(nums[i]) + 1);
}
}
}

int count = 0;
int kNum = Integer.MIN_VALUE;
int kNextNum = 0;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int currentCountIndex = entry.getValue();
if (kNum != Integer.MIN_VALUE) {
kNextNum = entry.getKey();
break;
}
if (count + currentCountIndex == topK) {
kNum = entry.getKey();
} else if (count + currentCountIndex > topK) {
kNum = entry.getKey();
kNextNum = entry.getKey();
break;
} else {
count += currentCountIndex;
}
}

return (kNum + kNextNum) / 2.0;
}

private double helperOdd(int[] nums, int topK) {
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int i = 0; i < nums.length; i++) {
if (nums[i] >= left && nums[i] <= right) {
if (!map.containsKey(nums[i])) {
map.put(nums[i], 1);
} else {
map.put(nums[i], map.get(nums[i]) + 1);
}
}
}
int count = 0;
int kNum = Integer.MIN_VALUE;
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int currentCountIndex = entry.getValue();
if (currentCountIndex + count >= topK) {
kNum = entry.getKey();
break;
} else {
count += currentCountIndex;
}
}

return kNum;
}

至此,我觉得算是一个比较好的解决方案,leetcode社区没有相关的解答和探索,欢迎大家交流。

热门阅读

  • 技术文章汇总
  • 【Leetcode】101. 对称二叉树
  • 【Leetcode】100. 相同的树
  • 【Leetcode】98. 验证二叉搜索树

Leetcode名企之路

本文转载自: 掘金

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

1…875876877…956

开发者博客

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