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

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


  • 首页

  • 归档

  • 搜索

一分钟看明白Java位运算

发表于 2020-11-22

前言

在阅读源码的时候,经常会碰到位运算,例如Java8中的HashMap部分源码。不同语言有各自的位运算方式,又大同小异。本篇文章带你一分钟彻底掌握Java中的位运算

1
2
3
4
5
6
7
java复制代码    /**
* 这段代码是计算hashcode的,其中的位运算和异或运算是为了降低hash碰撞概率
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

正文


1. 整数的机器级表示

对于Java,int类型长度为32位。我这里为了方便说明,假设某种语言的整数类型位4位,那么能表示的的范围是多少?

  • 如果是无符号类型,最大位二进制0~1111,也就是0~15
  • 如果有符号,最高位符号位1代表负数,0代表正数,因此能表示的范围为二进制1000~0111,也就是-8~7

为什么二进制1000等于-8?

这就涉及到补码编码,以下是其定义

也就是说,对于4位整数1000,最高位为1符号位,说明是一个负数。那么它的计算方式是:

  • -1 * 2^3 + 0 * 2^2 + 0 * 2^1 + 0 * 2^0=-8+0+0+0=-8

再例如二进制的1111,按照这个计算方式结果是:

  • -1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0=-8+4+2+1=-1

2. Java的位移运算

了解整数在机器中表示,就很容易能明白位移运算了。这里我拿Java中的int类型演示

int类型占32位,因此能表示的范围为-2^31 ~ 2^31-1,为了便于阅读,我这里把这个范围的十进制和二进制打印出来

可以看到

  • int类型的十进制范围表示为:-2147483648~2147483647
  • int类型的二进制表示范围为: 10000000 00000000 00000000 00000000 ~ 01111111 11111111 11111111 11111111
1
2
3
4
5
6
7
8
java复制代码public class BitShift {
public static void main(String[] args) {
System.out.println(Integer.MAX_VALUE); //2147483647
System.out.println(Integer.MIN_VALUE); //-2147483648
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE)); // 01111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE)); // 10000000 00000000 00000000 00000000
}
}

左移<<

二进制数向左移动k位,丢弃最高的k位,并在有右边补k个0

  • 01111111 11111111 11111111 11111111左移一位,符号位变成1,低位用0填充,所以结果位11111111 11111111 11111111 11111110,通过补码编码得出结果为-2
  • 10000000 00000000 00000000 00000000左移一位,符号位变为0,代表正数,低位同样用0填充,结果位00000000 00000000 00000000 00000000,因此结果为0
1
2
3
4
java复制代码System.out.println(Integer.MAX_VALUE<<1);   //-2
System.out.println(Integer.toBinaryString(-2)); //11111111 11111111 11111111 11111110
------------------------------------------------
System.out.println(Integer.MIN_VALUE<<1); //0

算术右移>>

算术右移到方式比较微妙,二进制右移动k位,丢弃低k位,并在高k位补最高位的值。其目的是为了负数的运算

如下:算术右移动后,高位原本是几就用几补充

  • 01111111 11111111 11111111 11111111算术右移1位为00111111 11111111 11111111 11111111
  • 10000000 00000000 00000000 00000000算术右移1位为11000000 00000000 00000000 00000000

可以看到十进制无论是正还是负数,逻辑右移一位相当于除以二

1
2
3
4
5
java复制代码System.out.println(Integer.MAX_VALUE>>1);   //  1073741823
System.out.println(Integer.MIN_VALUE>>1); // -1073741824

System.out.println(Integer.toBinaryString(Integer.MAX_VALUE>>1)); // 00111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE>>1)); // 11000000 00000000 00000000 00000000

逻辑右移>>>

逻辑右移就很简单了,直接高k位补0,丢弃低k位

1
2
3
4
5
java复制代码System.out.println(Integer.MAX_VALUE>>>1);   //  1073741823
System.out.println(Integer.MIN_VALUE>>>1); // 1073741824

System.out.println(Integer.toBinaryString(Integer.MAX_VALUE>>>1)); // 00111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE>>>1)); // 01000000 00000000 00000000 00000000

3. C语言中的位运算

  • 对于无符号整数,由移必须是逻辑的,也就是高位填充0
  • 对于有符号整数,有些机器会进行算数右移,有些机器会逻辑右移动

以下为8位整数的位移情况

补充

一个数左移动或者右移动k位,k非常大会是什么情况?

可以看到,对于Java int类型,右移或者左移32位相当于没移动,位移33位相当于位移1位。这是因为位运算会先将k取模再进行位移。比如k=33%32=1

1
2
3
4
5
6
7
8
java复制代码System.out.println(Integer.MAX_VALUE);  //2147483647
System.out.println(Integer.MAX_VALUE>>32); //2147483647

System.out.println(Integer.MAX_VALUE>>33); //1073741823
System.out.println(Integer.MAX_VALUE>>1); //1073741823

System.out.println(Integer.MAX_VALUE<<32); //2147483647
System.out.println(Integer.MAX_VALUE>>>32); //2147483647

最后附上完整测试代码

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
java复制代码public class BitShift {
public static void main(String[] args) {
System.out.println(Integer.MAX_VALUE); //2147483647
System.out.println(Integer.MIN_VALUE); //-2147483648
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE)); // 01111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE)); // 10000000 00000000 00000000 00000000

System.out.println(Integer.MAX_VALUE<<1); //-2
System.out.println(Integer.MIN_VALUE<<1); //0
System.out.println(Integer.toBinaryString(-2)); //11111111 11111111 11111111 11111110

System.out.println(Integer.MAX_VALUE>>1); // 1073741823
System.out.println(Integer.MIN_VALUE>>1); // -1073741824

System.out.println(Integer.toBinaryString(Integer.MAX_VALUE>>1)); // 00111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE>>1)); // 11000000 00000000 00000000 00000000

System.out.println(Integer.MAX_VALUE>>>1); // 1073741823
System.out.println(Integer.MIN_VALUE>>>1); // 1073741824

System.out.println(Integer.toBinaryString(Integer.MAX_VALUE>>>1)); // 00111111 11111111 11111111 11111111
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE>>>1)); // 01000000 00000000 00000000 00000000


System.out.println(Integer.MAX_VALUE); //2147483647
System.out.println(Integer.MAX_VALUE>>32); //2147483647

System.out.println(Integer.MAX_VALUE>>33); //1073741823
System.out.println(Integer.MAX_VALUE>>1); //1073741823

System.out.println(Integer.MAX_VALUE<<32); //2147483647
System.out.println(Integer.MAX_VALUE>>>32); //2147483647
}
}

本文转载自: 掘金

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

CompletableFuture让你的代码免受阻塞之苦

发表于 2020-11-22

前言

现在大部分的CPU都是多核,我们都知道想要提升我们应用程序的运行效率,就必须得充分利用多核CPU的计算能力;Java早已经为我们提供了多线程的API,但是实现方式略微麻烦,今天我们就来看看Java8在这方面提供的改善。


假设场景

现在你需要为在线教育平台提供一个查询用户详情的API,该接口需要返回用户的基本信息,标签信息,这两个信息存放在不同位置,需要远程调用来获取这两个信息;为了模拟远程调用,我们需要在代码里面延迟 1s;

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
typescript复制代码public interface RemoteLoader {

String load();

default void delay() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public class CustomerInfoService implements RemoteLoader {

public String load() {
this.delay();
return "基本信息";
}

}

public class LearnRecordService implements RemoteLoader {

public String load() {
this.delay();
return "学习信息";
}

}

同步方式实现版本

如果我们采用同步的方式来完成这个API接口,我们的实现代码:

1
2
3
4
5
6
7
8
9
ini复制代码@Test
public void testSync() {
long start = System.currentTimeMillis();
List<RemoteLoader> remoteLoaders = Arrays.asList(new CustomerInfoService(), new LearnRecordService());
List<String> customerDetail = remoteLoaders.stream().map(RemoteLoader::load).collect(toList());
System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

不出所料,因为调用的两个接口都是延迟了 1s ,所以结果大于2秒
result


Future实现的版本

接下来我们把这个例子用Java7提供的Future来实现异步的版本,看下效果如何呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码@Test
public void testFuture() {
long start = System.currentTimeMillis();
ExecutorService executorService = Executors.newFixedThreadPool(2);
List<RemoteLoader> remoteLoaders = Arrays.asList(new CustomerInfoService(), new LearnRecordService());
List<Future<String>> futures = remoteLoaders.stream()
.map(remoteLoader -> executorService.submit(remoteLoader::load))
.collect(toList());

List<String> customerDetail = futures.stream()
.map(future -> {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
return null;
})
.filter(Objects::nonNull)
.collect(toList());
System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

这次我们采用多线程的方式来改造了我们这个例子,结果还是比较满意的,时间大概花费了1s多一点
result

注意:这里我分成了两个Stream,如何合在一起用同一个Stream,那么在用future.get()的时候会导致阻塞,相当于提交一个任务执行完后才提交下一个任务,这样达不到异步的效果

这里我们可以看到虽然Future达到了我们预期的效果,但是如果需要实现将两个异步的结果进行合并处理就稍微麻一些,这里就不细说,后面主要看下CompletableFuture在这方面的改进


Java8并行流

以上我们用的是Java8之前提供的方法来实现,接下来我们来看下Java8中提供的并行流来实习我们这个例子效果怎样呢?

1
2
3
4
5
6
7
8
9
ini复制代码@Test
public void testParallelStream() {
long start = System.currentTimeMillis();
List<RemoteLoader> remoteLoaders = Arrays.asList(new CustomerInfoService(), new LearnRecordService());
List<String> customerDetail = remoteLoaders.parallelStream().map(RemoteLoader::load).collect(toList());
System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

运行的结果还是相当的满意,花费时间 1s 多点
result

和Java8之前的实现对比,我们发现整个代码会更加的简洁;

接下来我们把我们的例子改变一下,查询用户详情的接口还需要返回视频观看记录,用户的标签信息,购买订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typescript复制代码public class WatchRecordService implements RemoteLoader {
@Override
public String load() {
this.delay();
return "观看记录";
}
}

public class OrderService implements RemoteLoader {
@Override
public String load() {
this.delay();
return "订单信息";
}
}

public class LabelService implements RemoteLoader {
@Override
public String load() {
this.delay();
return "标签信息";
}
}

我们继续使用Java8提供的并行流来实现,看下运行的结果是否理想

1
2
3
4
5
6
7
8
9
10
11
12
13
14
scss复制代码@Test
public void testParallelStream2() {
long start = System.currentTimeMillis();
List<RemoteLoader> remoteLoaders = Arrays.asList(
new CustomerInfoService(),
new LearnRecordService(),
new LabelService(),
new OrderService(),
new WatchRecordService());
List<String> customerDetail = remoteLoaders.parallelStream().map(RemoteLoader::load).collect(toList());
System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

但是这次运行的结果不是太理想,花费时间超过了2秒


CompletableFuture

基本的用法
1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码@Test
public void testCompletableFuture() {
CompletableFuture<String> future = new CompletableFuture<>();
new Thread(() -> {
doSomething();
future.complete("Finish"); //任务执行完成后 设置返回的结果
}).start();
System.out.println(future.join()); //获取任务线程返回的结果
}

private void doSomething() {
System.out.println("doSomething...");
}

这种用法还有个问题,就是任务出现了异常,主线程会无感知,任务线程不会把异常给抛出来;这会导致主线程会一直等待,通常我们也需要知道出现了什么异常,做出对应的响应;改进的方式是在任务中try-catch所有的异常,然后调用future.completeExceptionally(e) ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码@Test
public void testCompletableFuture() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = new CompletableFuture<>();
new Thread(() -> {
try {
doSomething();
future.complete("Finish");
} catch (Exception e) {
future.completeExceptionally(e);
}
}).start();
System.out.println(future.get());
}

private void doSomething() {
System.out.println("doSomething...");
throw new RuntimeException("Test Exception");
}

从现在来看CompletableFuture的使用过程需要处理的事情很多,不太简洁,你会觉得看起来很麻烦;但是这只是表象,Java8其实对这个过程进行了封装,提供了很多简洁的操作方式;接下来我们看下如何改造上面的代码

1
2
3
4
5
6
7
8
arduino复制代码@Test
public void testCompletableFuture2() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
doSomething();
return "Finish";
});
System.out.println(future.get());
}

这里我们采用了supplyAsync,这下看起来简洁了许多,世界都明亮了; Java8不仅提供允许任务返回结果的supplyAsync,还提供了没有返回值的runAsync;让我们可以更加的关注业务的开发,不需要处理异常错误的管理


CompletableFuture异常处理

如果说主线程需要关心任务到底发生了什么异常,需要对其作出相应操作,这个时候就需要用到exceptionally

1
2
3
4
5
6
7
8
9
10
arduino复制代码@Test
public void testCompletableFuture2() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture
.supplyAsync(() -> {
doSomething();
return "Finish";
})
.exceptionally(throwable -> "Throwable exception message:" + throwable.getMessage());
System.out.println(future.get());
}

使用CompletableFuture来完成我们查询用户详情的API接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码@Test
public void testCompletableFuture3() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
List<RemoteLoader> remoteLoaders = Arrays.asList(
new CustomerInfoService(),
new LearnRecordService(),
new LabelService(),
new OrderService(),
new WatchRecordService());
List<CompletableFuture<String>> completableFutures = remoteLoaders
.stream()
.map(loader -> CompletableFuture.supplyAsync(loader::load))
.collect(toList());

List<String> customerDetail = completableFutures
.stream()
.map(CompletableFuture::join)
.collect(toList());

System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

这里依然是采用的两个Stream来完成的,执行的结果如下:

result

这个结果不太满意,和并行流的结果差不多,消耗时间 2秒多点;在这种场景下我们用CompletableFuture做了这么多工作,但是效果不理想,难道就有没有其他的方式可以让它在快一点吗?

为了解决这个问题,我们必须深入了解下并行流和CompletableFuture的实现原理,它们底层使用的线程池的大小都是CPU的核数Runtime.getRuntime().availableProcessors();那么我们来尝试一下修改线程池的大小,看看效果如何?


自定义线程池,优化CompletableFuture

使用并行流无法自定义线程池,但是CompletableFuture可以

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
scss复制代码@Test
public void testCompletableFuture4() throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
List<RemoteLoader> remoteLoaders = Arrays.asList(
new CustomerInfoService(),
new LearnRecordService(),
new LabelService(),
new OrderService(),
new WatchRecordService());

ExecutorService executorService = Executors.newFixedThreadPool(Math.min(remoteLoaders.size(), 50));

List<CompletableFuture<String>> completableFutures = remoteLoaders
.stream()
.map(loader -> CompletableFuture.supplyAsync(loader::load, executorService))
.collect(toList());

List<String> customerDetail = completableFutures
.stream()
.map(CompletableFuture::join)
.collect(toList());

System.out.println(customerDetail);
long end = System.currentTimeMillis();
System.out.println("总共花费时间:" + (end - start));
}

我们使用自定义线程池,设置最大的线程池数量50,来看下执行的结果
result

这下执行的结果比较满意了,1秒多点;理论上来说这个结果可以一直持续,直到达到线程池的大小50


并行流和CompletableFuture两者该如何选择

这两者如何选择主要看任务类型,建议

  1. 如果你的任务是计算密集型的,并且没有I/O操作的话,那么推荐你选择Stream的并行流,实现简单并行效率也是最高的
  2. 如果你的任务是有频繁的I/O或者网络连接等操作,那么推荐使用CompletableFuture,采用自定义线程池的方式,根据服务器的情况设置线程池的大小,尽可能的让CPU忙碌起来

CompletableFuture的其他常用方法
  1. thenApply、thenApplyAsync: 假如任务执行完成后,还需要后续的操作,比如返回结果的解析等等;可以通过这两个方法来完成
  2. thenCompose、thenComposeAsync: 允许你对两个异步操作进行流水线的操作,当第一个操作完成后,将其结果传入到第二个操作中
  3. thenCombine、thenCombineAsync:允许你把两个异步的操作整合;比如把第一个和第二个操作返回的结果做字符串的连接操作

总结

  1. Java8并行流的使用方式
  2. CompletableFuture的使用方式、异常处理机制,让我们有机会管理任务执行中发送的异常
  3. Java8并行流和CompletableFuture两者该如何选择
  4. CompletableFuture的常用方法

原创不易 转载请注明出处:silently9527.cn/archives/48

本文转载自: 掘金

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

PageHelper的高级使用,一个注解即可实现分页 一个注

发表于 2020-11-22

一个注解就能搞定分页,为何你的却那么复杂

一个java程序员都避免不了增删改查,最近这几天又开始去写增删改查的接口了。这个时候就避免不了做数据的分页。

所以这几天写下来发现,即使使用了 pagehelper 分页插件,去对数据物理分页。虽然 pagehelper 插件使用起来很简单了。

但是我是个非常懒的程序员,多一行非业务相关的代码,我都不想写。 一个函数下来,被来业务程序就四五行,一顿操作下来非业务代码都占用了3份1。我是非常不能接受这些无相关业务太多的代码嵌入到函数中的。

其中一点就是:代码阅读起来不清晰,代码结构冗余,我特别喜欢看即简洁,条理又清晰的的。

所以自己就封装了一个 基于 pagehelper 上分页工具使用,即简洁,使用又方便。

1、思路

使用Spring拦截器和aspectj的作用,MethodInterceptor.java 去做Mapper接口的拦截,然后对要分页的方法加上 自定义分页注解 @StartPage ,在拦截到该方法的时候就,方法执行前,利用 **pagehelper.startPage()**进行分页,然后继续执行该方法,将查询结果封装到Page里面。即可实现分页拦截,在不修改 pagehelper 的操作上。这样子就避免了,在也service层的业务中去写这个一个多余的代码。

还避免了,在业务的service函数中,出现误操作,提前执行了 PageHelper.start()方法。导致真正的分页方法不生效了。

比如:

1
2
3
4
5
6
java复制代码PageHelper.start(1,10);
........
cityMapper.selectById(id);
........
userMaper.selectList();
........

如上这种情况 userMaper.selectList(); 是需要分页的数据,但是在此之前执行了 cityMapper.selectById(id); 所以这个时候 cityMapper.selectById(id); 生效了。userMaper.selectList();分页 不生效。

使用这种注解方式,就不需要管这种情况,因为分页处理直接是在 当前mapper函数上处理了。

1、新建一个SpringBoot项目,将其做成一个starter。

2、相关文件介绍

封装一个 分页对象:

  • 1、Page.java
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Page<T> implements IPage<T>
{
private int pageNum = 1;
private int pageSize = 15;
private int pages;
private long total;
private int[] navigatepageNums = new int[]{0};
private List<T> rows = Collections.emptyList();
private boolean START_PAGE_ED = false;
private OrderBy[] orderBys = new OrderBy[0];

// 省略 set get
}
  • OrderBy: 排序查询对象
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
java复制代码/**
* <br>
*
* @author 永健
* @since 2020-06-16 15:02
*/
public class OrderBy
{
private String[] columns;
private Direction direction;

public OrderBy(OrderBy.Direction direction, String... properties) {
this.columns = properties;
this.direction = direction;
}

// set get

public enum Direction {
ASC,
DESC;

Direction()
{
}

public boolean isAscending()
{
return this.equals(ASC);
}

public boolean isDescending()
{
return this.equals(DESC);
}
}
}
  • PageHelperAutoConfiguration.java SpringBoot自动配置类,starter的关键操纵
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
java复制代码@Configuration
// 加载SqlSessionFactory
@ConditionalOnBean({SqlSessionFactory.class})
//允许自动读取配置文件
@EnableConfigurationProperties({PageHelperProperties.class})
// 在加载配置的类
@AutoConfigureAfter({MybatisAutoConfiguration.class})
// 加载自定义Config配置类
@Import(PageHelperConfig.class)
public class PageHelperAutoConfiguration
{

@Autowired
private List<SqlSessionFactory> sqlSessionFactoryList;

/**
* SpringBoot的参数自动配置类
*/
@Autowired
private PageHelperProperties properties;

public PageHelperAutoConfiguration()
{
}

/**
* 注册Pagehelper拦截器
*/
@PostConstruct
public void addPageInterceptor()
{
PageInterceptor interceptor = new PageInterceptor();
Properties properties = new Properties();
properties.putAll(this.properties.getProperties());
interceptor.setProperties(properties);
Iterator var3 = this.sqlSessionFactoryList.iterator();

while (var3.hasNext())
{
SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) var3.next();
sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
}
}
}
  • PageHelperProperties.java 就是读取 PageHelper 插件的配置类的。属性如下:
  • PageHelperConfig.java 自定义配置类,主要是配置 Spring的增强类 DefaultPointcutAdvisor
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
java复制代码/**
* <br>
*
* @author 永健
* @since 2020-11-11 11:19
*/
@Configuration
public class PageHelperConfig
{
/**
* 表达式
* 拦截哪个包下的所有方法
*/
private static final String EXECUTION = "execution(* %s..*.*(..))";

@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor2(PageHelperProperties pageHelperProperties) {

// 拦截器
PageMethodInterceptor interceptor = new PageMethodInterceptor();

// 切点
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
String mapperPackage = pageHelperProperties.getMapperPackage();
pointcut.setExpression(String.format(EXECUTION,mapperPackage));

DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut,interceptor);
return advisor;
}
}
  • PageMethodInterceptor.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
