RabbitMQ基础概念

引言

你是否遇到过两个(多个)系统间需要通过定时任务来同步某些数据?你是否在为异构系统的不同进程间相互调用、通讯的问题而苦恼、挣扎?如果是,那么恭喜你,消息服务让你可以很轻松地解决这些问题。
消息服务擅长于解决多系统、异构系统间的数据交换(消息通知/通讯)问题,你也可以把它用于系统间服务的相互调用(RPC)。本文将要介绍的RabbitMQ就是当前最主流的[消息中间件]

RabbitMQ简介

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

下面将重点介绍RabbitMQ中的一些基础概念,了解了这些概念,是使用好RabbitMQ的基础。

ConnectionFactory、Connection、Channel

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。
Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。

Queue

Queue(队列)是RabbitMQ的内部对象,用于存储消息.

RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。

多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

Message acknowledgment

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。

这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…

另外pub message是没有ack的。

Message durability

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。由于这里仅为RabbitMQ的简单介绍,所以这里将不讲解RabbitMQ相关的事务。

Prefetch count

前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。

Exchange

在上一节我们看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的X),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。

Exchange是按照什么逻辑将消息路由到Queue的?这个将在Binding一节介绍。
RabbitMQ中的Exchange有四种类型,不同的类型有着不同的路由策略,这将在Exchange Types一节介绍。

routing key

生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。
在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。
RabbitMQ为routing key设定的长度限制为255 bytes。

Binding

RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。

Binding key

在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。这个将在Exchange Types章节会列举实际的例子加以说明。
在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。

binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。

Exchange Types

RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述),下面分别进行介绍。

fanout

fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。

上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。

direct

direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。

我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。

topic

前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:

  • routing key为一个句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
  • binding key与routing key一样也是句点号“. ”分隔的字符串
  • binding key中可以存在两种特殊字符“”与“#”,用于做模糊匹配,其中“”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)
    topic

以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1与Q2,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。

headers

headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。
在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地,所以不做介绍。

RPC

MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。

RabbitMQ中实现RPC的机制是:

  • 客户端发送请求(消息)时,在消息的属性(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)中设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
  • 服务器端收到消息并处理
  • 服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性
  • 客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理

总结

本文介绍了RabbitMQ中个人认为最重要的概念,充分利用RabbitMQ提供的这些功能就可以处理我们绝大部分的异步业务了。

Joda-Time

一 Jode-Time 介绍

任何企业应用程序都需要处理时间问题。应用程序需要知道当前的时间点和下一个时间点,有时它们还必须计算这两个
时间点之间的路径。使用 JDK 完成这项任务将非常痛苦和繁琐。
既然无法摆脱时间,为何不设法简化时间处理?现在来看看 Joda Time,一个面向 Java™ 平台的易于
使用的开源时间/日期库。正如您在本文中了解的那样,JodaTime轻松化解了处理日期和时间的痛苦和繁琐。

Joda-Time 令时间和日期值变得易于管理、操作和理解。事实上,易于使用是 Joda 的主要设计目标。其他目标包括可扩展性、完整的特性集以及对多种日历系统的支持。
并且 Joda 与 JDK 是百分之百可互操作的,因此您无需替换所有 Java 代码,只需要替换执行日期/时间计算的那部分代码。
Joda-Time提供了一组Java类包用于处理包括ISO8601标准在内的date和time。可以利用它把JDK Date和Calendar类完全替换掉,而且仍然能够提供很好的集成。

为什么要使用 Joda?
考虑创建一个用时间表示的某个随意的时刻 — 比如,2000 年 1 月 1 日 0 时 0 分。
我如何创建一个用时间表示这个瞬间的 JDK 对象?使用 java.util.Date?
事实上这是行不通的,因为自 JDK 1.1 之后的每个 Java 版本的 Javadoc 都声明应当使用 java.util.Calendar。
Date 中不赞成使用的构造函数的数量严重限制了您创建此类对象的途径。

那么 Calendar 又如何呢?我将使用下面的方式创建必需的实例:

1
2
Calendar calendar = Calendar.getInstance();
calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);

使用 Joda,代码应该类似如下所示:

1
2
DateTime dateTime = new
DateTime(2000, 1, 1, 0, 0, 0, 0);

这一行简单代码没有太大的区别。但是现在我将使问题稍微复杂化。
假设我希望在这个日期上加上 90 天并输出结果。使用 JDK,我需要使用清单 1 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 以 JDK 的方式向某一个瞬间加上 90 天并输出结果

Calendar calendar = Calendar.getInstance();

calendar.set(2000, Calendar.JANUARY, 1, 0, 0, 0);

SimpleDateFormat sdf =

new SimpleDateFormat("E MM/dd/yyyy HH:mm:ss.SSS");

calendar.add(Calendar.DAY_OF_MONTH, 90);

System.out.println(sdf.format(calendar.getTime()));

// 以 Joda 的方式向某一个瞬间加上 90 天并输出结果

1
2
3
4
DateTime dateTime = new 
DateTime(2000, 1, 1, 0, 0, 0, 0);

