学习总结 2017-10

前言

记录个人在10月的记录总结

2017-10-05

windows查看端口占用和杀进程

查看端口占用
netstat -aon|findstr "49157"

找到进程pid后,杀掉该进程

taskkill /pid 12188 /f

2017-10-10

Java中的十个”单行代码编程”(One Liner)

本文列举了十个使用一行代码即可独立完成(不依赖其他代码)的业务逻辑,主要依赖的是Java8中的Lambda和Stream等新特性以及try-with-resources、JAXB等。

对列表/数组中的每个元素都乘以2

1
2
3
// Range是半开区间
int[] ia = range(1, 10).map(i -> i * 2).toArray();
List<Integer> result = range(1, 10).map(i -> i * 2).boxed().collect(toList());

计算集合/数组中的数字之和

1
2
3
4
range(1, 1000).sum();
range(1, 1000).reduce(0, Integer::sum);
Stream.iterate(0, i -> i + 1).limit(1000).reduce(0, Integer::sum);
IntStream.iterate(0, i -> i + 1).limit(1000).reduce(0, Integer::sum);

验证字符串是否包含集合中的某一字符串

1
2
3
4
5
final List<String> keywords = Arrays.asList("brown", "fox", "dog", "pangram");
final String tweet = "The quick brown fox jumps over a lazy dog. #pangram http://www.rinkworks.com/words/pangrams.shtml";
keywords.stream().anyMatch(tweet::contains);
keywords.stream().reduce(false, (b, keyword) -> b || tweet.contains(keyword), (l, r) -> l || r);

读取文件内容

原作者认为try with resources也是一种单行代码编程。

1
2
3
4
5
6
7
8
9
10
11
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String fileText = reader.lines().reduce("", String::concat);
}
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
List<String> fileLines = reader.lines().collect(toCollection(LinkedList<String>::new));
}
try (Stream<String> lines = Files.lines(new File("data.txt").toPath(), Charset.defaultCharset())) {
List<String> fileLines = lines.collect(toCollection(LinkedList<String>::new));
}

输出歌曲《Happy Birthday to You!》 - 根据集合中不同的元素输出不同的字符串

1
range(1, 5).boxed().map(i -> { out.print("Happy Birthday "); if (i == 3) return "dear NAME"; else return "to You"; }).forEach(out::println);

过滤并分组集合中的数字

1
Map<String, List<Integer>> result = Stream.of(49, 58, 76, 82, 88, 90).collect(groupingBy(forPredicate(i -> i > 60, "passed", "failed")));

获取并解析xml协议的Web Service

1
2
FeedType feed = JAXB.unmarshal(new URL("http://search.twitter.com/search.atom?&q=java8"), FeedType.class);
JAXB.marshal(feed, System.out);

获得集合中最小/最大的数字

1
2
3
4
5
6
7
int min = Stream.of(14, 35, -7, 46, 98).reduce(Integer::min).get();
min = Stream.of(14, 35, -7, 46, 98).min(Integer::compare).get();
min = Stream.of(14, 35, -7, 46, 98).mapToInt(Integer::new).min();
int max = Stream.of(14, 35, -7, 46, 98).reduce(Integer::max).get();
max = Stream.of(14, 35, -7, 46, 98).max(Integer::compare).get();
max = Stream.of(14, 35, -7, 46, 98).mapToInt(Integer::new).max();

并行处理

1
long result = dataList.parallelStream().mapToInt(line -> processItem(line)).sum();

集合上的各种查询(LINQ in Java)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<Album> albums = Arrays.asList(unapologetic, tailgates, red);
//筛选出至少有一个track评级4分以上的专辑,并按照名称排序后打印出来。
albums.stream()
.filter(a -> a.tracks.stream().anyMatch(t -> (t.rating >= 4)))
.sorted(comparing(album -> album.name))
.forEach(album -> System.out.println(album.name));
//合并所有专辑的track
List<Track> allTracks = albums.stream()
.flatMap(album -> album.tracks.stream())
.collect(toList());
//根据track的评分对所有track分组
Map<Integer, List<Track>> tracksByRating = allTracks.stream()
.collect(groupingBy(Track::getRating));

2017-10-17

Jackson的@JsonFormat注解日期少一天问题