JAVA复制代码 /**
* 核心方法
*/
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable
{
Method method = methodInvocation.getMethod();
StartPage annotation = method.getAnnotation(StartPage.class);
if (annotation != null)
{
LOGGER.info("start page mapper");
}
Page<?> page = getPage(methodInvocation);

if (page != null)
{
if (!page.isStartPageEd())
{

PageHelper.startPage(page.getPageNum(), page.getPageSize(),PageUtils.filterSql(page.getOrderBy()));
Object proceed = methodInvocation.proceed();
page.setRows(proceed);
return proceed;
}
}
return methodInvocation.proceed();
}
  • @StartPage.java 分页注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码/**
* <p>
*
* </p>
*
* @author 永健
* @since 2020-11-11 11:52
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface StartPage
{
}

……

项目搭建完毕! 将项目达成一个 jar文件引入到另外一个SpringBoot项目中。

3、使用

在另外一个项目中使用。

1
2
3
4
5
java复制代码        <dependency>
<groupId>cn.yj.pagehelper</groupId>
<artifactId>annotation-pagehelper</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

1、pagehelper配置文件

1
2
3
4
5
6
yaml复制代码# 如下的配置文件都除了多增加一个 mapper-package 属性外,其他的一切 pagehelper 的配置都保持原有的功能。
pagehelper:
helper-dialect: mysql
mapper-package: com.ex.das.mapper # mapper接口下的包
reasonable: false # true:当默认值为pageNum<=0时,自动配置为1,pageSize>pages时候,默认为最后一页
supportMethodsArguments: false # 不支持参数接口分页

2、在CityMapper.java中的方法加上 注解@StartPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码/**
* <br>
*
* @author 永健
* @since 2020-11-11 15:02
*/
public interface CityMapper
{
/**
*
* @param page 分页参数
* @return
*/
@StartPage
@Select("select * from city")
List<City> selectPage(IPage<City> page);

/**
*
* @param page 分页参数
* @param city 查询对象
* @return
*/
@StartPage
List<City> selectList(IPage<City> page,@Param("params") City city);
}

3、Service、Controller中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public interface ICityService
{
// 可有返回值
Page<City> getCityPage(Page<City> page);

// 可没有返回值
void getCitysPage(Page<City> page);

// 带查询参数
Page<City> getCitysPage3(Page<City> page,City city);

Page<City> getCitysPage4(Page<City> page,City city);
}

CityServiceImpl.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
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
java复制代码@Service
public class CityServiceImpl implements ICityService
{

private final CityMapper cityMapper;

@Autowired
public CityServiceImpl(CityMapper cityMapper)
{
this.cityMapper = cityMapper;
}

/**
* 注解分页
* @param page 分页参数
* @return
*/
@Override
public Page<City> getCityPage(Page<City> page)
{
return page.setRows(cityMapper.selectPage(page));
}

/**
* 使用注解分页可不带返回值,已经将结果封装到page 中
* @param page
*/
@Override
public void getCitysPage(Page<City> page)
{
cityMapper.selectPage(page);
}

/**
* 不使用中注解分页
* @param page 分页参数
* @param city 查询参数
* @return
*/
@Override
public Page<City> getCitysPage3(Page<City> page, City city)
{
return page.startPage().setRows(cityMapper.selectList(page, city));
}

/**
* 使用注解分页
* @param page 分页参数
* @param city 查询参数
* @return
*/
@Override
public Page<City> getCitysPage4(Page<City> page, City city)
{
cityMapper.selectList(page, city);
return page;
}
}

SpringController.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
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码/**
* <br>
*
* @author 永健
* @since 2020-04-22 09:30
*/
@RestController
public class SpringController extends BaseController<City>
{

@Autowired
ICityService cityService;


@GetMapping("/list")
public R page1()
{
return success(cityService.getCityPage(page()));
}

@GetMapping("/list2")
public R page2()
{
Page<City> page = page(new OrderBy(OrderBy.Direction.ASC, "id"));
cityService.getCitysPage(page);
return success(page);
}

@GetMapping("/list3")
public R page3(City city)
{
return success(cityService.getCitysPage3(page(new OrderBy(OrderBy.Direction.ASC, "id")), city));
}

@GetMapping("/list4")
public R page4(City city)
{
return success(cityService.getCitysPage4(page(new OrderBy(OrderBy.Direction.ASC, "id")), city));
}
}