System.out.println(dateTime.plusDays(90).toString("E MM/dd/yyyy HH:mm:ss.SSS");

两者之间的差距拉大了(Joda 用了两行代码,JDK 则是 5 行代码)。
现在假设我希望输出这样一个日期:距离 2000.1.1日 45 天之后的某天在下一个月的当前周的最后一天的日期。
坦白地说,我甚至不想使用 Calendar 处理这个问题。
使用 JDK 实在太痛苦了,即使是简单的日期计算,比如上面这个计算。
正是多年前的这样一个时刻,我第一次领略到 JodaTime的强大。使用 Joda,用于计算的代码所示:

1
2
3
4
5
6
7
DateTime dateTime = new DateTime(2000, 1, 1, 0, 0, 0, 0);

System.out.println(dateTime.plusDays(45).plusMonths(1).dayOfWeek().withMaximumValue().toString("E MM/dd/yyyy HH:mm:ss.SSS");

输出为:

Sun 03/19/2000 00:00:00.000

如果您正在寻找一种易于使用的方式替代 JDK 日期处理,那么您真的应该考虑 Joda。

创建Joda-Time对象

现在,我将展示在采用该库时会经常遇到的一些 Joda 类,并展示如何创建这些类的实例。

ReadableInstant
Joda 通过 ReadableInstant 类实现了瞬间性这一概念。表示时间上的不可变瞬间的 Joda 类都属于这个类的子类。
(将这个类命名为ReadOnlyInstant 可能更好,我认为这才是设计者需要传达的意思)。
换句话说,ReadableInstant 表示时间上的某一个不可修改的瞬间。
其中的两个子类分别为 DateTime 和 DateMidnight

DateTime:这是最常用的一个类。它以毫秒级的精度封装时间上的某个瞬间时刻。
DateTime 始终与 DateTimeZone 相关,如果您不指定它的话,它将被默认设置为运行代码的机器所在的时区。
可以使用多种方式构建 DateTime 对象。这个构造函数使用系统时间:

1
DateTime dateTime = new DateTime();

如果您创建了一个 DateTime 的实例,并且没有提供 Chronology 或 DateTimeZone,Joda将使用 ISOChronology(默认)和DateTimeZone(来自系统设置)

Joda 可以使您精确地控制创建 DateTime 对象的方式,该对象表示时间上的某个特定的瞬间。

1
2
3
4
5
6
7
8
DateTime dateTime = new DateTime( 2 2000, //year
1, // month
1, // day
0, // hour (midnight is zero)
0, // minute
0, // second
0 // milliseconds
);

下一个构造函数将指定从 epoch(1970年1月1日 子时 格林威治标准时间) 到某个时刻所经过的毫秒数。
它根据 JDK Date 对象的毫秒值创建一个DateTime 对象,其时间精度用毫秒表示,因为 epoch 与 Joda 是相同的:

1 java.util.Date jdkDate = new Date(); 2 long timeInMillis = jdkDate.getTime(); 3 DateTime dateTime = new DateTime(timeInMillis);

或者Date 对象直接传递给构造函数:

dateTime = new DateTime(new Date());

Joda 支持使用许多其他对象作为构造函数的参数,用于创建 DateTime:

1
2
3
4
5
6
7
// Use a Calendar
dateTime = new DateTime(calendar); 3
// Use another Joda DateTime
dateTime = new DateTime(anotherDateTime); 6
// Use a String (must be formatted properly)
String timeString = "2006-01-26T13:30:00-06:00";
dateTime = new DateTime(timeString); 10 timeString = "2006-01-26"; 11 dateTime = new DateTime(timeString);

注意,如果您准备使用 String(必须经过解析),您必须对其进行精确地格式化。

DateMidnight:这个类封装某个时区(通常为默认时区)在特定年/月/日的午夜时分的时刻。
它基本上类似于 DateTime,不同之处在于时间部分总是为与该对象关联的特定 DateTimeZone 时区的午夜时分。

ReadablePartial
应用程序所需处理的日期问题并不全部都与时间上的某个完整时刻有关,因此您可以处理一个局部时刻。
例如,有时您比较关心年/月/日,或者一天中的时间,甚至是一周中的某天。Joda 设计者使用ReadablePartial 接口捕捉这种表示局部时间的概念,
这是一个不可变的局部时间片段。用于处理这种时间片段的两个有用类分别为 LocalDate 和 LocalTime

LocalDate:该类封装了一个年/月/日的组合。当地理位置(即时区)变得不重要时,使用它存储日期将非常方便。
例如,某个特定对象的出生日期 可能为 1999 年 4 月 16 日,但是从技术角度来看,
在保存所有业务值的同时不会了解有关此日期的任何其他信息(比如这是一周中的星期几,或者这个人出生地所在的时区)。
在这种情况下,应当使用 LocalDate。

LocalTime:这个类封装一天中的某个时间,当地理位置不重要的情况下,可以使用这个类来只存储一天当中的某个时间。
例如,晚上 11:52 可能是一天当中的一个重要时刻(比如,一个 cron 任务将启动,它将备份文件系统的某个部分),
但是这个时间并没有特定于某一天,因此我不需要了解有关这一时刻的其他信息。

创建对象代码:

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
  package com.jt.joda;  
import java.util.Date;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.LocalTime;
import org.junit.Test;
public class Demo {
@Test
public void test1(){
//方法一:取系统点间
DateTime dt1 = new DateTime(); System.out.println(dt1);
//方法二:通过java.util.Date对象生成
DateTime dt2 = new DateTime(new Date());
System.out.println(dt2);
//方法三:指定年月日点分秒生成(参数依次是:年,月,日,时,分,秒,毫秒)
DateTime dt3 = new DateTime(2012, 5, 20, 13, 14, 0, 0);
System.out.println(dt3);
//方法四:ISO8601形式生成
DateTime dt4 = new DateTime("2012-05-20"); System.out.println(dt4);
DateTime dt5 = new DateTime("2012-05-20T13:14:00"); System.out.println(dt5);
//只需要年月日的时候
LocalDate localDate = new LocalDate(2009, 9, 6);// September 6, 2009
System.out.println(localDate);
//只需要时分秒毫秒的时候
LocalTime localTime = new LocalTime(13, 30, 26, 0);// 1:30:26PM
System.out.println(localTime);
}
/*
2015-09-25T17:51:12.900+08:00
2015-09-25T17:51:12.977+08:00
2012-05-20T13:14:00.000+08:00
2012-05-20T00:00:00.000+08:00
2012-05-20T13:14:00.000+08:00
2009-09-06
13:30:26.000 */

}

三 与JDK日期对象转换

许多代码都使用了 JDK Date 和 Calendar 类。但是幸亏有 Joda,可以执行任何必要的日期算法,然后再转换回 JDK 类。
这将两者的优点集中到一起。您在本文中看到的所有 Joda 类都可以从 JDK Calendar 或 Date 创建,正如您在 创建 JodaTime对象 中看到的那样。
出于同样的原因,可以从您所见过的任何 Joda 类创建 JDK Calendar 或 Date。

1
2
3
4
5
6
7
8
9
10
DateTime dt = new DateTime(); 

//转换成java.util.Date对象
Date d1 = new Date(dt.getMillis());
Date d2 = dt.toDate();

//转换成java.util.Calendar对象
Calendar c1 = Calendar.getInstance();
c1.setTimeInMillis(dt.getMillis());
Calendar c2 = dt.toCalendar(Locale.getDefault());

对于 ReadablePartial 子类,您还需要经过额外一步,如所示:

1
Date date = localDate.toDateMidnight().toDate();

要创建 Date 对象,您必须首先将它转换为一个 DateMidnight 对象,然后只需要将 DateMidnight 对象作为 Date。
(当然,产生的 Date 对象将把它自己的时间部分设置为午夜时刻)。
JDK 互操作性被内置到 Joda API 中,因此您无需全部替换自己的接口,如果它们被绑定到 JDK 的话。比
如,您可以使用 Joda 完成复杂的部分,然后使用 JDK 处理接口。

四 日期计算

现在,您已经了解了如何创建一些非常有用的 Joda 类,我将向您展示如何使用它们执行日期计算。

假设在当前的系统日期下,我希望计算上一个月的最后一天。对于这个例子,我并不关心一天中的时间,因为我只需要获得年/月/日,如所示:

1
2
3
LocalDate now = SystemFactory.getClock().getLocalDate();

LocalDate lastDayOfPreviousMonth = now.minusMonths(1).dayOfMonth().withMaximumValue();

首先,我从当前月份减去一个月,得到 “上一个月”。
接着,我要求获得 dayOfMonth 的最大值,它使我得到这个月的最后一天。
注意,这些调用被连接到一起(注意 Joda ReadableInstant 子类是不可变的),这样您只需要捕捉调用链中最后一个方法的结果,从而获得整个计算的结果。

您可能对dayOfMonth() 调用感兴趣。这在 Joda 中被称为属性(property)。它相当于 Java对象的属性。
属性是根据所表示的常见结构命名的,并且它被用于访问这个结构,用于完成计算目的。
属性是实现 Joda 计算威力的关键。您目前所见到的所有 4 个 Joda 类都具有这样的属性。一些例子包括:
yearOfCentury
dayOfYear
monthOfYear
dayOfMonth
dayOfWeek

假设您希望获得任何一年中的第 11 月的第一个星期二的日期,而这天必须是在这个月的第一个星期一之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LocalDate now = SystemFactory.getClock().getLocalDate();

LocalDate electionDate = now.monthOfYear()

.setCopy(11) // November

.dayOfMonth() // Access Day Of Month Property

.withMinimumValue() // Get its minimum value

.plusDays(6) // Add 6 days

.dayOfWeek() // Access Day Of Week Property

.setCopy("Monday") // Set to Monday (it will round down)

.plusDays(1); // Gives us Tuesday

.setCopy(“Monday”) 是整个计算的关键。不管中间LocalDate 值是多少,将其 dayOfWeek 属性设置为 Monday 总是能够四舍五入,
这样的话,在每月的开始再加上 6 天就能够让您得到第一个星期一。再加上一天就得到第一个星期二。Joda 使得执行此类计算变得非常容易。

下面是其他一些因为使用 Joda 而变得超级简单的计算:

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
  DateTime dt = new DateTime(); 

//昨天

DateTime yesterday = dt.minusDays(1);

//明天

DateTime tomorrow = dt.plusDays(1);

//1个月前

DateTime before1month = dt.minusMonths(1);

//3个月后

DateTime after3month = dt.plusMonths(3);

//2年前

DateTime before2year = dt.minusYears(2);

//5年后

DateTime after5year = dt.plusYears(5);

五 格式化时间

使用 JDK 格式化日期以实现打印是完全可以的,但是我始终认为它应该更简单一些。
这是 Joda 设计者进行了改进的另一个特性。要格式化一个 Joda 对象,调用它的 toString() 方法,
并且如果您愿意的话,传递一个标准的 ISO8601或一个 JDK 兼容的控制字符串,以告诉 JDK 如何执行格式化。
不需要创建单独的 SimpleDateFormat 对象
(但是 Joda 的确为那些喜欢自找麻烦的人提供了一个DateTimeFormatter 类)。
调用 Joda 对象的 toString() 方法,仅此而已。

1
2
3
4
5
6
dateTime.toString(ISODateTimeFormat.basicDateTime());
dateTime.toString(ISODateTimeFormat.basicDateTimeNoMillis());
dateTime.toString(ISODateTimeFormat.basicOrdinalDateTime());
dateTime.toString(ISODateTimeFormat.basicWeekDateTime());

20090906T080000.000-0500 20090906T080000-0500 2009249T080000.000-0500 2009W367T080000.000-0500
1
2
3
4
5
6
DateTime dateTime = DateTime.now();
dateTime.toString("MM/dd/yyyy hh:mm:ss.SSSa");
dateTime.toString("dd-MM-yyyy HH:mm:ss");
dateTime.toString("EEEE dd MMMM, yyyy HH:mm:ssa");
dateTime.toString("MM/dd/yyyy HH:mm ZZZZ");
dateTime.toString("MM/dd/yyyy HH:mm Z"); 09/06/2009 02:30:00.000PM 06-Sep-2009 14:30:00 Sunday 06 September, 2009 14:30:00PM 09/06/2009 14:30 America/Chicago 09/06/2009 14:30 -0500

结束语

谈到日期处理,Joda 是一种令人惊奇的高效工具。无论您是计算日期、打印日期,或是解析日期,Joda都将是工具箱中的便捷工具。
在本文中,我首先介绍了 Joda,它可以作为 JDK 日期/时间库的替代选择。然后介绍了一些 Joda 概念,以及如何使用 Joda 执行日期计算和格式化。

六 使用代码案例

2`、获取年月日点分秒`

1`. DateTime dt = newDateTime(); `

2`. //年 `

3`. intyear = dt.getYear(); `

4`. //月 `

5`. intmonth = dt.getMonthOfYear(); `

6`. //日 `

7`. intday = dt.getDayOfMonth(); `

8`. //星期 `

9`. intweek = dt.getDayOfWeek(); `

10`. //点 `

11`. inthour = dt.getHourOfDay(); `

12`. //分 `

13`. intmin = dt.getMinuteOfHour(); `

14`. //秒 `

15`. intsec = dt.getSecondOfMinute(); `

16`. //毫秒 `

17`. intmsec = dt.getMillisOfSecond(); `

3 星期的特殊处理

dt.getDayOfWeek()

1`. DateTime dt = newDateTime(); `

2`. `

3`. //星期 `

4`. switch(dt.getDayOfWeek()) { `

5`. caseDateTimeConstants.SUNDAY: `

6`. System.out.println("星期日"); `

7`. break; `

8`. caseDateTimeConstants.MONDAY: `

9`. System.out.println("星期一"); `

10`. break; `

11`. caseDateTimeConstants.TUESDAY: `

12`. System.out.println("星期二"); `

13`. break; `

14`. caseDateTimeConstants.WEDNESDAY: `

15`. System.out.println("星期三"); `

16`. break; `

17`. caseDateTimeConstants.THURSDAY: `

18`. System.out.println("星期四"); `

19`. break; `

20`. caseDateTimeConstants.FRIDAY: `

21`. System.out.println("星期五"); `

22`. break; `

23`. caseDateTimeConstants.SATURDAY: `

24`. System.out.println("星期六"); `

25`. break; `

26`. } `

4`、与JDK日期对象的转换`

1`. DateTime dt = newDateTime(); `

2`. `

3`. //转换成java.util.Date对象 `

4`. Date d1 = newDate(dt.getMillis()); `

5`. Date d2 = dt.toDate(); `

6`. `

7`. //转换成java.util.Calendar对象 `

8`. Calendar c1 = Calendar.getInstance(); `

9`. c1.setTimeInMillis(dt.getMillis()); `

10`. Calendar c2 = dt.toCalendar(Locale.getDefault());`

5`、日期前后推算`

1`. DateTime dt = newDateTime(); `

2`. `

3`. //昨天 `

4`. DateTime yesterday = dt.minusDays(1); `

5`. //明天 `

6`. DateTime tomorrow = dt.plusDays(1); `

7`. //1个月前 `

8`. DateTime before1month = dt.minusMonths(1); `

9`. //3个月后 `

10`. DateTime after3month = dt.plusMonths(3); `

11`. //2年前 `

12`. DateTime before2year = dt.minusYears(2); `

13`. //5年后 `

14`. DateTime after5year = dt.plusYears(5); `

6`、取特殊日期`

1`. DateTime dt = newDateTime(); `

2`. `

3`. //月末日期 `

4`. DateTime lastday = dt.dayOfMonth().withMaximumValue(); `

5`. `

6`. //90天后那周的周一 `

7`. DateTime firstday = dt.plusDays(90).dayOfWeek().withMinimumValue(); `

7`、时区`

1`. //默认设置为日本时间 `

2`. DateTimeZone.setDefault(DateTimeZone.forID("Asia/Tokyo")); `

3`. DateTime dt1 = newDateTime(); `

4`. `

5`. //伦敦时间 `

6`. DateTime dt2 = new` `DateTime(DateTimeZone.forID(“Europe/London”)); `

8`、计算区间`

1`. DateTime begin = new` `DateTime(“2012-02-01”); `

2`. DateTime end = new` `DateTime(“2012-05-01”); `

3`. `

4`. //计算区间毫秒数 `

5`. Duration d = newDuration(begin, end); `

6`. longtime = d.getMillis(); `

7`. `

8`. //计算区间天数 `

9`. Period p = newPeriod(begin, end, PeriodType.days()); `

10`. intdays = p.getDays(); `

11`. `

12`. //计算特定日期是否在该区间内 `

13`. Interval i = newInterval(begin, end); `

14`. boolean` `contained = i.contains(newDateTime("2012-03-01")); `

9`、日期比较`

1`. DateTime d1 = new` `DateTime(“2012-02-01”); `

2`. DateTime d2 = new` `DateTime(“2012-05-01”); `

3`. `

4`. //和系统时间比 `

5`. booleanb1 = d1.isAfterNow(); `

6`. booleanb2 = d1.isBeforeNow(); `

7`. booleanb3 = d1.isEqualNow(); `

8`. `

9`. //和其他日期比 `

10`. booleanf1 = d1.isAfter(d2); `

11`. booleanf2 = d1.isBefore(d2); `

12`. booleanf3 = d1.isEqual(d2); `

10`、格式化输出`

1`. DateTime dateTime = newDateTime(); `

2`. `

3`. String s1 = dateTime.toString("yyyy/MM/dd hh:mm:ss.SSSa"); `

4`. String s2 = dateTime.toString("yyyy-MM-dd HH:mm:ss"); `

5`. String s3 = dateTime.toString("EEEE dd MMMM, yyyy HH:mm:ssa"); `

6`. String s4 = dateTime.toString("yyyy/MM/dd HH:mm ZZZZ"); `

7`. String s5 = dateTime.toString("yyyy/MM/dd HH:mm Z"); `

|

案例:

public static DateTime getNowWeekMonday() { 
    DateTime date = DateTime.now(); 
    int dayOfWeek = date.getDayOfWeek(); 
    return DateTime.parse(date.minusDays(dayOfWeek - 1).toString("yyyy-MM-dd")); 
}
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
private static final String DATE_FORMAT = "yyyy-MM-dd";

//每周一0点0分0秒触发,处理上上周
@Scheduled(cron = "0 0 0 ? * MON ")
public void weeklyRemind() {
logger.info("CyclePendingReminderTask.weeklyRemind");
logger.info("周期性待处理提醒任务开始");
String now = DateTime.now().toString(DATE_FORMAT);
//往前推2周,上上周周一
String from = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusWeeks(2).toString(DATE_FORMAT);
//上上周周日
String to = DateTime.parse(from, ISODateTimeFormat.dateElementParser())
.plusWeeks(1).minusDays(1).toString(DATE_FORMAT);
//上上周周一0点时间戳
long fromTime = DateTime.parse(from, ISODateTimeFormat.dateElementParser()).getMillis();
//上周周一0点时间戳
long toTime = DateTime.parse(to, ISODateTimeFormat.dateElementParser()).plus(1).getMillis();
List<String> userIdList = ideaService.getUserIdList();
for (String userId : userIdList) {
List<Idea> ideaList = ideaService.findIdeasByCreateAt(userId, fromTime, toTime);
//有创建想法才会有提醒
if (ideaList.size() > 0) {
CyclePendingIdeaReminder reminder = new CyclePendingIdeaReminder();
reminder.setUserId(userId);
reminder.setFrom(from);
reminder.setTo(to);
reminder.setFinished(false);
cpiReminderService.save(reminder);
}
}
logger.info("周期性待处理提醒任务完成");
}
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
//每月一号0点0分0秒触发
//当中再判断当前月份进行季度和年度的处理操作
@Scheduled(cron = "0 0 0 1 * ? ")
public void monthlySelectionRemind() {
logger.info("IdeaSelectionReminderTask monthlySelectionRemind start.");
DateTime nowTime = DateTime.now();
int month = nowTime.getMonthOfYear();
String now = nowTime.toString(DATE_FORMAT);
//年度处理: 1
if (month == 1) {
logger.info("年度精选任务开始");
String from = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusYears(1).toString(DATE_FORMAT);
String to = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusDays(1).toString(DATE_FORMAT);
doMonthly(from, to, OriginalityType.year);
logger.info("年度精选任务完成");
}
//季度处理: 3(4) 6(7) 9(10) 12(1)
if (month == 4 || month == 7 || month == 10 || month == 1) {
logger.info("季度精选任务开始");
String from = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusMonths(3).toString(DATE_FORMAT);
String to = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusDays(1).toString(DATE_FORMAT);
doMonthly(from, to, OriginalityType.quarter);
logger.info("季度精选任务完成");
}
//月份处理
logger.info("月精选任务开始");
String from = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusMonths(1).toString(DATE_FORMAT);
String to = DateTime.parse(now, ISODateTimeFormat.dateElementParser())
.minusDays(1).toString(DATE_FORMAT);
doMonthly(from, to, OriginalityType.month);
logger.info("月精选任务完成");
logger.info("IdeaSelectionReminderTask monthlySelectionRemind finish.");
}
1
2
3
4
5
6
 // 今日凌晨
Date date = DateTime.parse(DateTime.now().toString("yyyy-MM-dd")).toDate()
// 今天9点对应的日期
Date date = DateTime.parse(DateTime.now().toString("yyyy-MM-dd")).hourOfDay().addToCopy(9).toDate();
// 当前时间加1分钟
Date date = DateTime.now().minuteOfHour().addToCopy(1)).toDate()

Github的webhooks

GitHub的webhooks设置包括 url设置 ,Content type设置(application/json或application/x-www-form-urlencoded请求内容方式), secret设置(可选,采用HMAC 方式加密) ,event设置(全选,或部分),页面如下:

h1

点击add webhook则设置成功,同时github会发出ping请求测试填写的url是否可用,请求如下:

h2

之后当所设置的event发生时,github则会向设置的url发送请求,所有请求全部为post方式,并且记录请求发送内容和url返回的情况,页面如下:

h3

无论hook请求是否成功,都会有列表记录,并且都有重发按钮进行请求重新发送,页面如下:

h4

h5

GitHub官网介绍:webhooks

Jackson使用

Java生态圈中有很多处理JSON和XML格式化的类库,Jackson是其中比较著名的一个。虽然JDK自带了XML处理类库,但是相对来说比较低级,使用本文介绍的Jackson等高级类库处理起来会方便很多。

引入类库

由于Jackson相关类库按照功能分为几个相对独立的,所以需要同时引入多个类库,为了方便我将版本号单独提取出来设置,相关Gradle配置如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ext {
jacksonVersion = '2.9.5'
}

dependencies {
compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jacksonVersion
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jacksonVersion
compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jacksonVersion
// 引入XML功能
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: jacksonVersion
// 比JDK自带XML实现更高效的类库
compile group: 'com.fasterxml.woodstox', name: 'woodstox-core', version: '5.1.0'
// Java 8 新功能
compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: jacksonVersion
compile group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: jacksonVersion
compile group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jdk8', version: jacksonVersion

compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.16.22'
}

Maven配置请去mvnrepository搜索。

Jackson注解

Jackson类库包含了很多注解,可以让我们快速建立Java类与JSON之间的关系。详细文档可以参考Jackson-Annotations。下面介绍一下常用的。

属性命名
==@JsonProperty==注解指定一个属性用于JSON映射,默认情况下映射的JSON属性与注解的属性名称相同,不过可以使用该注解的value值修改JSON属性名,该注解还有一个index属性指定生成JSON属性的顺序,如果有必要的话。

属性包含
还有一些注解可以管理在映射JSON的时候包含或排除某些属性,下面介绍一下常用的几个。

==@JsonIgnore==注解用于排除某个属性,这样该属性就不会被Jackson序列化和反序列化。

==@JsonIgnoreProperties==注解是类注解。在序列化为JSON的时候,@JsonIgnoreProperties({“prop1”, “prop2”})会忽略pro1和pro2两个属性。在从JSON反序列化为Java类的时候,@JsonIgnoreProperties(ignoreUnknown=true)会忽略所有没有Getter和Setter的属性。该注解在Java类和JSON不完全匹配的时候很有用。

==@JsonIgnoreType==也是类注解,会排除所有指定类型的属性。

序列化相关
==@JsonPropertyOrder==和==@JsonProperty的index==属性类似,指定属性序列化时的顺序。

==@JsonRootName==注解用于指定JSON根属性的名称。

处理JSON

简单映射

我们用Lombok设置一个简单的Java类。

1
2
3
4
5
6
7
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Friend {
private String nickname;
private int age;
}

然后就可以处理JSON数据了。首先需要一个ObjectMapper对象,序列化和反序列化都需要它。

ObjectMapper mapper = new ObjectMapper();
Friend friend = new Friend("yitian", 25);
// 写为字符串
String text = mapper.writeValueAsString(friend);
// 写为文件
mapper.writeValue(new File("friend.json"), friend);
// 写为字节流
byte[] bytes = mapper.writeValueAsBytes(friend);
System.out.println(text);
// 从字符串中读取
Friend newFriend = mapper.readValue(text, Friend.class);
// 从字节流中读取
newFriend = mapper.readValue(bytes, Friend.class);
// 从文件中读取
newFriend = mapper.readValue(new File("friend.json"), Friend.class);
System.out.println(newFriend);

程序结果如下。可以看到生成的JSON属性和Java类中定义的一致。

1
2
{"nickname":"yitian","age":25}
Friend(nickname=yitian, age=25)

集合的映射

除了使用Java类进行映射之外,我们还可以直接使用Map和List等Java集合组织JSON数据,在需要的时候可以使用readTree方法直接读取JSON中的某个属性值。需要注意的是从JSON转换为Map对象的时候,由于Java的类型擦除,所以类型需要我们手动用new TypeReference给出。

ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = new HashMap<>();
map.put("age", 25);
map.put("name", "yitian");
map.put("interests", new String[]{"pc games", "music"});
String text = mapper.writeValueAsString(map);
System.out.println(text);
Map<String, Object> map2 = mapper.readValue(text, new TypeReference<Map<String, Object>>() {
});
System.out.println(map2);
JsonNode root = mapper.readTree(text);
String name = root.get("name").asText();
int age = root.get("age").asInt();
System.out.println("name:" + name + " age:" + age);

程序结果如下。

1
2
3
{"name":"yitian","interests":["pc games","music"],"age":25}
{name=yitian, interests=[pc games, music], age=25}
name:yitian age:25

Jackson配置

Jackson预定义了一些配置,我们通过启用和禁用某些属性可以修改Jackson运行的某些行为。详细文档参考JacksonFeatures。下面我简单翻译一下Jackson README上列出的一些属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 美化输出
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// 允许序列化空的POJO类
// (否则会抛出异常)
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// 把java.util.Date, Calendar输出为数字(时间戳)
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

// 在遇到未知属性的时候不抛出异常
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 强制JSON 空字符串("")转换为null对象值:
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);