项目中使用了@JsonFormat来进行格式化日期,却意外发现日期少了一天,
比如数据库存的日期是2017-10-15,转成json则变成了2017-10-14
于是查资料,发现这个注解有个坑,JsonFormat默认是不带时区
解决办法:

1
2
3
4
@JsonFormat(pattern="yyyy-MM-dd")
public Date getRegistDate() {
return this.registDate;
}

改成

1
2
3
4
@JsonFormat(pattern="yyyy-MM-dd",timezone="GMT+8")
public Date getRegistDate() {
return this.registDate;
}

加上时区即可,中国是东八区

2017-10-18

mysql如何解决幻读问题

mysql默认的事务隔离级别是repeatable-read,也就是可重复读,按照sql事务标准的话,这个隔壁级别是可以解决脏读和不可重复的问题的,但是不能解决幻读的问题,但是mysql却解决了幻读这个问题,那到底是怎么实现的呢?

官方文档

1
2
3
http://dev.mysql.com/doc/refman/5.0/en/innodb-record-level-locks.html
By default, InnoDB operates in REPEATABLE READ transaction isolation level and with the innodb_locks_unsafe_for_binlog system variable disabled. In this case, InnoDB uses next-key locks for searches and index scans, which prevents phantom rows (see Section 13.6.8.5, “Avoiding the Phantom Problem Using Next-Key Locking”).

准备的理解是,当隔离级别是可重复读,且禁用innodb_locks_unsafe_for_binlog的情况下,在搜索和扫描index的时候使用的next-key locks可以避免幻读。

关键点在于,是InnoDB默认对一个普通的查询也会加next-key locks,还是说需要应用自己来加锁呢?如果单看这一句,可能会以为InnoDB对普通的查询也加了锁,如果是,那和序列化(SERIALIZABLE)的区别又在哪里呢?

MySQL manual里还有一段:

1
2
3
4
5
13.2.8.5. Avoiding the Phantom Problem Using Next-Key Locking (http://dev.mysql.com/doc/refman/5.0/en/innodb-next-key-locking.html)
To prevent phantoms, InnoDB uses an algorithm called next-key locking that combines index-row locking with gap locking.
You can use next-key locking to implement a uniqueness check in your application: If you read your data in share mode and do not see a duplicate for a row you are going to insert, then you can safely insert your row and know that the next-key lock set on the successor of your row during the read prevents anyone meanwhile inserting a duplicate for your row. Thus, the next-key locking enables you to “lock” the nonexistence of something in your table.

我的理解是说,InnoDB提供了next-key locks,但需要应用程序自己去加锁。manual里提供一个例子:

1
SELECT * FROM child WHERE id > 100 FOR UPDATE;

SHOW ENGINE INNODB STATUS 来查看是否给表加上了锁。

mySQL manual里对可重复读里的锁的详细解释:

1
2
3
http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
For locking reads (SELECT with FOR UPDATE or LOCK IN SHARE MODE),UPDATE, and DELETE statements, locking depends on whether the statement uses a unique index with a unique search condition, or a range-type search condition. For a unique index with a unique search condition, InnoDB locks only the index record found, not the gap before it. For other search conditions, InnoDB locks the index range scanned, using gap locks or next-key (gap plus index-record) locks to block insertions by other sessions into the gaps covered by the range.

实验

一致性读和提交读,先看实验,实验四:

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
t Session A Session B
|
| START TRANSACTION; START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| | 1 | a |
| +----+-------+
| INSERT INTO t_bitfly
| VALUES (2, 'b');
| COMMIT;
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| | 1 | a |
| +----+-------+
|
| SELECT * FROM t_bitfly LOCK IN SHARE MODE;
| +----+-------+
| | id | value |
| +----+-------+
| | 1 | a |
| | 2 | b |
| +----+-------+
|
| SELECT * FROM t_bitfly FOR UPDATE;
| +----+-------+
| | id | value |
| +----+-------+
| | 1 | a |
| | 2 | b |
| +----+-------+
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| | 1 | a |
| +----+-------+
v

如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。

本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。

可以这么讲,InnoDB提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据。

1
2
3
4
5
6
http://dev.mysql.com/doc/refman/5.0/en/innodb-consistent-read.html
If you want to see the “freshest” state of the database, you should use either the READ COMMITTED isolation level or a locking read:
SELECT * FROM t_bitfly LOCK IN SHARE MODE;
------

mvcc

在官方文档中写道

1
2
3
http://dev.mysql.com/doc/refman/5.7/en/innodb-consistent-read.html
A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point of time, and no changes made by later or uncommitted transactions. The exception to this rule is that the query sees the changes made by earlier statements within the same transaction. This exception causes the following anomaly: If you update some rows in a table, a SELECT sees the latest version of the updated rows, but it might also see older versions of any rows. If other sessions simultaneously update the same table, the anomaly means that you might see the table in a state that never existed in the database.

一致性读是通过 MVCC 为查询提供了一个基于时间的点的快照。这个查询只能看到在自己之前提交的数据,而在查询开始之后提交的数据是不可以看到的。一个特例是,这个查询可以看到于自己开始之后的同一个事务产生的变化。这个特例会产生一些反常的现象

1
If the transaction isolation level is REPEATABLE READ (the default level), all consistent reads within the same transaction read the snapshot established by the first such read in that transaction. You can get a fresher snapshot for your queries by committing the current transaction and after that issuing new queries.

在默认隔离级别REPEATABLE READ下,同一事务的所有一致性读只会读取第一次查询时创建的快照

结论

结论:MySQL InnoDB的可重复读并不保证避免幻读,需要应用使用加锁读来保证。而这个加锁度使用到的机制就是next-key locks。

MySQL MVCC简介

什么是MVCC

MVCC是一种多版本并发控制机制。

MVCC是为了解决什么问题?

大多数的MYSQL事务型存储引擎,如,InnoDB,Falcon以及PBXT都不使用一种简单的行锁机制.事实上,他们都和MVCC–多版本并发控制来一起使用.
大家都应该知道,锁机制可以控制并发操作,但是其系统开销较大,而MVCC可以在大多数情况下代替行级锁,使用MVCC,能降低其系统开销.

MVCC实现

MVCC是通过保存数据在某个时间点的快照来实现的. 不同存储引擎的MVCC. 不同存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制.

具体实现分析

下面,我们通过InnoDB的MVCC实现来分析MVCC使怎样进行并发控制的.
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列,分别保存了这个行的创建时间,一个保存的是行的删除时间。这里存储的并不是实际的时间值,而是系统版本号(可以理解为事务的ID),没开始一个新的事务,系统版本号就会自动递增,事务开始时刻的系统版本号会作为事务的ID.下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的.

简单的小例子

1
2
3
create table zhang(
id int primary key auto_increment,
name varchar(20));

假设系统的版本号从1开始.

INSERT

InnoDB为新插入的每一行保存当前系统版本号作为版本号.
第一个事务ID为1;

1
2
3
4
5
start transaction;
insert into yang values(NULL,'zhang') ;
insert into yang values(NULL,'guo');
insert into yang values(NULL,'ji');
commit;

对应在数据中的表如下(后面两列是隐藏列,我们通过查询语句并看不到)

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 undefined
2 guo 1 undefined
3 ji 1 undefined

SELECT

InnoDB会根据以下两个条件检查每行记录:
a.InnoDB只会查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的.
b.行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前未被删除.
只有a,b同时满足的记录,才能返回作为查询结果.

DELETE

InnoDB会为删除的每一行保存当前系统的版本号(事务的ID)作为删除标识.
看下面的具体例子分析:
第二个事务,ID为2;

1
2
3
4
start transaction;
select * from zhang; //(1)
select * from zhang; //(2)
commit;

假设1

假设在执行这个事务ID为2的过程中,刚执行到(1),这时,有另一个事务ID为3往这个表里插入了一条数据;
第三个事务ID为3;

1
2
3
start transaction;
insert into zhang values(NULL,'tian');
commit;

这时表中的数据如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 undefined
2 guo 1 undefined
3 ji 1 undefined
4 tian 3 undefined

然后接着执行事务2中的(2),由于id=4的数据的创建时间(事务ID为3),执行当前事务的ID为2,而InnoDB只会查找事务ID小于等于当前事务ID的数据行,所以id=4的数据行并不会在执行事务2中的(2)被检索出来,在事务2中的两条select 语句检索出来的数据都只会下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 undefined
2 guo 1 undefined
3 ji 1 undefined

假设2

假设在执行这个事务ID为2的过程中,刚执行到(1),假设事务执行完事务3后,接着又执行了事务4;
第四个事务:

1
2
3
start transaction;
delete from zhang where id=1;
commit;

此时数据库中的表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 4
2 guo 1 undefined
3 ji 1 undefined
4 tian 3 undefined

接着执行事务ID为2的事务(2),根据SELECT 检索条件可以知道,它会检索创建时间(创建事务的ID)小于当前事务ID的行和删除时间(删除事务的ID)大于当前事务的行,而id=4的行上面已经说过,而id=1的行由于删除时间(删除事务的ID)大于当前事务的ID,所以事务2的(2)select * from yang也会把id=1的数据检索出来.所以,事务2中的两条select 语句检索出来的数据都如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 undefined
2 guo 1 undefined
3 ji 1 undefined

UPDATE

InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间.

假设3

假设在执行完事务2的(1)后又执行,其它用户执行了事务3,4,这时,又有一个用户对这张表执行了UPDATE操作:
第5个事务:

1
2
3
start transaction;
update yang set name='Guo' where id=2;
commit;

根据update的更新原则:会生成新的一行,并在原来要修改的列的删除时间列上添加本事务ID,得到表如下:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 4
2 guo 1 5
3 ji 1 undefined
4 tian 3 undefined
5 Guo 5 undefined

继续执行事务2的(2),根据select 语句的检索条件,得到下表:

id name 创建时间(事务ID) 删除时间(事务ID)
1 zhang 1 undefined
2 guo 1 undefined
3 ji 1 undefined

还是和事务2中(1)select 得到相同的结果.

2017-10-23

《Java8实战》笔记

  1. 行为参数化,就是一个方法接受多个不同的做为参数,并在内部使用它们,完成不同行为的能力。
  2. 行为参数化可以让代码更好的适应不断变化的要求,减轻代码的工作量。
  3. 传递代码,就是将新行为作为参数传递给方法。对比之前的实现方式,是通过类-》匿名内部类-》lambda表达式不断演变的,是代码变得越来越简洁

2017-10-30

System.arraycooy

System中提供了一个native静态方法arraycopy(),可以使用这个方法来实现数组之间的复制。对于一维数组来说,这种复制属性值传递,修改副本不会影响原来的值。对于二维或者一维数组中存放的是对象时,复制结果是一维的引用变量传递给副本的一维数组,修改副本时,会影响原来的数组。

CopyOnWrite

原理

Copy-On-Write简称COW,称为写入时复制,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。

分析源码

分析CopyOnWriteArrayList源代码的时候主要看两个方法,读方法和写方法,也即add()和get()方法

add()方法的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//复制数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将元素加入新的数组
newElements[len] = e;
//将新数组的引用指向旧的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}

add方法通过ReetrantLock实现了对add()方法的加锁,保证同一时刻只允许有一个线程对容器进行增加操作

get()方法的源代码

1
2
3
4
5
6
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}