controller 中的使用,为了更加将分页参数与查询参数分离开。抽取一个共用的BaseController 也可以将封装好统一的返回对象给客户端,这样子即规范统一又好管理,简洁又方便。还可以放一些当前用户的信息啥的….

所以 BaseController.java 负责获取分页参数和封装好分页对象。包括统一响应客户端的对象,不用特意去 new一个对象了直接重父类拿

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
java复制代码public class BaseController<T>
{
// 。。。。。。
protected Page<T> page()
{
int pageNumber = getParamsInt("pageNum");
int pageSize = getParamsInt("pageSize");
return new Page<>(pageNumber, pageSize);
}

/**
*
* @param orderBy 排序对象
* @return
*/
protected Page<T> page(OrderBy... orderBy)
{
int pageNumber = getParamsInt("pageNum");
int pageSize = getParamsInt("pageSize");
return new Page<>(pageNumber==0?1:pageNumber, pageSize==0?15:pageSize, orderBy);
}

private HttpServletRequest getRequest()
{
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return ((ServletRequestAttributes) attributes).getRequest();
}

private int getParamsInt(String name)
{
String parameter = getRequest().getParameter(name);
if (parameter == null || "".equals(parameter.trim()))
{
return 0;
}
return Integer.valueOf(parameter);
}
}

4、验证分页结果

项目启动访问获取数据。

  • list 接口

拦截器已经生效。

​

  • list3带参数的分页注解分页

使用结束了,一个注解就能简单的搞定分页操作,mysql的分页操作。你还在写多那几行非业务相关的代码吗????

当然,如果不使用该注解分页方式也可使用 PageHelper 的方式进行分页。两不冲突。引入到你的项目中根据自己需要定制自己的一套规则。没有固定死的,合适你的项目的就是好的。

因为我懒,所以我就爱动脑爱动手,减轻开发中的痛苦,去写一个更加适合我个人开发的组建,抽离开来,方便以后个人使用便捷,不需要重复去写。直接引用这一套代码即可,更能提升个人开发的效率。

如上代 码可完善地方很多,感兴趣的,可以将其继续扩展使用学习使用…

如果公司让你封装一个分页插件的使用,你会怎么去封装一个???

更多PageHelper的使用方式,请移步官网

github.com/pagehelper/…

本文转载自: 掘金

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

【Redis 实验室】 性能测试笔记——验证 Redis 网

发表于 2020-11-22
  1. 前言

最近在公司写代码,有一个工程由于担心 Redis 处理的数据量太大,存在性能问题,所以做了比较复杂的设计。但是我在想,Redis 的处理真的很耗时么?根据阿姆达尔定律,我们应该先发现耗时的点所在。目前接口的处理时长大概是100ms-200ms,为了找到最耗时的环节,我对 Redis 做了一下性能测试。

  1. 实验

2.1 环境

  1. Docker Redis
  2. Redis Tool 自带的 benchmark 压力测试工具。
  3. Linux 命令:top,iftop(网络流量top)

2.2 步骤

Redis 官方曾经宣布 Redis 的 QPS 可以达到 10W/s,并且性能瓶颈一般是网络层的带宽。

由此,我们设计实验验证一下这个观点。

  • 本地压力测试
  • 局域网压力测试

2.2.1 本机压力测试

  1. 我们在一个 Ubuntu 主机的 Docker 上安装 Redis,配置保持默认。
  2. 在 Terminal 上输入一下命令,默认50个并发客户端,一遍观察计算机的工况,一遍等待结束
1
shell复制代码root@ubuntu:~# redis-benchmark -p 6379
  1. 计算机工况:
    硬件 描述
    CPU us、sy 均上升,达到 97%+,由于Redis单线程,redis-server 进程不会突破100%
    网络IO 无流量
  2. 数据与简单结论(具体可以查看文章底部:附录)
    指标 描述
    时间复杂度为O(1) 的 QPS 5W+,99%的请求在 1ms 内返回
    时间复杂度O(n) 的 QPS (范围查询:range) 8k-1.5W+,99%的请求在 4ms 内返回

2.2.2 局域网压力测试

  1. 我们在一个 Ubuntu 主机的 Docker 上安装 Redis,配置保持默认。
  2. 在局域网上,通过 Mac 向 Ubuntu 发送压力测试请求。局域网配置是普通家庭设置,路由器是 小米路由器4
  3. 在 Terminal 上输入一下命令,默认50个并发客户端,一遍观察计算机的工况,一遍等待结束
1
shell复制代码➜  ~ redis-benchmark -h home.qpm.com
  1. 计算机工况

硬件 描述
CPU 使用率稳定在 27%+
网络IO 监控到流量,如上图
  1. 数据与简单结论(具体可以查看文章底部:附录)
    指标 描述
    时间复杂度为O(1) 的 QPS 8k+,99%的请求在 40ms 内返回,网络流量Total峰值:10-12Mb
    时间复杂度O(n) 的 QPS (范围查询:range) 1k-3k,99%的请求在 180ms 内返回,最长时间为:1700ms
    网络流量,在写入类 的 TPS 服务器->客户端的约 3M/s
    网路流量,在 range 类 查询 服务器 -> 客户端的带宽表显示 40M/s, 但 Mac 显示只收到 9M/s

2.3 测试结论

如 Redis 官方所言,网络带宽通常是 Redis 的瓶颈

从上面的数据可知,在一个比较差的网络配置下,Redis 的性能是完全没发掘出来的。在网络拥塞情况下,Redis 的性能表现抖动还会比较明显。

2.4 心得

既然网络带宽会成为瓶颈,那在大量使用 Redis 请求的情况下,可以优先考虑 Pipeline 的交互方式。当一段代码中涉及 for 循环 + Redis,很容易就会成为系统最差的瓶颈,除非带宽已经赶上 CPU 的处理速度(这不太可能),否则 Pipeline 会非常有用地提供效率。

注意设计好使用 Redis 的数据结构,避免拉取过量的数据,或过度冗余地存储数据,这样也会消耗带宽,降低 Redis 中间件的服务性能

3 参考

  1. Redis压力测试工具benchmark
  2. 网络流量测试工具 iftop ifstats

4 附录

  • 本机压力测试报告.log
  • 远程压力测试报告.log

本文转载自: 掘金

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

java开发两年,连Spring的依赖注入的方式都搞不清楚,

发表于 2020-11-20

Spring依赖注入

常的java开发中,程序员在某个类中需要依赖其它类的方法,则通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理,spring提出了依赖注入的思想,即依赖类不由程序员实例化,而是通过spring容器帮我们new指定实例并且将实例注入到需要该对象的类中。依赖注入的另一种说法是“控制反转”,通俗的理解是:平常我们new一个实例,这个实例的控制权是我们程序员,而控制反转是指new实例工作不由我们程序员来做而是交给spring容器来做。

构造函数注入

在bean标签的内部使用constructor-arg标签就可以进行构造函数注入了。
constructor-arg标签的属性:

  • type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
  • index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值,索引的位置从0开始
  • name:用于给指定构造函数中指定名称的参数赋值
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他的bean类型数据,就是在IOC容器中出现过的bean对象
  • bean.xml*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountService" class="com.sks.service.imp.AccountServiceImpl">
<constructor-arg type="java.lang.String" value="张三"/>
<constructor-arg index="1" value="20"/>
<constructor-arg name="birthday" ref="birthday"/>
</bean>

<bean id="birthday" class="java.util.Date"/>

</beans>

AccountServiceImpl 类

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
typescript复制代码public class AccountServiceImpl implements AccountService {

private String name;
private Integer age;
private Date birthday;

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

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

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

@Override
public String toString() {
return "AccountServiceImpl{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}

public AccountServiceImpl(String name, Integer age, Date birthday) {
System.out.println("含参的构造方法被调用了");
this.name = name;
this.age = age;
this.birthday = birthday;
}

public AccountServiceImpl() {
System.out.println("构造方法调用");
}

@Override
public int addMoney(int money) {
System.out.println("向账户中加钱:" + money);
return 0;
}

@Override
public void saveAccount(Account account) {
System.out.println("saveAccount方法执行了");
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码	/**
* 测试构造函数注入
*/
@Test
public void test8() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");;

AccountService accountService = (AccountService) applicationContext.getBean("accountService");

System.out.println(accountService.toString());
}

运行测试以后,可以在控制台看到以下内容:

优点:在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
缺点:改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据也必须提供。

setter方法注入

在bean标签内部使用property标签进行配置。
property标签的属性:

  • name:用于指定注入时所调用的set方法名称
  • value:用于提供基本类型和String类型的数据
  • ref:用于指定其他的bean类型数据

这里面我们注入了基本类型、包装类型、日期类型数据。
AccountServiceImpl 类

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
typescript复制代码public class AccountServiceImpl implements AccountService {

private String name;
private Integer age;
private Date birthday;

public String getName() {
return name;
}

public void setName(String name) {
System.out.println("给name设置值");
this.name = name;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
System.out.println("给age设置值");
this.age = age;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
System.out.println("给birthday设置值");
this.birthday = birthday;
}

@Override
public String toString() {
return "AccountServiceImpl{" +
"name='" + name + '\'' +
", age=" + age +
", birthday=" + birthday +
'}';
}

public AccountServiceImpl(String name, Integer age, Date birthday) {
System.out.println("含参的构造方法被调用了");
this.name = name;
this.age = age;
this.birthday = birthday;
}

public AccountServiceImpl() {
System.out.println("构造方法调用");
}

@Override
public int addMoney(int money) {
System.out.println("向账户中加钱:" + money);
return 0;
}

@Override
public void saveAccount(Account account) {
System.out.println("saveAccount方法执行了");
}
}

bean.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountService" class="com.sks.service.imp.AccountServiceImpl">
<!--注入基本类型、包装类型、日期类型数据-->
<property name="age" value="22"/>
<property name="name" value="李四"/>
<property name="birthday" ref="birthday"/>
</bean>

<bean id="birthday" class="java.util.Date"/>
</beans>

测试

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码    /**
* 测试setter方法注入
*/
@Test
public void test9() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");;

AccountService accountService = (AccountService) applicationContext.getBean("accountService");

System.out.println(accountService.toString());
}

运行测试以后,可以在控制台看到以下内容:

优势:创建对象时没有明确的限制,可以直接使用默认构造函数。
缺点:如果又某个成员必须有值,则获取对象有可能是set方法没有执行。

对集合类型数据进行注入

AccountService2Impl 类

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
typescript复制代码public class AccountService2Impl implements AccountService2 {

private String[] myStrs;

private List<String> myList;

private Set<String> mySet;

private Map<String, String> myMap;

private Properties myProps;

public String[] getMyStrs() {
return myStrs;
}

public void setMyStrs(String[] myStrs) {
this.myStrs = myStrs;
}

public List<String> getMyList() {
return myList;
}

public void setMyList(List<String> myList) {
this.myList = myList;
}

public Set<String> getMySet() {
return mySet;
}

public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}

public Map<String, String> getMyMap() {
return myMap;
}

public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}

public Properties getMyProps() {
return myProps;
}

public void setMyProps(Properties myProps) {
this.myProps = myProps;
}

@Override
public String toString() {
return "AccountService2Impl{" +
"myStrs=" + Arrays.toString(myStrs) +
", myList=" + myList +
", mySet=" + mySet +
", myMap=" + myMap +
", myProps=" + myProps +
'}';
}

}

bean.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountService2" class="com.sks.service.imp.AccountService2Impl">
<property name="myStrs">
<array>
<value>AAA</value>
<value>BBB</value>
<value>CCC</value>
</array>
</property>

<property name="myList">
<list>
<value>list1</value>
<value>list2</value>
<value>list3</value>
</list>
</property>

<property name="mySet">
<set>
<value>set1</value>
<value>set2</value>
<value>set3</value>
</set>
</property>

<property name="myProps">
<props>
<prop key="name">柯森</prop>
<prop key="age">23</prop>
</props>
</property>

<property name="myMap">
<map>
<entry key="key1" value="value1"/>
<entry key="key2" value="value2"/>
<entry key="key3">
<value>value3</value>
</entry>
</map>
</property>
</bean>

</beans>

测试

1
2
3
4
5
6
7
8
9
10
11
csharp复制代码    /**
* 测试注入复杂类型/集合数据
*/
@Test
public void test10() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");

AccountService2 accountService2 = (AccountService2) applicationContext.getBean("accountService2");

System.out.println(accountService2.toString());
}

运行测试以后,可以看到在控制台打印输出了以下内容:

这说明我们注入集合类型数据成功了。

注解注入

用于注入数据的注解

bean.xml文件

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<!--创建bean时要扫描的包-->
<context:component-scan base-package="com.sks"/>

</beans>

AccountService4Impl 类

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
public class AccountService4Impl implements AccountService3 {

@Autowired
private AccountDao accountDao;

@Override
public void addMoney(int money) {
System.out.println("向账户中加钱....AccountService3Impl");
}
}

假设此时只有一个AccountDao的实现类,并且这个类也加上了@Repository注解,那么我们这样注入是可以成功的,但是如果容器中存在多个AccountDao的实现类,此时仅仅使用AccountDao是不能完成数据注入的,需要配合@Qualifier注解使用注入数据。

假设现有如下两个实现类,那我们应该怎么写才能成功注入数据?

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
less复制代码@Component
public class AccountService4Impl implements AccountService3 {

//错误写法,默认会去容器中查找名称为accountDao的bean
//@Autowired
//private AccountDao accountDao;

//正确写法
//@Autowired
//private AccountDao accountDao1

//正确写法
//@Autowired
//private AccountDao accountDao1;

//正确写法
@Autowired
@Qualifier("accountDao1")
private AccountDao accountDao;

@Override
public void addMoney(int money) {
System.out.println("向账户中加钱....AccountService3Impl");
}

}

测试

1
2
3
4
5
6
7
java复制代码    @Test
public void test2() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");
AccountService4Impl accountService4 = (AccountService4Impl) applicationContext.getBean("accountService4Impl");

System.out.println("accountService4:" + accountService4);
}

@Value注解的基本使用
在使用@Value注入基本类型和String类型的数据时使用”#“号;使用@Value读取配置文件的值时需要使用”$”符号,同时使用@PropertySource注解指定配置文件的位置。

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
less复制代码@Component
@PropertySource("classpath:db.properties")
public class AccountService4Impl implements AccountService3 {

@Autowired
@Qualifier("accountDao1")
private AccountDao accountDao;

//使用SPEL表达式只注入值
@Value("#{19 - 9}")
private int age;

@Value("zhangsan")
private String name;

//读取操作系统的名称
@Value("#{systemProperties['os.name']}")
private String osname;

//读取数据库配置文件中的值
@Value("${password}")
private String password;

@Override
public void addMoney(int money) {
System.out.println("向账户中加钱....AccountService3Impl");
}

}

测试

1
2
3
4
5
6
7
8
java复制代码    @Test
public void test2() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:bean.xml");
AccountService4Impl accountService4 = (AccountService4Impl) applicationContext.getBean("accountService4Impl");

System.out.println("accountService4:" + accountService4 + " " + accountService4.getName() + " " + accountService4.getAge());

}

断点调试可以看到如下结果,说明我们使用@Value注入数据成功。

最后

欢迎关注公众号:前程有光,领取一线大厂Java面试题总结+各知识点学习思维导+一份300页pdf文档的Java核心知识点总结!

本文转载自: 掘金

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

设计模式最佳套路—— 愉快地使用策略模式 何时使用策略模式

发表于 2020-11-20

作者|周密(之叶)

策略模式(Strategy Pattern)定义了一组策略,分别在不同类中封装起来,每种策略都可以根据当前场景相互替换,从而使策略的变化可以独立于操作者。比如我们要去某个地方,会根据距离的不同(或者是根据手头经济状况)来选择不同的出行方式(共享单车、坐公交、滴滴打车等等),这些出行方式即不同的策略。

何时使用策略模式

阿里开发规约-编程规约-控制语句-第六条 :超过 3 层的 if-else 的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现。相信大家都见过这种代码:

1
2
3
4
5
6
7
8
9
arduino复制代码if (conditionA) {
逻辑1
} else if (conditionB) {
逻辑2
} else if (conditionC) {
逻辑3
} else {
逻辑4
}

这种代码虽然写起来简单,但是很明显违反了面向对象的 2 个基本原则:

  • 单一职责原则(一个类应该只有一个发生变化的原因):因为之后修改任何一个逻辑,当前类都会被修改
  • 开闭原则(对扩展开放,对修改关闭):如果此时需要添加(删除)某个逻辑,那么不可避免的要修改原来的代码

因为违反了以上两个原则,尤其是当 if-else 块中的代码量比较大时,后续代码的扩展和维护就会逐渐变得非常困难且容易出错,使用卫语句也同样避免不了以上两个问题。因此根据我的经验,得出一个我个人认为比较好的实践:

  • if-else 不超过 2 层,块中代码 1~5 行,直接写到块中,否则封装为方法
  • if-else 超过 2 层,但块中的代码不超过 3 行,尽量使用卫语句
  • if-else 超过 2 层,且块中代码超过 3 行,尽量使用策略模式

愉快地使用策略模式

在 Spring 中,实现策略模式的方法多种多样,下面我分享一下我目前实现策略模式的 “最佳套路”(如果你有更好的套路,欢迎赐教,一起讨论哦)。

需求背景

我们平台的动态表单,之前专门用于模型输入的提交。现在业务方希望对表单能力进行开放,除了可用于模型提交,还可以用于业务方指定功能的提交(方式设计为绑定一个 HSF 泛化服务,HSF 即淘系内部的 RPC 框架)。加上我们在配置表单时的 “预览模式” 下的提交,那么表单目前便有以下三种提交类型:

  • 预览表单时的提交
  • 模型输入时的提交
  • 绑定 HSF 时的提交

现在,有请我的 “最佳套路” 上场。

第一步,定义策略接口

首先定义策略的接口,包括两个方法:

1、获取策略类型的方法

2、处理策略逻辑的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
csharp复制代码/**
* 表单提交处理器
*/
public interface FormSubmitHandler<R extends Serializable> {

/**
* 获得提交类型(返回值也可以使用已经存在的枚举类)
*
* @return 提交类型
*/
String getSubmitType();

/**
* 处理表单提交请求
*
* @param request 请求
* @return 响应,left 为返回给前端的提示信息,right 为业务值
*/
CommonPairResponse<String, R> handleSubmit(FormSubmitRequest request);
}
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
less复制代码/**
* 表单提交的请求
*/
@Getter
@Setter
public class FormSubmitRequest {

/**
* 提交类型
*
* @see FormSubmitHandler#getSubmitType()
*/
private String submitType;

/**
* 用户 id
*/
private Long userId;

/**
* 表单提交的值
*/
private Map<String, Object> formInput;

// 其他属性
}

其中,FormSubmitHandler 的 getSubmitType 方法用来获取表单的提交类型(即策略类型),用于根据客户端传递的参数直接获取到对应的策略实现;客户端传递的相关参数都被封装为 FormSubmitRequest,传递给 handleSubmit 进行处理。

第二步,相关策略实现

预览表单时的提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码@Component
public class FormPreviewSubmitHandler implements FormSubmitHandler<Serializable> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public String getSubmitType() { return "preview"; }

@Override
public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
logger.info("预览模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

return CommonPairResponse.success("预览模式提交数据成功!", null);
}
}

模型输入时的提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码@Component
public class FormModelSubmitHandler implements FormSubmitHandler<Long> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public String getSubmitType() { return "model"; }

@Override
public CommonPairResponse<String, Long> handleSubmit(FormSubmitRequest request) {
logger.info("模型提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

// 模型创建成功后获得模型的 id
Long modelId = createModel(request);

return CommonPairResponse.success("模型提交成功!", modelId);
}

private Long createModel(FormSubmitRequest request) {
// 创建模型的逻辑
return 123L;
}
}

HSF 模式的提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@Component
public class FormHsfSubmitHandler implements FormSubmitHandler<Serializable> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public String getSubmitType() { return "hsf"; }

@Override
public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
logger.info("HSF 模式提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

// 进行 HSF 泛化调用,获得业务方返回的提示信息和业务数据
CommonPairResponse<String, Serializable> response = hsfSubmitData(request);

return 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
typescript复制代码@Component
public class FormSubmitHandlerFactory implements InitializingBean, ApplicationContextAware {

private static final
Map<String, FormSubmitHandler<Serializable>> FORM_SUBMIT_HANDLER_MAP = new HashMap<>(8);

private ApplicationContext appContext;

/**
* 根据提交类型获取对应的处理器
*
* @param submitType 提交类型
* @return 提交类型对应的处理器
*/
public FormSubmitHandler<Serializable> getHandler(String submitType) {
return FORM_SUBMIT_HANDLER_MAP.get(submitType);
}

@Override
public void afterPropertiesSet() {
// 将 Spring 容器中所有的 FormSubmitHandler 注册到 FORM_SUBMIT_HANDLER_MAP
appContext.getBeansOfType(FormSubmitHandler.class)
.values()
.forEach(handler -> FORM_SUBMIT_HANDLER_MAP.put(handler.getSubmitType(), handler));
}

@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
appContext = applicationContext;
}
}

我们让 FormSubmitHandlerFactory 实现 InitializingBean 接口,在 afterPropertiesSet 方法中,基于 Spring 容器将所有 FormSubmitHandler 自动注册到 FORM_SUBMIT_HANDLER_MAP,从而 Spring 容器启动完成后, getHandler 方法可以直接通过 submitType 来获取对应的表单提交处理器。

第四步,使用 & 测试

在表单服务中,我们通过 FormSubmitHandlerFactory 来获取对应的表单提交处理器,从而处理不同类型的提交:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码@Service
public class FormServiceImpl implements FormService {

@Autowired
private FormSubmitHandlerFactory submitHandlerFactory;

public CommonPairResponse<String, Serializable> submitForm(@NonNull FormSubmitRequest request) {
String submitType = request.getSubmitType();

// 根据 submitType 找到对应的提交处理器
FormSubmitHandler<Serializable> submitHandler = submitHandlerFactory.getHandler(submitType);

// 判断 submitType 对应的 handler 是否存在
if (submitHandler == null) {
return CommonPairResponse.failure("非法的提交类型: " + submitType);
}

// 处理提交
return submitHandler.handleSubmit(request);
}
}

Factory 只负责获取 Handler,Handler 只负责处理具体的提交,Service 只负责逻辑编排,从而达到功能上的 “低耦合高内聚”。

写一个简单的 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码@RestController
public class SimpleController {

@Autowired
private FormService formService;

@PostMapping("/form/submit")
public CommonPairResponse<String, Serializable> submitForm(@RequestParam String submitType,
@RequestParam String formInputJson) {
JSONObject formInput = JSON.parseObject(formInputJson);

FormSubmitRequest request = new FormSubmitRequest();
request.setUserId(123456L);
request.setSubmitType(submitType);
request.setFormInput(formInput);

return formService.submitForm(request);
}
}

最后来个简单的测试:



我感觉到了,这就是非常流畅的感觉~

设想一次扩展

如果我们需要加入一个新的策略,比如绑定 FaaS 函数的提交,我们只需要添加一个新的策略实现即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typescript复制代码@Component
public class FormFaasSubmitHandler implements FormSubmitHandler<Serializable> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public String getSubmitType() { return "faas"; }

@Override
public CommonPairResponse<String, Serializable> handleSubmit(FormSubmitRequest request) {
logger.info("FaaS 模式的提交:userId={}, formInput={}", request.getUserId(), request.getFormInput());

// 进行 FaaS 函数调用,并获得业务方返回的提示信息和业务数据
CommonPairResponse<String, Serializable> response = faasSubmitData(request);

return response;
}

...
}

此时不需要修改任何代码,因为 Spring 容器重启时会自动将 FormFaasSubmitHandler 注册到 FormSubmitHandlerFactory 中 —— 面向 Spring 编程,太香惹~

本文转载自: 掘金

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

懒猿改变世界 - Java热部署思路

发表于 2020-11-19

好久不见,最近忙着加班,没时间写水文,公众号后台:炸哥你这号是不是废了…….

好不容易划两天水分享个工作中遇到的小问题以及如何解决问题

来自前端&测试 和 后端RD的冲突

好的,我不动,你来你来。

问题:我修复的bug,我要亲自部署到服务器,服务重启期间影响测试和前端调试,抱怨不断。

如果减少部署频率?

  1. 我改一天bug,晚上统一部署一次。(前端:那我这一白天干啥?一步一个坑,进展不下去)
  2. 我改一个bug发一次代码,于是上面的对话冲突产生。

部署服务的时间成本

改代码1min,代码打包(构建)56s,机器重启170s,四舍五入3min。
RD每天本地重启服务5-12次,单次大概3-8分钟,每天向Cargo部署3-5次,单次时长20-45分钟,部署频繁频次高、耗时长。插件提供的本地和远程热部署功能可让将代码变更秒级生效,再配合流量回放和远程日志查看器,提高RD自测联调效率。

这么一算,程序员真的是一天8小时写代码30min,和产品经理BBB,代码打包线上重启7小时。。。