// 在JSON中允许C/C++ 样式的注释(非标准,默认禁用)
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
// 允许没有引号的字段名(非标准)
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// 允许单引号(非标准)
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
// 强制转义非ASCII字符
mapper.configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true);
// 将内容包裹为一个JSON属性,属性名由@JsonRootName注解指定
mapper.configure(SerializationFeature.WRAP_ROOT_VALUE, true);

这里有三个方法,configure方法接受配置名和要设置的值,Jackson 2.5版本新加的enable和disable方法则直接启用和禁用相应属性,我推荐使用后面两个方法。

用注解管理映射

前面介绍了一些Jackson注解,下面来应用一下这些注解。首先来看看使用了注解的Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("FriendDetail")
@JsonIgnoreProperties({"uselessProp1", "uselessProp3"})
public class FriendDetail {
@JsonProperty("NickName")
private String name;
@JsonProperty("Age")
private int age;
private String uselessProp1;
@JsonIgnore
private int uselessProp2;
private String uselessProp3;
}

然后看看代码。需要注意的是,由于设置了排除的属性,所以生成的JSON和Java类并不是完全对应关系,所以禁用DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES是必要的。

ObjectMapper mapper = new ObjectMapper();
//mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
FriendDetail fd = new FriendDetail("yitian", 25, "", 0, "");
String text = mapper.writeValueAsString(fd);
System.out.println(text);
FriendDetail fd2 = mapper.readValue(text, FriendDetail.class);
System.out.println(fd2);