get()方法直接调用内部的getArray()方法,而getArray()方法则直接返回成员变量array。

array指向一个数组,是CopyOnWriteArrayList的内部数据结构:

1
private transient volatile Object[] array;

array是一个volatile变量,其读、写操作具有Happends-Before关系。
具体来讲,线程W1通过set()方法“修改”集合后,线程R1能立刻通过get()方法得到array的最新值。

如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

使用场景

通过上面的分析,CopyOnWriteArrayList 有几个缺点:

  1. 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc

  2. 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用
因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。

透露的思想

如上面的分析CopyOnWriteArrayList表达的一些思想:

  1. 读写分离,读和写分开
  2. 最终一致性
  3. 使用另外开辟空间的思路,来解决并发冲突

2017-10-31

Hibernate Validator

Hibernate Validator 是 Bean Validation 的参考实现 。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。
在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。

类型

  1. Bean Validation 中内置的 constraint
注解 作用
@Valid 被注释的元素是一个对象,需要检查此对象的所有字段值
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式
  1. Hibernate Validator 附加的 constraint
注解 作用
@Email 被注释的元素必须是电子邮箱地址
@Length(min=, max=) 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range(min=, max=) 被注释的元素必须在合适的范围内
@NotBlank 被注释的字符串的必须非空
@URL(protocol=,host=, port=, regexp=, flags=) 被注释的字符串必须是一个有效的url
@CreditCardNumber 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
@ScriptAssert(lang=, script=, alias=) 要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的实现
@SafeHtml(whitelistType=, additionalTags=) classpath中要有jsoup包