企业开发如何减少测试联调中频繁部署耗时问题 - 代码热部署

对于程序员来说,懒驱动技术进步

我猜一定有解决方案,公司千人大群里喊一句,有没有热部署方案,果然,试用版已经小范围推行。

开始动手解决:
第一步:下载sonic插件

第二步:工程配置代理服务器地址

第三步:修改代码,实施发布。

第四步:确认结果

把6分钟缩短为4秒,真的太太太太香了,真的太太太感谢我司勤劳的的程序员用技术改变世界。

本文转载自: 掘金

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

分库分表神器 Sharding-JDBC,几千万的数据你不搞

发表于 2020-11-19

今天我们介绍一下 Sharding-JDBC框架和快速的搭建一个分库分表案例,为讲解后续功能点准备好环境。

一、Sharding-JDBC 简介

Sharding-JDBC 最早是当当网内部使用的一款分库分表框架,到2017年的时候才开始对外开源,这几年在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为 ShardingSphere,2020年4⽉16⽇正式成为 Apache 软件基⾦会的顶级项⽬。

随着版本的不断更迭 ShardingSphere 的核心功能也变得多元化起来。从最开始 Sharding-JDBC 1.0 版本只有数据分片,到 Sharding-JDBC 2.0 版本开始支持数据库治理(注册中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分布式事务 (支持 Atomikos、Narayana、Bitronix、Seata),如今已经迭代到了 Sharding-JDBC 4.0 版本。

现在的 ShardingSphere 不单单是指某个框架而是一个生态圈,这个生态圈 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 这三款开源的分布式数据库中间件解决方案所构成。

ShardingSphere 的前身就是 Sharding-JDBC,所以它是整个框架中最为经典、成熟的组件,我们先从 Sharding-JDBC 框架入手学习分库分表。

二、核心概念

在开始 Sharding-JDBC分库分表具体实战之前,我们有必要先了解分库分表的一些核心概念。

分片

一般我们在提到分库分表的时候,大多是以水平切分模式(水平分库、分表)为基础来说的,数据分片将原本一张数据量较大的表 t_order 拆分生成数个表结构完全一致的小数据量表 t_order_0、t_order_1、···、t_order_n,每张表只存储原大表中的一部分数据,当执行一条SQL时会通过 分库策略、分片策略 将数据分散到不同的数据库、表内。

数据节点

数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成,例如上图中 order_db_1.t_order_0、order_db_2.t_order_1 就表示一个数据节点。

逻辑表

逻辑表是指一组具有相同逻辑和数据结构表的总称。比如我们将订单表t_order 拆分成 t_order_0 ··· t_order_9 等 10张表。此时我们会发现分库分表以后数据库中已不在有 t_order 这张表,取而代之的是 t_order_n,但我们在代码中写 SQL 依然按 t_order 来写。此时 t_order 就是这些拆分表的逻辑表。

真实表

真实表也就是上边提到的 t_order_n 数据库中真实存在的物理表。

分片键

用于分片的数据库字段。我们将 t_order 表分片以后,当执行一条SQL时,通过对字段 order_id 取模的方式来决定,这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是 t_order 表的分片健。

这样以来同一个订单的相关数据就会存在同一个数据库表中,大幅提升数据检索的性能,不仅如此 sharding-jdbc 还支持根据多个字段作为分片健进行分片。

分片算法

上边我们提到可以用分片健取模的规则分片,但这只是比较简单的一种,在实际开发中我们还希望用 >=、<=、>、<、BETWEEN 和 IN 等条件作为分片规则,自定义分片逻辑,这时就需要用到分片策略与分片算法。

从执行 SQL 的角度来看,分库分表可以看作是一种路由机制,把 SQL 语句路由到我们期望的数据库或数据表中并获取数据,分片算法可以理解成一种路由规则。

咱们先捋一下它们之间的关系,分片策略只是抽象出的概念,它是由分片算法和分片健组合而成,分片算法做具体的数据分片逻辑。

分库、分表的分片策略配置是相对独立的,可以各自使用不同的策略与算法,每种策略中可以是多个分片算法的组合,每个分片算法可以对多个分片健做逻辑判断。

注意:sharding-jdbc 并没有直接提供分片算法的实现,需要开发者根据业务自行实现。

sharding-jdbc 提供了4种分片算法:

1、精确分片算法

精确分片算法(PreciseShardingAlgorithm)用于单个字段作为分片键,SQL中有 = 与 IN 等条件的分片,需要在标准分片策略(StandardShardingStrategy )下使用。

2、范围分片算法

范围分片算法(RangeShardingAlgorithm)用于单个字段作为分片键,SQL中有 BETWEEN AND、>、<、>=、<= 等条件的分片,需要在标准分片策略(StandardShardingStrategy )下使用。

3、复合分片算法

复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,根据多个字段处理业务逻辑。需要在复合分片策略(ComplexShardingStrategy )下使用。

4、Hint分片算法

Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中我们都是解析SQL 语句提取分片键,并设置分片策略进行分片。但有些时候我们并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定SQL的目标数据库和表信息,这也叫强制路由。

分片策略

上边讲分片算法的时候已经说过,分片策略是一种抽象的概念,实际分片操作的是由分片算法和分片健来完成的。

1、标准分片策略

标准分片策略适用于单分片键,此策略支持 PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。

其中 PreciseShardingAlgorithm 是必选的,用于处理 = 和 IN 的分片。RangeShardingAlgorithm 是可选的,用于处理BETWEEN AND, >, <,>=,<= 条件分片,如果不配置RangeShardingAlgorithm,SQL中的条件等将按照全库路由处理。

2、复合分片策略

复合分片策略,同样支持对 SQL语句中的 =,>, <, >=, <=,IN和 BETWEEN AND 的分片操作。不同的是它支持多分片键,具体分配片细节完全由应用开发者实现。

3、行表达式分片策略

行表达式分片策略,支持对 SQL语句中的 = 和 IN 的分片操作,但只支持单分片键。这种策略通常用于简单的分片,不需要自定义分片算法,可以直接在配置文件中接着写规则。

t_order_$->{t_order_id % 4} 代表 t_order 对其字段 t_order_id取模,拆分成4张表,而表名分别是t_order_0 到 t_order_3。

4、Hint分片策略

Hint分片策略,对应上边的Hint分片算法,通过指定分片健而非从 SQL中提取分片健的方式进行分片的策略。

分布式主键

数据分⽚后,不同数据节点⽣成全局唯⼀主键是⾮常棘⼿的问题,同⼀个逻辑表(t_order)内的不同真实表(t_order_n)之间的⾃增键由于⽆法互相感知而产⽣重复主键。

尽管可通过设置⾃增主键 初始值 和 步⻓ 的⽅式避免ID碰撞,但这样会使维护成本加大,乏完整性和可扩展性。如果后去需要增加分片表的数量,要逐一修改分片表的步长,运维成本非常高,所以不建议这种方式。

实现分布式主键⽣成器的方式很多,具体可以百度,网上有很多

为了让上手更加简单,ApacheShardingSphere 内置了UUID、SNOWFLAKE 两种分布式主键⽣成器,默认使⽤雪花算法(snowflake)⽣成64bit的⻓整型数据。不仅如此它还抽离出分布式主键⽣成器的接口,⽅便我们实现⾃定义的⾃增主键⽣成算法。

广播表

广播表:存在于所有的分片数据源中的表,表结构和表中的数据在每个数据库中均完全一致。一般是为字典表或者配置表 t_config,某个表一旦被配置为广播表,只要修改某个数据库的广播表,所有数据源中广播表的数据都会跟着同步。

绑定表

绑定表:那些分片规则一致的主表和子表。比如:t_order 订单表和 t_order_item 订单服务项目表,都是按 order_id 字段分片,因此两张表互为绑定表关系。

那绑定表存在的意义是啥呢?

通常在我们的业务中都会使用 t_order 和 t_order_item 等表进行多表联合查询,但由于分库分表以后这些表被拆分成N多个子表。如果不配置绑定表关系,会出现笛卡尔积关联查询,将产生如下四条SQL。

1
2
3
4
vbnet复制代码SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id

而配置绑定表关系后再进行关联查询时,只要对应表分片规则一致产生的数据就会落到同一个库中,那么只需 t_order_0 和 t_order_item_0 表关联即可。

1
2
vbnet复制代码SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id 
SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id

注意:在关联查询时 t_order 它作为整个联合查询的主表。所有相关的路由计算都只使用主表的策略,t_order_item 表的分片相关的计算也会使用 t_order 的条件,所以要保证绑定表之间的分片键要完全相同。

三、和JDBC的猫腻

从名字上不难看出,Sharding-JDBC 和 JDBC有很大关系,我们知道 JDBC 是一种 Java 语言访问关系型数据库的规范,其设计初衷就是要提供一套用于各种数据库的统一标准,不同厂家共同遵守这套标准,并提供各自的实现方案供应用程序调用。

但其实对于开发人员而言,我们只关心如何调用 JDBC API 来访问数据库,只要正确使用 DataSource、Connection、Statement 、ResultSet 等 API 接口,直接操作数据库即可。所以如果想在 JDBC 层面实现数据分片就必须对现有的 API 进行功能拓展,而 Sharding-JDBC 正是基于这种思想,重写了 JDBC 规范并完全兼容了 JDBC 规范。

对原有的 DataSource、Connection 等接口扩展成 ShardingDataSource、ShardingConnection,而对外暴露的分片操作接口与 JDBC 规范中所提供的接口完全一致,只要你熟悉 JDBC 就可以轻松应用 Sharding-JDBC 来实现分库分表。

因此它适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate,Mybatis,Spring JDBC Template 或直接使用的 JDBC。完美兼容任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP,Druid, HikariCP 等,几乎对主流关系型数据库都支持。

那 Sharding-JDBC 又是如何拓展这些接口的呢?想知道答案我们就的从源码入手了,下边我们以 JDBC API 中的 DataSource 为例看看它是如何被重写扩展的。

数据源 DataSource 接口的核心作用就是获取数据库连接对象 Connection,我们看其内部提供了两个获取数据库连接的方法 ,并且继承了 CommonDataSource 和 Wrapper 两个接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public interface DataSource  extends CommonDataSource, Wrapper {

/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
* @return a connection to the data source
*/
Connection getConnection() throws SQLException;

/**
* <p>Attempts to establish a connection with the data source that
* this {@code DataSource} object represents.
* @param username the database user on whose behalf the connection is
* being made
* @param password the user's password
*/
Connection getConnection(String username, String password)
throws SQLException;
}

其中 CommonDataSource 是定义数据源的根接口这很好理解,而 Wrapper 接口则是拓展 JDBC 分片功能的关键。

由于数据库厂商的不同,他们可能会各自提供一些超越标准 JDBC API 的扩展功能,但这些功能非 JDBC 标准并不能直接使用,而 Wrapper 接口的作用就是把一个由第三方供应商提供的、非 JDBC 标准的接口包装成标准接口,也就是适配器模式。

既然讲到了适配器模式就多啰嗦几句,也方便后边的理解。

适配器模式个种比较常用的设计模式,它的作用是将某个类的接口转换成客户端期望的另一个接口,使原本因接口不匹配(或者不兼容)而无法在一起工作的两个类能够在一起工作。比如用耳机听音乐,我有个圆头的耳机,可手机插孔却是扁口的,如果我想要使用耳机听音乐就必须借助一个转接头才可以,这个转接头就起到了适配作用。举个栗子:假如我们 Target 接口中有 hello() 和 word() 两个方法。

1
2
3
4
5
6
csharp复制代码public interface Target {

void hello();

void world();
}

可由于接口版本迭代Target 接口的 word() 方法可能会被废弃掉或不被支持,Adaptee 类的 greet()方法将代替hello() 方法。

1
2
3
4
5
6
7
8
9
csharp复制代码public class Adaptee {

public void greet(){

}
public void world(){

}
}

但此时旧版本仍然有大量 word() 方法被使用中,解决此事最好的办法就是创建一个适配器Adapter,这样就适配了 Target 类,解决了接口升级带来的兼容性问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typescript复制代码public class Adapter extends Adaptee implements Target {

@Override
public void world() {

}

@Override
public void hello() {
super.greet();
}

@Override
public void greet() {

}
}

而 Sharding-JDBC 提供的正是非 JDBC 标准的接口,所以它也提供了类似的实现方案,也使用到了 Wrapper 接口做数据分片功能的适配。除了 DataSource 之外,Connection、Statement、ResultSet 等核心对象也都继承了这个接口。

下面我们通过 ShardingDataSource 类源码简单看下实现过程,下图是继承关系流程图。

ShardingDataSource 类它在原 DataSource 基础上做了功能拓展,初始化时注册了分片SQL路由包装器、SQL重写上下文和结果集处理引擎,还对数据源类型做了校验,因为它要同时支持多个不同类型的数据源。到这好像也没看出如何适配,那接着向上看 ShardingDataSource 的继承类 AbstractDataSourceAdapter 。

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
scala复制代码@Getter
public class ShardingDataSource extends AbstractDataSourceAdapter {

private final ShardingRuntimeContext runtimeContext;

/**
* 注册路由、SQl重写上下文、结果集处理引擎
*/
static {
NewInstanceServiceLoader.register(RouteDecorator.class);
NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class);
NewInstanceServiceLoader.register(ResultProcessEngine.class);
}

/**
* 初始化时校验数据源类型 并根据数据源 map、分片规则、数据库类型得到一个分片上下文,用来获取数据库连接
*/
public ShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRule shardingRule, final Properties props) throws SQLException {
super(dataSourceMap);
checkDataSourceType(dataSourceMap);
runtimeContext = new ShardingRuntimeContext(dataSourceMap, shardingRule, props, getDatabaseType());
}