运行结果如下。可以看到生成JSON的时候忽略了我们制定的值,而且在转换为Java类的时候对应的属性为空。

1
2
{"NickName":"yitian","Age":25}
FriendDetail(name=yitian, age=25, uselessProp1=null, uselessProp2=0, uselessProp3=null)

然后取消注释代码中的那行,也就是启用WRAP_ROOT_VALUE功能,再运行一下程序,运行结果如下。可以看到生成的JSON结果发生了变化,而且由于JSON结果变化,所以Java类转换失败(所有字段值全为空)。WRAP_ROOT_VALUE这个功能在有些时候比较有用,因为有些JSON文件需要这种结构。

1
2
{"FriendDetail":{"NickName":"yitian","Age":25}}
FriendDetail(name=null, age=0, uselessProp1=null, uselessProp2=0, uselessProp3=null)

Java8日期时间类支持

Java8增加了一套全新的日期时间类,Jackson对此也有支持。这些支持是以Jackson模块形式提供的,所以首先就是注册这些模块。

ObjectMapper mapper = new ObjectMapper()
        .registerModule(new JavaTimeModule())
        .registerModule(new ParameterNamesModule())
        .registerModule(new Jdk8Module());

导入类库之后,Jackson也可以自动搜索所有模块,不需要我们手动注册。