hibernate补充的注解中,最后3个不常用,可忽略。
主要区分下@NotNull @NotEmpty @NotBlank 3个注解的区别:

注解 作用
@NotNull 任何对象的value不能为null
@NotEmpty 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
@NotBlank 只能用于字符串不为null,并且字符串trim()以后length要大于0

用法

  1. JavaBean 中加上注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class User {
@NotBlank
private String name;
//年龄要大于18岁
@Min(18)
private int age;
@Email
private String email;
//嵌套验证
@Valid
private Product products;
... //省略getter,setter
}
public class Product {
@NotBlank
private String name;
//价格在10元-50元之间
@Range(min=10,max=50)
private int price;
... //省略getter,setter
}

验证某一对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static String validateModel(Object obj) {//验证某一个对象
StringBuffer buffer = new StringBuffer(64);//用于存储验证后的错误信息
Validator validator = Validation.buildDefaultValidatorFactory()
.getValidator();
Set<ConstraintViolation<Object>> constraintViolations = validator
.validate(obj);//验证某个对象,,其实也可以只验证其中的某一个属性的
Iterator<ConstraintViolation<Object>> iter = constraintViolations
.iterator();
while (iter.hasNext()) {
String message = iter.next().getMessage();
buffer.append(message);
}
return buffer.toString();
}

整合Spring MVC

  1. 验证单个简单参数
    在Controller上加上@Validated注解,方法上加上如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
    * 查询优惠券信息
    * @param keywords 关键字
    * @param publishStatus 发布状态
    * @return
    */
    @Pagination
    @RequestMapping("/list")
    public Result listCoupon(String keywords, @Min(value=1, message="publishStatus not null") Integer publishStatus) {
    MapResult result = new MapResult();
    result.getInfo().put("keywords", keywords);
    result.getInfo().put("publishStatus", publishStatus);
    Page<CouponVo> page = couponService.pageCoupon(publishStatus, keywords);
    result.getInfo().put("page", page);
    return result;
    }
  2. 验证POJO参数, 在POJO里面加入验证注解,再在方法参数上加上@Validated注解

    1
    2
    3
    4
    5
    @RequestMapping("/add")
    public Result addUser(@Validated User user) {
    userService.add(user);
    return result;
    }

ForkJoin

Fork
Fork就是一个不断分枝的过程,在当前任务的基础上长出n多个子任务。

Join
Join是一个不断等待,获取任务执行结果的过程。

联想-归并排序的思想

下面演示了一段代码,该代码实现了从1-n的求和,采用了fork-join的思想,将大的任务拆分成小的子任务,再将这些子任务合并:

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
package java8;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
/**
* java7 forkJoinPool
* 任务拆分,工作窃取
* @author guoji_z
* create on 2017/10/31 12:08
*/
public class ForkJoinSumCalculator extends RecursiveTask<Long> {
private static final long THRESHOLD = 10_000;
private final long[] numbers;
private final int start;
private final int end;
public ForkJoinSumCalculator(long[] numbers, int start, int end) {
this.numbers = numbers;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
int length = end - start;
//如果长度小于或等于阈值,计算,否则,递归拆分任务
if(length <= THRESHOLD) {
return computeSequentially();
}
//利用另一个ForkJoinPool线程异步执行新创建的子任务,前半部分求和
ForkJoinSumCalculator leftTask = new ForkJoinSumCalculator(numbers, start, start + length / 2);
leftTask.fork();
//后半部分求和
ForkJoinSumCalculator rightTask = new ForkJoinSumCalculator(numbers, start + length / 2, end);
//同步执行第二个子任务
Long rightResult = rightTask.compute();
//读取第一个任务的结果,如果未完成,就阻塞
Long leftResult = leftTask.join();
//合并结果
return leftResult + rightResult;
}
private long computeSequentially() {
long sum = 0;
for(int i = start; i < end; i++) {
sum += numbers[i];
}
return sum;
}
private static long forkJoinSum(long n) {
long[] numbers = LongStream.rangeClosed(1, n).toArray();
ForkJoinTask<Long> task = new ForkJoinSumCalculator(numbers, 0, numbers.length);
return new ForkJoinPool().invoke(task);
}
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
System.out.println(forkJoinSum(10000));
System.out.println("Use time: " + (System.currentTimeMillis() - startTime));
}
}

核心:递归分解任务直到任务不可再分,双端队列,工作窃取算法