private void checkDataSourceType(final Map<String, DataSource> dataSourceMap) {
for (DataSource each : dataSourceMap.values()) {
Preconditions.checkArgument(!(each instanceof MasterSlaveDataSource), "Initialized data sources can not be master-slave data sources.");
}
}

/**
* 数据库连接
*/
@Override
public final ShardingConnection getConnection() {
return new ShardingConnection(getDataSourceMap(), runtimeContext, TransactionTypeHolder.get());
}
}

AbstractDataSourceAdapter 抽象类内部主要获取不同类型的数据源对应的数据库连接对象,实现 AutoCloseable 接口是为在使用完资源后可以自动将这些资源关闭(调用 close方法),那再看看继承类 AbstractUnsupportedOperationDataSource 。

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
java复制代码@Getter
public abstract class AbstractDataSourceAdapter extends AbstractUnsupportedOperationDataSource implements AutoCloseable {

private final Map<String, DataSource> dataSourceMap;

private final DatabaseType databaseType;

public AbstractDataSourceAdapter(final Map<String, DataSource> dataSourceMap) throws SQLException {
this.dataSourceMap = dataSourceMap;
databaseType = createDatabaseType();
}

public AbstractDataSourceAdapter(final DataSource dataSource) throws SQLException {
dataSourceMap = new HashMap<>(1, 1);
dataSourceMap.put("unique", dataSource);
databaseType = createDatabaseType();
}

private DatabaseType createDatabaseType() throws SQLException {
DatabaseType result = null;
for (DataSource each : dataSourceMap.values()) {
DatabaseType databaseType = createDatabaseType(each);
Preconditions.checkState(null == result || result == databaseType, String.format("Database type inconsistent with '%s' and '%s'", result, databaseType));
result = databaseType;
}
return result;
}

/**
* 不同数据源类型获取数据库连接
*/
private DatabaseType createDatabaseType(final DataSource dataSource) throws SQLException {
if (dataSource instanceof AbstractDataSourceAdapter) {
return ((AbstractDataSourceAdapter) dataSource).databaseType;
}
try (Connection connection = dataSource.getConnection()) {
return DatabaseTypes.getDatabaseTypeByURL(connection.getMetaData().getURL());
}
}

@Override
public final Connection getConnection(final String username, final String password) throws SQLException {
return getConnection();
}

@Override
public final void close() throws Exception {
close(dataSourceMap.keySet());
}
}

AbstractUnsupportedOperationDataSource 实现DataSource 接口并继承了 WrapperAdapter 类,它内部并没有什么具体方法只起到桥接的作用,但看着是不是和我们前边讲适配器模式的例子方式有点相似。

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码public abstract class AbstractUnsupportedOperationDataSource extends WrapperAdapter implements DataSource {

@Override
public final int getLoginTimeout() throws SQLException {
throw new SQLFeatureNotSupportedException("unsupported getLoginTimeout()");
}

@Override
public final void setLoginTimeout(final int seconds) throws SQLException {
throw new SQLFeatureNotSupportedException("unsupported setLoginTimeout(int seconds)");
}
}

WrapperAdapter 是一个包装器的适配类,实现了 JDBC 中的 Wrapper 接口,其中有两个核心方法 recordMethodInvocation 用于添加需要执行的方法和参数,而 replayMethodsInvocation 则将添加的这些方法和参数通过反射执行。仔细看不难发现两个方法中都用到了 JdbcMethodInvocation类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arduino复制代码public abstract class WrapperAdapter implements Wrapper {

private final Collection<JdbcMethodInvocation> jdbcMethodInvocations = new ArrayList<>();

/**
* 添加要执行的方法
*/
@SneakyThrows
public final void recordMethodInvocation(final Class<?> targetClass, final String methodName, final Class<?>[] argumentTypes, final Object[] arguments) {
jdbcMethodInvocations.add(new JdbcMethodInvocation(targetClass.getMethod(methodName, argumentTypes), arguments));
}

/**
* 通过反射执行 上边添加的方法
*/
public final void replayMethodsInvocation(final Object target) {
for (JdbcMethodInvocation each : jdbcMethodInvocations) {
each.invoke(target);
}
}
}

JdbcMethodInvocation 类主要应用反射通过传入的 method 方法和 arguments 参数执行对应的方法,这样就可以通过 JDBC API 调用非 JDBC 方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码@RequiredArgsConstructor
public class JdbcMethodInvocation {

@Getter
private final Method method;

@Getter
private final Object[] arguments;

/**
* Invoke JDBC method.
*
* @param target target object
*/
@SneakyThrows
public void invoke(final Object target) {
method.invoke(target, arguments);
}
}

那 Sharding-JDBC 拓展 JDBC API 接口后,在新增的分片功能里又做了哪些事情呢?

一张表经过分库分表后被拆分成多个子表,并分散到不同的数据库中,在不修改原业务 SQL 的前提下,Sharding-JDBC 就必须对 SQL进行一些改造才能正常执行。

大致的执行流程:SQL 解析 -> 执⾏器优化 -> SQL 路由 -> SQL 改写 -> SQL 执⾏ -> 结果归并 六步组成,一起瞅瞅每个步骤做了点什么。

SQL 解析

SQL解析过程分为词法解析和语法解析两步,比如下边这条查询用户订单的SQL,先用词法解析将SQL拆解成不可再分的原子单元。在根据不同数据库方言所提供的字典,将这些单元归类为关键字,表达式,变量或者操作符等类型。

1
sql复制代码SELECT order_no,price FROM t_order_ where user_id = 10086 and order_status > 0

接着语法解析会将拆分后的SQL转换为抽象语法树,通过对抽象语法树遍历,提炼出分片所需的上下文,上下文包含查询字段信息(Field)、表信息(Table)、查询条件(Condition)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit)等,并标记出 SQL中有可能需要改写的位置。

执⾏器优化

执⾏器优化对SQL分片条件进行优化,处理像关键字 OR这种影响性能的坏味道。

SQL 路由

SQL 路由通过解析分片上下文,匹配到用户配置的分片策略,并生成路由路径。简单点理解就是可以根据我们配置的分片策略计算出 SQL该在哪个库的哪个表中执行,而SQL路由又根据有无分片健区分出 分片路由 和 广播路由。

有分⽚键的路由叫分片路由,细分为直接路由、标准路由和笛卡尔积路由这3种类型。

标准路由

标准路由是最推荐也是最为常⽤的分⽚⽅式,它的适⽤范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。

当 SQL分片健的运算符为 = 时,路由结果将落⼊单库(表),当分⽚运算符是BETWEEN 或IN 等范围时,路由结果则不⼀定落⼊唯⼀的库(表),因此⼀条逻辑SQL最终可能被拆分为多条⽤于执⾏的真实SQL。

1
sql复制代码SELECT * FROM t_order  where t_order_id in (1,2)

SQL路由处理后

1
2
sql复制代码SELECT * FROM t_order_0  where t_order_id in (1,2)
SELECT * FROM t_order_1 where t_order_id in (1,2)

直接路由

直接路由是通过使用 HintAPI 直接将 SQL路由到指定⾄库表的一种分⽚方式,而且直接路由可以⽤于分⽚键不在SQL中的场景,还可以执⾏包括⼦查询、⾃定义函数等复杂情况的任意SQL。

比如根据 t_order_id 字段为条件查询订单,此时希望在不修改SQL的前提下,加上 user_id作为分片条件就可以使用直接路由。

笛卡尔积路由

笛卡尔路由是由⾮绑定表之间的关联查询产生的,查询性能较低尽量避免走此路由模式。


无分⽚键的路由又叫做广播路由,可以划分为全库表路由、全库路由、 全实例路由、单播路由和阻断路由这 5种类型。

全库表路由

全库表路由针对的是数据库 DQL和 DML,以及 DDL等操作,当我们执行一条逻辑表 t_order SQL时,在所有分片库中对应的真实表 t_order_0 ··· t_order_n 内逐一执行。

全库路由

全库路由主要是对数据库层面的操作,比如数据库 SET 类型的数据库管理命令,以及 TCL 这样的事务控制语句。

对逻辑库设置 autocommit 属性后,所有对应的真实库中都执行该命令。

1
ini复制代码SET autocommit=0;

全实例路由

全实例路由是针对数据库实例的 DCL 操作(设置或更改数据库用户或角色权限),比如:创建一个用户 order ,这个命令将在所有的真实库实例中执行,以此确保 order 用户可以正常访问每一个数据库实例。

1
sql复制代码CREATE USER order@127.0.0.1 identified BY '程序员内点事';

单播路由

单播路由用来获取某一真实表信息,比如获得表的描述信息:

1
ini复制代码DESCRIBE t_order;

t_order 的真实表是 t_order_0 ···· t_order_n,他们的描述结构相完全同,我们只需在任意的真实表执行一次就可以。

阻断路由

⽤来屏蔽SQL对数据库的操作,例如:

1
ini复制代码USE order_db;

这个命令不会在真实数据库中执⾏,因为 ShardingSphere 采⽤的是逻辑 Schema(数据库的组织和结构) ⽅式,所以无需将切换数据库的命令发送⾄真实数据库中。

SQL 改写

将基于逻辑表开发的SQL改写成可以在真实数据库中可以正确执行的语句。比如查询 t_order 订单表,我们实际开发中 SQL是按逻辑表 t_order 写的。

1
sql复制代码SELECT * FROM t_order

但分库分表以后真实数据库中 t_order 表就不存在了,而是被拆分成多个子表 t_order_n 分散在不同的数据库内,还按原SQL执行显然是行不通的,这时需要将分表配置中的逻辑表名称改写为路由之后所获取的真实表名称。

1
sql复制代码SELECT * FROM t_order_n

SQL执⾏

将路由和改写后的真实 SQL 安全且高效发送到底层数据源执行。但这个过程并不是简单的将 SQL 通过JDBC 直接发送至数据源执行,而是平衡数据源连接创建以及内存占用所产生的消耗,它会自动化的平衡资源控制与执行效率。

结果归并

将从各个数据节点获取的多数据结果集,合并成一个大的结果集并正确的返回至请求客户端,称为结果归并。而我们SQL中的排序、分组、分页和聚合等语法,均是在归并后的结果集上进行操作的。

四、快速实践

下面我们结合 Springboot + mybatisplus 快速搭建一个分库分表案例。

1、准备工作

先做准备工作,创建两个数据库 ds-0、ds-1,两个库中分别建表 t_order_0、t_order_1、t_order_2 、t_order_item_0、t_order_item_1、t_order_item_2,t_config,方便后边验证广播表、绑定表的场景。

表结构如下:

t_order_0 订单表