mapper.findAndRegisterModules();

我们新建一个带有LocalDate字段的Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Person")
public class Person {
@JsonProperty("Name")
private String name;
@JsonProperty("NickName")
private String nickname;
@JsonProperty("Age")
private int age;
@JsonProperty("IdentityCode")
private String identityCode;
@JsonProperty
@JsonFormat(pattern = "yyyy-MM-DD")
private LocalDate birthday;

}

然后来看看代码。

static void java8DateTime() throws IOException {
    Person p1 = new Person("yitian", "易天", 25, "10000", LocalDate.of(1994, 1, 1));
    ObjectMapper mapper = new ObjectMapper()
            .registerModule(new JavaTimeModule());
    //mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    String text = mapper.writeValueAsString(p1);
    System.out.println(text);
    Person p2 = mapper.readValue(text, Person.class);
    System.out.println(p2);
}

运行结果如下。可以看到,生成的JSON日期变成了[1994,1,1]这样的时间戳形式,一般情况下不符合我们的要求。

1
2
{"birthday":[1994,1,1],"Name":"yitian","NickName":"易天","Age":25,"IdentityCode":"10000"}
Person(name=yitian, nickname=易天, age=25, identityCode=10000, birthday=1994-01-01)

取消注释那行代码,程序运行结果如下。这样一来就变成了我们一般使用的形式了。如果有格式需要的话,可以使用@JsonFormat(pattern = “yyyy-MM-DD”)注解格式化日期显示。