1
2
3
4
5
6
7
sql复制代码CREATE TABLE `t_order_0` (
`order_id` bigint(200) NOT NULL,
`order_no` varchar(100) DEFAULT NULL,
`create_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

t_order_0 与 t_order_item_0 互为关联表

1
2
3
4
5
6
7
sql复制代码CREATE TABLE `t_order_item_0` (
`item_id` bigint(100) NOT NULL,
`order_no` varchar(200) NOT NULL,
`item_name` varchar(50) DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

广播表 t_config

1
2
3
4
5
6
sql复制代码  `id` bigint(30) NOT NULL,
`remark` varchar(50) CHARACTER SET utf8 DEFAULT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`last_modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

ShardingSphere 提供了4种分片配置方式:

  • Java 代码配置
  • Yaml 、properties 配置
  • Spring 命名空间配置
  • Spring Boot配置

为让代码看上去更简洁和直观,后边统一使用 properties 配置的方式,引入 shardingsphere 对应的 sharding-jdbc-spring-boot-starter 和 sharding-core-common 包,版本统一用的 4.0.0-RC1。

2、分片配置

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-core-common</artifactId>
<version>4.0.0-RC1</version>
</dependency>

准备工作做完( mybatis 搭建就不赘述了),接下来我们逐一解读分片配置信息。

我们首先定义两个数据源 ds-0、ds-1,并分别加上数据源的基础信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码# 定义两个全局数据源
spring.shardingsphere.datasource.names=ds-0,ds-1

# 配置数据源 ds-0
spring.shardingsphere.datasource.ds-0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-0.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-0.url=jdbc:mysql://127.0.0.1:3306/ds-0?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=root

# 配置数据源 ds-1
spring.shardingsphere.datasource.ds-1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds-1.driverClassName=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds-1.url=jdbc:mysql://127.0.0.1:3306/ds-1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
spring.shardingsphere.datasource.ds-1.username=root
spring.shardingsphere.datasource.ds-1.password=root

配置完数据源接下来为表添加分库和分表策略,使用 sharding-jdbc 做分库分表需要我们为每一个表单独设置分片规则。

1
2
3
ini复制代码# 配置分片表 t_order
# 指定真实数据节点
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds-$->{0..1}.t_order_$->{0..2}

actual-data-nodes 属性指定分片的真实数据节点,$是一个占位符,{0..1}表示实际拆分的数据库表数量。

ds-$->{0..1}.t_order_$->{0..2} 表达式相当于 6个数据节点

  • ds-0.t_order_0
  • ds-0.t_order_1
  • ds-0.t_order_2
  • ds-1.t_order_0
  • ds-1.t_order_1
  • ds-1.t_order_2
1
2
3
4
5
ini复制代码### 分库策略
# 分库分片健
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.sharding-column=order_id
# 分库分片算法
spring.shardingsphere.sharding.tables.t_order.database-strategy.inline.algorithm-expression=ds-$->{order_id % 2}

为表设置分库策略,上边讲了 sharding-jdbc 它提供了四种分片策略,为快速搭建我们先以最简单的行内表达式分片策略来实现,在下一篇会介绍四种分片策略的详细用法和使用场景。

database-strategy.inline.sharding-column 属性中 database-strategy 为分库策略,inline 为具体的分片策略,sharding-column 代表分片健。

database-strategy.inline.algorithm-expression 是当前策略下具体的分片算法,ds-$->{order_id % 2} 表达式意思是 对 order_id字段进行取模分库,2 代表分片库的个数,不同的策略对应不同的算法,这里也可以是我们自定义的分片算法类。

1
2
3
4
5
6
7
8
9
ini复制代码# 分表策略
# 分表分片健
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id
# 分表算法
spring.shardingsphere.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order_$->{order_id % 3}
# 自增主键字段
spring.shardingsphere.sharding.tables.t_order.key-generator.column=order_id
# 自增主键ID 生成方案
spring.shardingsphere.sharding.tables.t_order.key-generator.type=SNOWFLAKE

分表策略 和 分库策略 的配置比较相似,不同的是分表可以通过 key-generator.column 和 key-generator.type 设置自增主键以及指定自增主键的生成方案,目前内置了SNOWFLAKE 和 UUID 两种方式,还能自定义的主键生成算法类,后续会详细的讲解。

1
2
ini复制代码# 绑定表关系
spring.shardingsphere.sharding.binding-tables= t_order,t_order_item

必须按相同分片健进行分片的表才能互为成绑定表,在联合查询时就能避免出现笛卡尔积查询。

1
2
ini复制代码# 配置广播表
spring.shardingsphere.sharding.broadcast-tables=t_config

广播表,开启 SQL解析日志,能清晰的看到 SQL分片解析的过程

1
2
ini复制代码# 是否开启 SQL解析日志
spring.shardingsphere.props.sql.show=true

3、验证分片

分片配置完以后我们无需在修改业务代码了,直接执行业务逻辑的增、删、改、查即可,接下来验证一下分片的效果。

我们同时向 t_order、t_order_item 表插入 5条订单记录,并不给定主键 order_id ,item_id 字段值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码public String insertOrder() {

for (int i = 0; i < 4; i++) {
TOrder order = new TOrder();
order.setOrderNo("A000" + i);
order.setCreateName("订单 " + i);
order.setPrice(new BigDecimal("" + i));
orderRepository.insert(order);

TOrderItem orderItem = new TOrderItem();
orderItem.setOrderId(order.getOrderId());
orderItem.setOrderNo("A000" + i);
orderItem.setItemName("服务项目" + i);
orderItem.setPrice(new BigDecimal("" + i));
orderItemRepository.insert(orderItem);
}
return "success";
}

看到订单记录被成功分散到了不同的库表中, order_id 字段也自动生成了主键ID,基础的分片功能就完成了。

那向广播表 t_config 中插入一条数据会是什么效果呢?

1
2
3
4
5
6
7
8
9
typescript复制代码public String config() {

TConfig tConfig = new TConfig();
tConfig.setRemark("我是广播表");
tConfig.setCreateTime(new Date());
tConfig.setLastModifyTime(new Date());
configRepository.insert(tConfig);
return "success";
}

发现所有库中 t_config 表都执行了这条SQL,广播表和 MQ广播订阅的模式很相似,所有订阅的客户端都会收到同一条消息。

简单SQL操作验证没问通,接下来在试试复杂一点的联合查询,前边我们已经把 t_order 、t_order_item 表设为绑定表,直接联表查询执行一下。

通过控制台日志发现,逻辑表SQL 经过解析以后,只对 t_order_0 和 t_order_item_0 表进行了关联产生一条SQL。

那如果不互为绑定表又会是什么情况呢?去掉 spring.shardingsphere.sharding.binding-tables试一下。

发现控制台解析出了 3条真实表SQL,而去掉 order_id 作为查询条件再次执行后,结果解析出了 9条SQL,进行了笛卡尔积查询。所以相比之下绑定表的优点就不言而喻了。

五、总结

以上对分库分表中间件 sharding-jdbc 的基础概念做了简单梳理,快速的搭建了一个分库分表案例,但这只是实践分库分表的第一步,下一篇我们会详细的介绍四种分片策略的具体用法和使用场景(必知必会),后边将陆续讲解自定义分布式主键、分布式数据库事务、分布式服务治理,数据脱敏等。

**转载自公众号:程序员那点事

本文转载自: 掘金

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

带你解惑大厂必会使用的 Stream流、方法引用🔥 第三章

发表于 2020-11-19

前言:

应广大读者的需要,霈哥给大家带来新一期的干货啦!

带你解惑大厂必会使用的 Lambda表达式、函数式接口

带你解惑大厂必会使用的 Stream流、方法引用

若对你和身边的朋友有帮助, 抓紧关注 IT霈哥 点赞! 点赞! 点赞! 评论!收藏! 分享给更多的朋友共同学习交流, 每天持续掘金离不开你的点赞支持!


第三章 Stream流

在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

3.1 引言

传统集合的多步遍历代码

几乎所有的集合(如Collection接口或Map接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Demo10ForEach {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
for (String name : list) {
System.out.println(name);
}
}
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

循环遍历的弊端

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:

  • for循环的语法就是“怎么做”
  • for循环的循环体才是“做什么”

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B;
  2. 然后再根据条件二过滤为子集C。

那怎么办?在Java 8之前的做法可能为:

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  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
java复制代码public class Demo11NormalFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");

List<String> zhangList = new ArrayList<>();
for (String name : list) {
if (name.startsWith("张")) {
zhangList.add(name);
}
}

List<String> shortList = new ArrayList<>();
for (String name : zhangList) {
if (name.length() == 3) {
shortList.add(name);
}
}

for (String name : shortList) {
System.out.println(name);
}
}
}

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。

那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

Stream的更优写法

下面来看一下借助Java 8的Stream API,什么才叫优雅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Demo12StreamFilter {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");

list.stream()
.filter(s -> s.startsWith("张"))
.filter(s -> s.length() == 3)
.forEach(s -> System.out.println(s));
}
}

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

3.2 流式思想概述

注意:请暂时忘记对传统IO流的固有印象!

整体来看,流式思想类似于工厂车间的“生产流水线”。

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。

这里的filter、map、skip都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

3.3 获取流方式

java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)

获取一个流非常简单,有以下几种常用的方式:

  • 所有的Collection集合都可以通过stream默认方法获取流;
  • Stream接口的静态方法of可以获取数组对应的流。

方式1 : 根据Collection获取流

首先,java.util.Collection接口中加入了default方法stream用来获取流,所以其所有实现类均可获取流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import java.util.*;
import java.util.stream.Stream;
/*
获取Stream流的方式

1.Collection中 方法
Stream stream()
2.Stream接口 中静态方法
of(T...t) 向Stream中添加多个数据
*/
public class Demo13GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// ...
Stream<String> stream1 = list.stream();

Set<String> set = new HashSet<>();
// ...
Stream<String> stream2 = set.stream();
}
}

方式2: 根据数组获取流

如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream接口中提供了静态方法of,使用很简单:

1
2
3
4
5
6
7
8
java复制代码import java.util.stream.Stream;

public class Demo14GetStream {
public static void main(String[] args) {
String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
Stream<String> stream = Stream.of(array);
}
}

备注:of方法的参数其实是一个可变参数,所以支持数组。

3.4 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。本小节中,终结方法包括count和forEach方法。
  • 非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

备注:本小节之外的更多方法,请自行参考API文档。

forEach : 逐一处理

虽然方法名字叫forEach,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的。

1
java复制代码void forEach(Consumer<? super T> action);

该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。例如:

1
2
3
4
5
6
7
8
9
java复制代码import java.util.stream.Stream;

public class Demo15StreamForEach {
public static void main(String[] args) {
Stream<String> stream = Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爷爷","蛇精","蝎子精");
//Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
stream.forEach((String str)->{System.out.println(str);});
}
}

在这里,lambda表达式(String str)->{System.out.println(str);}就是一个Consumer函数式接口的示例。

filter:过滤

可以通过filter方法将一个流转换成另一个子集流。方法声明:

1
java复制代码Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个Predicate函数式接口参数(可以是一个Lambda)作为筛选条件。

基本使用

Stream流中的filter方法基本使用的代码如:

1
2
3
4
5
6
java复制代码public class Demo16StreamFilter {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter((String s) -> {return s.startsWith("张");});
}
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

count:统计个数

正如旧集合Collection当中的size方法一样,流提供count方法来数一数其中的元素个数:

1
java复制代码long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:

1
2
3
4
5
6
7
java复制代码public class Demo17StreamCount {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s -> s.startsWith("张"));
System.out.println(result.count()); // 2
}
}

limit:取用前几个

limit方法可以对流进行截取,只取用前n个。方法签名:

1
java复制代码Stream<T> limit(long maxSize):获取Stream流对象中的前n个元素,返回一个新的Stream流对象

参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:

1
2
3
4
5
6
7
8
9
java复制代码import java.util.stream.Stream;

public class Demo18StreamLimit {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.limit(2);
System.out.println(result.count()); // 2
}
}

skip:跳过前几个

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:

1
java复制代码Stream<T> skip(long n): 跳过Stream流对象中的前n个元素,返回一个新的Stream流对象

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

1
2
3
4
5
6
7
8
9
java复制代码import java.util.stream.Stream;

public class Demo19StreamSkip {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.skip(2);
System.out.println(result.count()); // 1
}
}

concat:组合

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat:

1
java复制代码static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把参数列表中的两个Stream流对象a和b,合并成一个新的Stream流对象

备注:这是一个静态方法,与java.lang.String当中的concat方法是不同的。

该方法的基本使用代码如:

1
2
3
4
5
6
7
8
9
java复制代码import java.util.stream.Stream;

public class Demo20StreamConcat {
public static void main(String[] args) {
Stream<String> streamA = Stream.of("张无忌");
Stream<String> streamB = Stream.of("张翠山");
Stream<String> result = Stream.concat(streamA, streamB);
}
}

3.5 Stream综合案例

现在有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 打印整个队伍的姓名信息。

两个队伍(集合)的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Demo21ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("老子");
one.add("庄子");
one.add("孙子");
one.add("洪七公");

List<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("张三丰");
two.add("赵丽颖");
two.add("张二狗");
two.add("张天爱");
two.add("张三");
// ....
}
}

传统方式

使用for循环 , 示例代码:

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
java复制代码public class Demo22ArrayListNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...

List<String> two = new ArrayList<>();
// ...

// 第一个队伍只要名字为3个字的成员姓名;
List<String> oneA = new ArrayList<>();
for (String name : one) {
if (name.length() == 3) {
oneA.add(name);
}
}

// 第一个队伍筛选之后只要前3个人;
List<String> oneB = new ArrayList<>();
for (int i = 0; i < 3; i++) {
oneB.add(oneA.get(i));
}

// 第二个队伍只要姓张的成员姓名;
List<String> twoA = new ArrayList<>();
for (String name : two) {
if (name.startsWith("张")) {
twoA.add(name);
}
}

// 第二个队伍筛选之后不要前2个人;
List<String> twoB = new ArrayList<>();
for (int i = 2; i < twoA.size(); i++) {
twoB.add(twoA.get(i));
}

// 将两个队伍合并为一个队伍;
List<String> totalNames = new ArrayList<>();
totalNames.addAll(oneB);
totalNames.addAll(twoB);

// 打印整个队伍的姓名信息。
for (String name : totalNames) {
System.out.println(name);
}
}
}

运行结果为:

1
2
3
4
5
6
复制代码宋远桥
苏星河
洪七公
张二狗
张天爱
张三

Stream方式

等效的Stream流式处理代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Demo23StreamNames {
public static void main(String[] args) {
List<String> one = new ArrayList<>();
// ...

List<String> two = new ArrayList<>();
// ...

// 第一个队伍只要名字为3个字的成员姓名;
// 第一个队伍筛选之后只要前3个人;
Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);

// 第二个队伍只要姓张的成员姓名;
// 第二个队伍筛选之后不要前2个人;
Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);

// 将两个队伍合并为一个队伍;
// 根据姓名创建Person对象;
// 打印整个队伍的Person对象信息。
Stream.concat(streamOne, streamTwo).forEach(s->System.out.println(s));
}
}

运行效果完全一样:

1
2
3
4
5
6
复制代码宋远桥
苏星河
洪七公
张二狗
张天爱
张三

3.6 函数拼接与终结方法

在上述介绍的各种方法中,凡是返回值仍然为Stream接口的为函数拼接方法,它们支持链式调用;而返回值不再为Stream接口的为终结方法,不再支持链式调用。如下表所示:

方法名 方法作用 方法种类 是否支持链式调用
count 统计个数 终结 否
forEach 逐一处理 终结 否
filter 过滤 函数拼接 是
limit 取用前几个 函数拼接 是
skip 跳过前几个 函数拼接 是
concat 组合 函数拼接 是

第四章 方法引用

4.1 概述和方法引用符

来看一个简单的函数式接口以应用Lambda表达式 , 在accept方法中接收字符串 , 目的就是为了打印显示字符串 , 那么通过Lambda来使用它的代码很简单:

1
2
3
4
5
6
7
8
java复制代码public class DemoPrintSimple {
private static void printString(Consumer<String> data, String str) {
data.accept(str);
}
public static void main(String[] args) {
printString(s -> System.out.println(s), "Hello World");
}
}

由于lambda表达式中,调用了已经实现的println方法 ,可以使用方法引用替代lambda表达式.

符号表示 : ::

符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用。

**应用场景 : **如果Lambda要表达的函数方案 , 已经存在于某个方法的实现中,那么则可以使用方法引用。

如上例中,System.out对象中有个println(String)方法 , 恰好就是我们所需要的 , 那么对于Consumer接口作为参数,对比下面两种写法,完全等效:

  • Lambda表达式写法:s -> System.out.println(s);
    拿到参数之后经Lambda之手,继而传递给System.out.println方法去处理。
  • 方法引用写法:System.out::println
    直接让System.out中的println方法来取代Lambda。

**推导与省略 : ** 如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。函数式接口是Lambda的基础,而方法引用是Lambda的简化形式。

4.2 方法引用简化

只要“引用”过去就好了:

1
2
3
4
5
6
7
8
java复制代码public class DemoPrintRef {
private static void printString(Consumer<String> data, String str) {
data.accept(str);
}
public static void main(String[] args) {
printString(System.out::println, "HelloWorld");
}
}

请注意其中的双冒号::写法,这被称为“方法引用”,而双冒号是一种新的语法。

4.3 扩展的引用方式

对象名–引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法,代码为:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class DemoMethodRef {
public static void main(String[] args) {
String str = "hello";
printUP(str::toUpperCase);
}

public static void printUP(Supplier< String> sup ){
String apply =sup.get();
System.out.println(apply);
}
}

类名–引用静态方法

由于在java.lang.Math类中已经存在了静态方法random,所以当我们需要通过Lambda来调用该方法时,可以使用方法引用 , 写法是:

1
2
3
4
5
6
7
8
9
10
java复制代码public class DemoMethodRef {
public static void main(String[] args) {
printRanNum(Math::random);
}

public static void printRanNum(Supplier<Double> sup ){
Double apply =sup.get();
System.out.println(apply);
}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:n -> Math.abs(n)
  • 方法引用:Math::abs

类–构造引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用类名称::new的格式表示。首先是一个简单的Person类:

1
2
3
4
5
6
7
8
9
java复制代码public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

要使用这个函数式接口,可以通过方法引用传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Demo09Lambda {
public static void main(String[] args) {
String name = "tom";
Person person = createPerson(Person::new, name);
System.out.println(person);

}

public static Person createPerson(Function<String, Person> fun , String name){
Person p = fun.apply(name);
return p;

}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:name -> new Person(name)
  • 方法引用:Person::new

数组–构造引用

数组也是Object的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:

在应用该接口的时候,可以通过方法引用传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public class Demo11ArrayInitRef {   
public static void main(String[] args) {

int[] array = createArray(int[]::new, 3);
System.out.println(array.length);

}

public static int[] createArray(Function<Integer , int[]> fun , int n){
int[] p = fun.apply(n);
return p;

}
}

在这个例子中,下面两种写法是等效的:

  • Lambda表达式:length -> new int[length]
  • 方法引用:int[]::new

注意 :
方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为Lambda表达式的缩写形式 , 同学们可以尝试着 , 将之前使用lambda的地方 , 改写成方法引用的形式 ,不过要注意的是方法引用只能”引用”已经存在的方法!

后续精彩连载文章, 敬请期待:

  • 欢迎留言,写下你感兴趣的技术话题,霈哥话优先编写哦~!
    观看更多文章,请移步至 搞后端开发,边摸鱼边跟他学就够了,靠谱!🔥

若对你和身边的朋友有帮助, 抓紧关注 IT霈哥 点赞! 点赞! 点赞! 评论!收藏! 分享给更多的朋友共同学习交流, 每天持续更新离不开你的支持!

欢迎关注我的B站,将来会发布文章同步视频

1
2
![](https://gitee.com/songjianzaina/juejin_p13/raw/master/img/cfdb24b01fa6729fd734bf363337e1e645254641a86cc83a6ac024e2af3f5fbe)
欢迎关注我的公众号,获取更多资料

欢迎关注我的公众号,获取更多资料~

本文转载自: 掘金

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

Requests使用归纳与总结(一)

发表于 2020-11-18

该文章基于官方文档进行整理,数据接口均为官方文档中提供

按照官方的文档是说法,requests是一个非转基因的Python HTTP 库。功能强大,语法简洁。可以说,使用Python写Web程序,requests是不可避免的。

虽然说requests是使用简单,但是其大部分功能并非需要常常用到。但是在需要用到时又要去查文档就比较繁琐。所以也是想说做一个整理和总结。方便自己也方便他人。

附上官方文档地址

1
2
shell复制代码# 安装。注意,千万别安装成request,别少了末尾的s
pip install resquests

基础请求

首先导入Requests模块

1
python复制代码import requests

各类请求方式

1
2
3
4
5
6
7
python复制代码r = requests.get('http://httpbin.org/get')
# post带参
r = requests.post('http://httpbin.org/post', data={'key': 'value'})
r = requests.put('http://httpbin.org/put', data={'key': 'value'})
r = requests.delete('http://httpbin.org/delete')
r = requests.head('http://httpbin.org/get')
r = requests.options('http://httpbin.org/get')

requests允许传递URL参数,通过传递参数键值对给params变量,requests会自动构建好对应的URL。

1
2
3
4
5
6
7
8
python复制代码payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.get("http://httpbin.org/get", params=payload)
# 注意,字典里值为的None的键并不会被拼接到URL中
# 同时,你还可以将列表作为值进行传入
payload = {'key1': 'value1', 'key2': ['value2', 'value3']}
r = requests.get('http://httpbin.org/get', params=payload)
print(r.url)
>>> http://httpbin.org/get?key1=value1&key2=value2&key2=value3

响应内容

通过text返回响应内容的Unicode型数据。requests会自动解码来自服务器的内容。

1
2
3
python复制代码# 在需要读取文本信息时,可使用text进行获取
r = requests.get('http://httpbin.org/get')
r.text

通过content返回响应内容的bytes型(二进制)数据。

1
2
3
4
5
python复制代码# 在需要获取文件时,可通过content获取
# 例如获取一张图片并保存
r = requests.get('http://httpbin.org/get/xxx.jpg')
with open('example.jpg', 'wb') as img:
img.write(r)

通过json()处理响应的json数据。

1
2
3
python复制代码import requests
r = requests.get('http://httpbin.org/get')
r.json()

定制请求头

为请求添加头部,只需要传递dict给headers参数即可

1
2
3
4
5
6
7
python复制代码# HTTP头部大小写是不敏感的
headers = {
'token': token,
'content-type': 'application/json'
}
url = 'http://httpbin.org/get'
r = requests.get(url, headers=headers)

POST发送非表单形式数据

在post请求带有请求体时,可以使用json模块对数据进行编码

1
2
3
python复制代码url = 'http://httpbin.org/get'
body = {'data': data}
r = requests.post(url, data=json.dumps(body))

除了使用json进行编码外,还可以直接对json参数进行传值

1
2
3
python复制代码url = 'http://httpbin.org/get'
body = {'data': data}
r = requests.post(url, json=body)

通过POST上传文件

使用open方法以二进制形式读取文件后,即可方便地进行文件上传

1
2
3
4
5
6
7
8
python复制代码url = 'http://httpbin.org/post'
files = {'file': open('report.xls', 'rb')}
r = requests.post(url, files=files)

# 同时,可以显式地设置文件名、文件类型和请求头
url = 'http://httpbin.org/post'
files = {'file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel', {'Expires': '0'})}
r = requests.post(url, files=files)

发送cookie

可通过给参数cookies传参进行cookie的传递

1
2
3
4
5
6
7
8
9
10
python复制代码url = 'http://httpbin.org/cookies'
cookies = dict(cookies_are='working')
r = requests.get(url, cookies=cookies)

# 在跨域使用时,可以通过RequestsCookieJar进行域名和路径的定义
jar = requests.cookies.RequestsCookieJar()
jar.set('tasty_cookie', 'yum', domain='httpbin.org', path='/cookies')
jar.set('gross_cookie', 'blech', domain='httpbin.org', path='/elsewhere')
url = 'http://httpbin.org/cookies'
r = requests.get(url, cookies=jar)

获取响应信息

通过status_code获取响应状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码r = requests.get('http://httpbin.org/get')
r.status_code
>>> 200

# requests内置一个状态码查询对象
print(r.status_code == requests.codes.ok)
>>> True

# 如果发生了4xx或者5xx的错误响应,可以使用raise_for_status()函数来抛出异常
bad_r = requests.get('http://httpbin.org/status/404')
bad_r.status_code
>>> 404

bad_r.raise_for_status()
# 如果请求没有发生错误,则raise_for_status()返回None

通过headers获取响应头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码r = requests.get('http://httpbin.org/get')
r.headers
>>> {
'content-encoding': 'gzip',
'transfer-encoding': 'chunked',
'connection': 'close',
'server': 'nginx/1.0.4',
'x-runtime': '148ms',
'etag': '"e1ca502697e5c9317743dc078f67693f"',
'content-type': 'application/json'
}

# 同时,我们可以通过任意大小写形式来访问这些响应头字段
r.headers['Content-Type']
>>> 'application/json'

r.headers.get('content-type')
>>> 'application/json'

通过cookies获取cookie数据

1
2
3
4
python复制代码url = 'http://example.com/some/cookie/setting/url'
r = requests.get(url)
r.cookies['example_cookie_name']
>>> 'example_cookie_value'

重定向与请求历史

默认情况下,除了HEAD请求,requests会自动处理所有重定向

可以通过history方法进行重定向的追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python复制代码# 例如Github 将所有的HTTP请求重定向到HTTPS
r = requests.get('http://github.com')

r.url
>>> 'https://github.com/'

r.status_code
>>> 200

# 如果使用的时GET、OPTIONS、POST、PUT、PATCH、DELETE请求时,可以通过设置allow_redirects=False来禁用重定向

r = requests.get('http://github.com', allow_redirects=False)
r.status_code
>>> 301

# 也可以通过设置allow_redirects=True来启用HEAD请求的重定向
r = requests.head('http://github.com', allow_redirects=True)

最后

这篇文章算是关于requests基础使用的总结。后续会参照官方文档,进行一些高级用法的总结。梳理一下requests的高级用法,熟练使用requests在进行Web开发时会有事半功倍的效果。

本文转载自: 掘金

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

1…764765766…956

开发者博客

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