1
2
{"birthday":"1994-01-01","Name":"yitian","NickName":"易天","Age":25,"IdentityCode":"10000"}
Person(name=yitian, nickname=易天, age=25, identityCode=10000, birthday=1994-01-01)

处理XML

Jackson是一个处理JSON的类库,不过它也通过jackson-dataformat-xml包提供了处理XML的功能。Jackson建议我们在处理XML的时候使用woodstox-core包,它是一个XML的实现,比JDK自带XML实现更加高效,也更加安全。

这里有个注意事项,如果你正在使用Java 9以上的JDK,可能会出现java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException异常,这是因为Java 9实现了JDK的模块化,将原本和JDK打包在一起的JAXB实现分隔出来。所以这时候需要我们手动添加JAXB的实现。在Gradle中添加下面的代码即可。

1
compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'

注解
Jackson XML除了使用Jackson JSON和JDK JAXB的一些注解之外,自己也定义了一些注解。下面简单介绍一下几个常用注解。

==@JacksonXmlProperty==注解有三个属性,namespace和localname属性用于指定XML命名空间的名称,isAttribute指定该属性作为XML的属性()还是作为子标签().

==@JacksonXmlRootElement==注解有两个属性,namespace和localname属性用于指定XML根元素命名空间的名称。

==@JacksonXmlText==注解将属性直接作为未被标签包裹的普通文本表现。

==@JacksonXmlCData==将属性包裹在CDATA标签中。

XML映射
新建如下一个Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonRootName("Person")
public class Person {
@JsonProperty("Name")
private String name;
@JsonProperty("NickName")
//@JacksonXmlText
private String nickname;
@JsonProperty("Age")
private int age;
@JsonProperty("IdentityCode")
@JacksonXmlCData
private String identityCode;
@JsonProperty("Birthday")
//@JacksonXmlProperty(isAttribute = true)
@JsonFormat(pattern = "yyyy/MM/DD")
private LocalDate birthday;

下面是代码示例,基本上和JSON的API非常相似,XmlMapper实际上就是ObjectMapper的子类。

Person p1 = new Person("yitian", "易天", 25, "10000", LocalDate.of(1994, 1, 1));
XmlMapper mapper = new XmlMapper();
mapper.findAndRegisterModules();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String text = mapper.writeValueAsString(p1);
System.out.println(text);
Person p2 = mapper.readValue(text, Person.class);
System.out.println(p2);

运行结果如下。

1
2
3
4
5
6
7
8
9
<Person>
<Name>yitian</Name>
<NickName>易天</NickName>
<Age>25</Age>
<IdentityCode><![CDATA[10000]]></IdentityCode>
<Birthday>1994/01/01</Birthday>
</Person>

Person(name=yitian, nickname=易天, age=25, identityCode=10000, birthday=1994-01-01)

如果取消那两行注释,那么运行结果如下。可以看到Jackson XML注解对生成的XML的控制效果。

1
2
3
4
5
6
7
<Person birthday="1994/01/01">
<Name>yitian</Name>易天
<Age>25</Age>
<IdentityCode><![CDATA[10000]]></IdentityCode>
</Person>

Person(name=yitian, nickname=null, age=25, identityCode=10000, birthday=1994-01-01)

Spring Boot集成

自动配置
Spring Boot对Jackson的支持非常完善,只要我们引入相应类库,Spring Boot就可以自动配置开箱即用的Bean。Spring自动配置的ObjectMapper(或者XmlMapper)作了如下配置,基本上可以适应大部分情况。

禁用了MapperFeature.DEFAULT_VIEW_INCLUSION
禁用了DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
禁用了SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
如果需要修改自动配置的ObjectMapper属性也非常简单,Spring Boot提供了一组环境变量,直接在application.properties文件中修改即可。

|Jackson枚举|Spring环境变量|
|—–|—–|

1
2
3
4
5
6
com.fasterxml.jackson.databind.DeserializationFeature|spring.jackson.deserialization.=true|false 
com.fasterxml.jackson.core.JsonGenerator.Feature|spring.jackson.generator.=true|false
com.fasterxml.jackson.databind.MapperFeature|spring.jackson.mapper.=true|false
com.fasterxml.jackson.core.JsonParser.Feature|spring.jackson.parser.=true|false
com.fasterxml.jackson.databind.SerializationFeature|spring.jackson.serialization.=true|false
com.fasterxml.jackson.annotation.JsonInclude.Include|spring.jackson.default-property-inclusion=always|non_null|non_absent|non_default|non_empty

由于Spring会同时配置相应的HttpMessageConverters,所以我们其实要做的很简单,用Jackson注解标注好要映射的Java类,然后直接让控制器返回对象即可!下面是一个Java类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@JsonRootName("person")
public class Person {
@JsonProperty
private String name;
@JsonProperty
private int id;
@JsonFormat(pattern = "yyyy-MM-DD")
private LocalDate birthday;

public Person(String name, int id, LocalDate birthday) {
this.name = name;
this.id = id;
this.birthday = birthday;
}
}

然后是控制器代码。在整个过程中我们只需要引入Jackson类库,然后编写业务代码就好了。关于如何配置Jackson类库,我们完全不需要管,这就是Spring Boot的方便之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class MainController {
private Person person = new Person("yitian", 10000, LocalDate.of(1994, 1, 1));

@RequestMapping("/")
public String index() {
return "index";
}

@RequestMapping(value = "/json", produces = "application/json")
@ResponseBody
public Person json() {
return person;
}
}

进入localhost:8080/xml就可以看到对应结果了。

json

手动配置
Spring Boot自动配置非常方便,但不是万能的。在必要的时候,我们需要手动配置Bean来替代自动配置的Bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class JacksonConfig {
@Bean
@Primary
@Qualifier("xml")
public XmlMapper xmlMapper(Jackson2ObjectMapperBuilder builder) {
XmlMapper mapper = builder.createXmlMapper(true)
.build();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}

@Bean
@Qualifier("json")
public ObjectMapper jsonMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper mapper = builder.createXmlMapper(false)
.build();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}

然后在需要的地方进行依赖注入。需要注意为了区分ObjectMapper和XmlMapper,需要使用@Qualifier注解进行标记。

1
2
3
4
5
6
7
8
9
10
@Controller
public class MainController {
private ObjectMapper jsonMapper;
private XmlMapper xmlMapper;
private Person person = new Person("yitian", 10000, LocalDate.of(1994, 1, 1));

public MainController(@Autowired @Qualifier("json") ObjectMapper jsonMapper, @Autowired @Qualifier("xml") XmlMapper xmlMapper) {
this.jsonMapper = jsonMapper;
this.xmlMapper = xmlMapper;
}

以上就是Jackson类库的一些介绍,希望对大家有所帮助。

jdbcTemplate和namedParameterJdbcTemplate

我们开发DAO层时用的最多的就是ORM框架(Mybatis,hibernate)了。在有些特殊的情况下,ORM框架的搭建略显笨重,这时最好的选择就是Spring中的jdbcTemplate了。本文对jdbcTemplate进行详解,并且会对具名参数namedParameterJdbcTemplate进行讲解。

jdbcTemplate讲解

jdbcTemplate提供的主要方法:

  • execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
  • update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
  • query方法及queryForXXX方法:用于执行查询相关语句;
  • call方法:用于执行存储过程、函数相关语句。

    jdbcTemplate环境搭建:

1 在spring配置文件中加上jdbcTemplate的bean:

1
2
3
4
<!--注入jdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>

注意:在这之前我们需要先配置好数据库数据源dateSource。
2.在使用jdbcTemplate类中使用@Autowired进行注入

1
2
@Autowired
private JdbcTemplate jdbcTemplate;

1.查询单个对象queryForObject:

1
2
3
4
5
6
7
8
@Test
public void testQuery(){
String sql = "select id,username,password from user where id=?";

BeanPropertyRowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper,1);
System.out.println(user);
}

输出结果: User{id=1, username=’123’, password=’123’}
2.查询多个对象query:

1
2
3
4
5
6
7
8
9
10
@Test
public void testMutiQuery(){
String sql = "select id,username,password from user";

BeanPropertyRowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
List<User> users = jdbcTemplate.query(sql, rowMapper);
for (User user : users) {
System.out.println(user);
}
}

输出结果:
User{id=1, username=’123’, password=’123’}
User{id=2, username=’1234’, password=’1234’}
3.查询count、avg、sum等函数返回唯一值:

1
2
3
4
5
6
7
8
@Test
public void testCountQuery(){
String sql = "select count(*) from user";

BeanPropertyRowMapper<User> rowMapper = new BeanPropertyRowMapper<User>(User.class);
Integer result = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println(result);
}

输出结果:2
4.增删改方法测试:
新增:

1
2
3
4
5
6
@Test
public void testCreate(){
String sql = "insert into user (username,password) values (?,?)";
int create = jdbcTemplate.update(sql, new Object[]{255, 255});
System.out.println(create);
}

输出结果为1,去数据库查看也确实插入这条。

修改:

1
2
3
4
5
6
@Test
public void testUpdate(){
String sql = "update user set username=? , password=? where id=?";
int update = jdbcTemplate.update(sql, new Object[]{256, 256,3});
System.out.println(update);
}

输出结果为1,并且确实数据已经修改
删除:

1
2
3
4
5
6
@Test
public void testDelete(){
String sql = "delete from user where id=?";
int delete = jdbcTemplate.update(sql, new Object[]{3});
System.out.println(delete);
}

5.批量操作:

1
2
3
4
5
6
7
8
9
@Test
public void testBatch(){
List<Object[]> batchArgs=new ArrayList<Object[]>();
batchArgs.add(new Object[]{777,888});
batchArgs.add(new Object[]{666,888});
batchArgs.add(new Object[]{555,888});
String sql = "insert into user (username,password) values (?,?)";
jdbcTemplate.batchUpdate(sql, batchArgs);
}

以上方法基本满足了日常我们多DAO层进行的操作,不过当你进行CRUD操作时如果传入的参数不确定,这时候你可能会想起ORM框架的便利。没关系!Spring也为我们提供了这样的操作NamedParameterJdbcTemplate。

NamedParameterJdbcTemplate讲解

在经典的 JDBC 用法中, SQL 参数是用占位符 ? 表示,并且受到位置的限制. 定位参数的问题在于, 一旦参数的顺序发生变化, 就必须改变参数绑定.
在 Spring JDBC 框架中, 绑定 SQL 参数的另一种选择是使用具名参数(named parameter).
那么什么是具名参数?
具名参数: SQL 按名称(以冒号开头)而不是按位置进行指定. 具名参数更易于维护, 也提升了可读性. 具名参数由框架类在运行时用占位符取代
具名参数只在 NamedParameterJdbcTemplate 中得到支持。NamedParameterJdbcTemplate可以使用全部jdbcTemplate方法,除此之外,我们来看看使用它的具名参数案例:
具名新增:

1
2
3
4
5
6
7
8
@Test
public void testNamedParameter(){
String sql = "insert into user (username,password) values (:username,:password)";
User u = new User();
u.setUsername("555");
SqlParameterSource sqlParameterSource=new BeanPropertySqlParameterSource(u);
namedParameterJdbcTemplate.update(sql,sqlParameterSource);
}

这样我们就可以根据pojo类的属性值使用JDBC来操作数据库了。
获取新增的主键:
NamedParameterJdbcTemplate还新增了KeyHolder类,使用它我们可以获得主键,类似Mybatis中的useGeneratedKeys。

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testKeyHolder(){
String sql = "insert into user (username,password) values (:username,:password)";
User u = new User();
u.setUsername("555");
SqlParameterSource sqlParameterSource=new BeanPropertySqlParameterSource(u);
KeyHolder keyHolder = new GeneratedKeyHolder();
namedParameterJdbcTemplate.update(sql, sqlParameterSource, keyHolder);
int k = keyHolder.getKey().intValue();
System.out.println(k);
}

输出结果就是新增的主键。

paxos协议的理解

paxos

协议分为两大阶段,每个阶段又分为A/B两小步骤:

1.准备阶段(占坑阶段)

第一阶段A:Proposer选择一个提议编号n,向所有的Acceptor广播Prepare(n)请求。

第一阶段B:Acceptor接收到Prepare(n)请求,若提议编号n比之前接收的Prepare请求都要大,则承诺将不会接收提议编号比n小的提议,并且带上之前Accept的提议中编号小于n的最大的提议,否则不予理会。

2.接受阶段(提交阶段)

第二阶段A:整个协议最为关键的点:Proposer得到了Acceptor响应

如果未超过半数accpetor响应,直接转为提议失败;

如果超过多数Acceptor的承诺,又分为不同情况:

如果所有Acceptor都未接收过值(都为null),那么向所有的Acceptor发起自己的值和提议编号n,记住,一定是所有Acceptor都没接受过值;

如果有部分Acceptor接收过值,那么从所有接受过的值中选择对应的提议编号最大的作为提议的值,提议编号仍然为n。但此时Proposer就不能提议自己的值,只能信任Acceptor通过的值,维护一但获得确定性取值就不能更改原则;

第二阶段B:Acceptor接收到提议后,如果该提议版本号不等于自身保存记录的版本号(第一阶段记录的),不接受该请求,相等则写入本地。

paxos-flow

2pc和3pc

2PC

1.1 简述

2PC(tow phase commit)两阶段提交。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。

我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts)。

1.2 阶段1

在阶段1中,协调者发起一个提议,分别问询各参与者是否接受,如下图:

1

1.3 阶段2

在阶段2中,协调者根据参与者的反馈,提交或中止事务,如果参与者全部同意则提交,只要有一个参与者不同意就中止。

如下图:

3

1.4 优缺点

在异步环境并且没有节点宕机的模型下,2PC可以满足全认同、值合法、可结束,是解决一致性问题的一种协议。从协调者接收到一次事务请求、发起提议到事务完成,经过2PC协议后增加了2次RTT(propose+commit),带来的时延增加相对较少。

二阶段提交有几个缺点:

· 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

· 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

· 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

· 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

3pc

2.1 简述

三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

· 引入超时机制。同时在协调者和参与者中都引入超时机制。

· 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

2.2 CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

· 事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

· 响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

2.3 PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

· 发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

· 事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

· 响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

· 发送中断请求 协调者向所有参与者发送abort请求。

· 中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

2.4 doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

· 发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

· 事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

· 响应反馈 事务提交完之后,向协调者发送Ack响应。

· 完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

· 发送中断请求 协调者向所有参与者发送abort请求

· 事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

· 反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

· 中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

2

2.5 2pc和3pc的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。

在2PC中一个参与者的状态只有它自己和协调者知晓,假如协调者提议后自身宕机,在协调者备份启用前一个参与者又宕机,其他参与者就会进入既不能回滚、又不能强制commit的阻塞状态,直到参与者宕机恢复。

参与者如果在不同阶段宕机,我们来看看3PC如何应对:

· 阶段1: 协调者或协调者备份未收到宕机参与者的vote,直接中止事务;宕机的参与者恢复后,读取logging发现未发出赞成vote,自行中止该次事务

· 阶段2: 协调者未收到宕机参与者的precommit ACK,但因为之前已经收到了宕机参与者的赞成反馈(不然也不会进入到阶段2),协调者进行commit;协调者备份可以通过问询其他参与者获得这些信息,过程同理;宕机的参与者恢复后发现收到precommit或已经发出赞成vote,则自行commit该次事务

· 阶段3: 即便协调者或协调者备份未收到宕机参与者t的commit ACK,也结束该次事务;宕机的参与者恢复后发现收到commit或者precommit,也将自行commit该次事务

总结0319

–1.save和saveandflush
save不会立刻提交到数据库,flush则立刻提交生效,save可能只是修改在内存中的
–2.webhook
3.HMAC,SHA1
–4.ngrok
https://tonybai.com/2015/05/14/ngrok-source-intro/
–5.node vue
–6.JodaTimeConverter

–7.jackson

–8.jdbctemplate namedParameterJdbcTemplate
https://segmentfault.com/a/1190000010907688
–9.rabbit(exchange type,routingkey,queue,channel)
–0.zookeeper的配置属性,集群(hostname)
11.npm i和npm install的区别
https://blog.csdn.net/chern1992/article/details/79193211
–12.搭建hexo博客相关
https://blog.csdn.net/sinat_37781304/article/details/82729029
13.阿里巴巴为什么不用 ZooKeeper 做服务发现?
https://yq.aliyun.com/articles/599997
14.what are webhooks?
https://zapier.com/blog/what-are-webhooks/
You’re only limited by your imagination.
15.windows中的换行符为CRLF,而Linux下的换行符为LF
禁用自动转换,即将设置:git config –global core.autocrlf false