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

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


  • 首页

  • 归档

  • 搜索

【Redis入门】Redis事务中的乐与悲 👉写在前边 Re

发表于 2021-11-28

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

👉写在前边

  • 上篇我们了解了 Redis持久化技术–RDB&AOF,这篇我们将进入redis中的另一个重点–事务!!

Redis事务

定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。

Multi、Exec、discard?(SpringBoot怎么实现)

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。

组队的过程中可以通过discard来放弃组队。

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码    //增加乐观锁
jedis.watch(qtkey);

//3.判断库存
String qtkeystr = jedis.get(qtkey);
if(qtkeystr==null || "".equals(qtkeystr.trim())) {
System.out.println("未初始化库存");
jedis.close();
return false ;
}

int qt = Integer.parseInt(qtkeystr);
if(qt<=0) {
System.err.println("已经秒光");
jedis.close();
return false;
}

//增加事务
Transaction multi = jedis.multi();

//4.减少库存
//jedis.decr(qtkey);
multi.decr(qtkey);

//5.加人
//jedis.sadd(usrkey, uid);
multi.sadd(usrkey, uid);

//执行事务
List<Object> list = multi.exec();

//判断事务提交是否失败
if(list==null || list.size()==0) {
System.out.println("秒杀失败");
jedis.close();
return false;
}
System.err.println("秒杀成功");
jedis.close();

错误处理

组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
image.png
image.png

此处在multi过程中,exec之前,我们对一个v1去increase 导致发生了错误,组队期间的所有东西都会不成功

***与MySQL的区别!!!

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。
image.png

总结

在组队期间发生错误,就会回滚。

  • 而在执行阶段发生错误了,则不会回滚。

悲观锁

image.png

乐观锁

版本机制,类似CAS中的ABA问题解决
但这里不会循环去查询,而是直接就失败了? 想一想这里会不会引发其他的问题呢?

image.png

对比

image.png

秒杀库存变成负数问题

  • 乐观锁和悲观锁都能解决,因为在每次购买下单之前,都会先去检查一下
    • 乐观锁会去检查版本,发现版本号不一样了,直接就失败了!(那肯定不会出现负数的情况)
    • 而悲观锁呢? 悲观锁是等待上一个人执行完了,再来操作.这样可以保证不会出现负数的情况,同时也能够继续进行购买,不会说就此失败了!

乐观锁引发的库存遗留问题

解决–Lua脚本

将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
但是注意redis的lua脚本功能,只有在Redis 2.6以上的版本才可以使用。
利用lua脚本淘汰用户,解决超卖问题。
redis 2.6版本以后,通过lua脚本解决争抢问题,实际上是redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。

watch

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

事务三特性

  • 单独的隔离操作

​事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念

​队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

  • 不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

💠下篇预告

  • 下篇我们将聊聊redis中的主从复制!!!之后还会有哨兵模式,集群。

参考

  • 尚硅谷Redis6视频
  • 狂神说Redis视频

本文转载自: 掘金

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

验证mybatis批量插入能否一次能插入1万条数据

发表于 2021-11-28

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

之前聊到自己做过的功能优化,就说了通讯录同步的优化,详细见通讯录同步效率优化,提到用Mybatis批量插入数据,把上限1万条数据一次性的插入到表中。面试官对一次性插入1万条数据有疑问,认为不可以插入这么多数据,但是我做这个功能的时候确实是成功的,那具体能一次插入数据的上限我也不确定,后面就找时间做了下面这个实验。

首先自己搭建了SpringBoot+Mybatis的项目测试的,搭建步骤如下

1. 搭建测试工程

idea构建SpringBoot+MyBatis项目

gitee上代码:https://gitee.com/AJiSun/SpringBoot-MyBatis

File->New->Project

image.png

image.png

image.png

依赖:不选也行,后续在pom中添加,这里就选了一个mysql的依赖

image.png

image.png

添加需要的pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

然后就是新建文件夹,新建需要的文件,我的目录结构如下

image.png

application.yml中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码server:
port: 7070
spring:
application:
  name: ajisun-mybatis
datasource:
  driver-class-name: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/ajisun_mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&useInformationSchema=true
  username: root
  password: root
​
mybatis:
mapperLocations: classpath:mapper/*.xml
typeAliasesPackage: com.ajisun.coding.ajisunmybatis.entity
 #开启驼峰命名
configuration:
    map-underscore-to-camel-case: true

启动类中加上注解@MapperScan(启动的时候能够扫描到mapper)

1
2
3
4
5
6
7
8
Java复制代码@SpringBootApplication
@MapperScan("com.ajisun.coding.ajisunmybatis.mapper")
public class AjisunMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(AjisunMybatisApplication.class, args);
}
​
}

实体类中的内容

1
2
3
4
5
6
7
8
Java复制代码public class SyncEmployee implements Serializable {
   private Long syncId;
   private Long syncCode;
   private String employeeNum;
   private String imageUrl;
.......
   // set/get省略
}

mapper.java中内容

1
2
3
4
5
6
7
Java复制代码public interface SyncEmployeeMapper {
   /**
    * 查询列表
    * @return
    */
   List<SyncEmployee> selectList();
}

mapper.xml中内容

1
2
3
4
5
6
7
8
Java复制代码<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
​
<mapper namespace="com.ajisun.coding.ajisunmybatis.mapper.SyncEmployeeMapper">
   <select id="selectList" resultType="com.ajisun.coding.ajisunmybatis.entity.SyncEmployee">
      SELECT * FROM sync_employee;
   </select>
</mapper>

Interface service类 中的内容

1
2
3
4
5
6
7
Java复制代码public interface SyncEmployeeService {
   /**
    * 查询列表
    * @return
    */
   List<SyncEmployee> selectList();
}

实现Interface service的类中的内容

加上@Service 注解,标明这个类是一个service,会被springboot扫描。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java复制代码@Service
public class SyncEmployeeServiceImpl implements SyncEmployeeService {
​
   @Autowired
   private SyncEmployeeMapper syncEmployeeMapper;
​
   /**
    * 查询列表
    * @return
    */
   @Override
   public List<SyncEmployee> selectList() {
       return syncEmployeeMapper.selectList();
  }

接口文件controller的内容

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码@RestController
@RequestMapping("/user")
public class SyncEmployeeController {
​
   @Autowired
   private SyncEmployeeService syncEmployeeService;
​
   @GetMapping("/list")
   public ResponseEntity<List<SyncEmployee>>  list(){
       return ResponseEntity.ok(syncEmployeeService.selectList());
  }
}

至此就可以启动了,端口是yml中配置的7070,访问接口是【get】http://localhost:7070/user/list

2. 造数据,做测试

1. 表结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sql复制代码CREATE TABLE `ajisun_mybatis`.`sync_employee`  (
`sync_id` bigint(20) NOT NULL AUTO_INCREMENT,
`sync_code` bigint(30) NOT NULL COMMENT '同步批次号',
`employee_num` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '员工编码',
`name` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '员工姓名',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户ID',
`email` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '电子邮件',
`mobile` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '移动电话',
`inter_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '国际码',
`code_mobile` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '国际码+手机号',
`gender` tinyint(4) NULL DEFAULT NULL,
`cid` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '身份编码',
`status` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '员工状态',
`enabled_flag` tinyint(1) NULL DEFAULT NULL COMMENT '启用状态',
`entry_date` datetime(0) NULL DEFAULT NULL COMMENT '员工入职时间',
`birthday` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '出生日期',
`ldap_flag` tinyint(1) NOT NULL DEFAULT 0 COMMENT '否是ldap用户',
`password` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '密码',
`image_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '头像',
`sync_status` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'sync状态',
`sync_message` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'sync信息',
PRIMARY KEY (`sync_id`) USING BTREE,
INDEX `sync_emp_n1`(`sync_code`, `tenant_id`) USING BTREE,
INDEX `sync_emp_n2`(`email`, `sync_code`, `tenant_id`) USING BTREE,
INDEX `sync_emp_n3`(`mobile`, `sync_code`, `tenant_id`) USING BTREE,
INDEX `sync_emp_n4`(`employee_num`, `tenant_id`, `sync_code`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '员工同步中间表' ROW_FORMAT = Dynamic;

在SpringBoot-Mybatis 的项目中添加方法(具体参考代码,这里只说主要的地方)

2. mapper.xml中批量insert代码
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
Java复制代码<insert id="batchInsertData">
      insert into sync_employee (sync_id,sync_code,employee_num,name,tenant_id,
      email,mobile,inter_code,code_mobile,gender,cid,status,enabled_flag,
      entry_date,birthday,ldap_flag,image_url,sync_status,sync_message)
      values
       <foreach collection="syncEmployeeList" item="sync" separator=",">
          (
          #{sync.syncId},
          #{sync.syncCode},
          #{sync.employeeNum},
          #{sync.name},
          #{sync.tenantId},
          #{sync.email},
          #{sync.mobile},
          #{sync.interCode},
          #{sync.codeMobile},
          #{sync.gender},
          #{sync.cid},
          #{sync.status},
          #{sync.enabledFlag},
          #{sync.entryDate},
          #{sync.birthday},
          #{sync.ldapFlag},
          #{sync.imageUrl},
          0,
          #{sync.syncMessage}
          )
       </foreach>
   </insert>
3. service中的代码(循环造需要的数据量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Java复制代码@Override
   public void batchInsertData(Long syncCode) {
       List<SyncEmployee> syncEmployeeList = new ArrayList<SyncEmployee>();
       for (int i=10000;i<20000;i++){
           SyncEmployee ee = new SyncEmployee();
           ee.setSyncId(Long.valueOf(i));
           ee.setSyncCode(syncCode);
           ee.setEmail(i+"@qq.com");
           ee.setMobile("121000"+String.valueOf(+i));
           ee.setCodeMobile(ee.getInterCode()+"-"+ee.getMobile());
           ee.setEmployeeNum("ajisun"+i);
           ee.setEnabledFlag(1);
           ee.setGender(i%2);
           ee.setImageUrl("http://ajisun.com/头像.png");
           ee.setName(i+"");
           ee.setLdapFlag(i%2);
           ee.setEntryDate(new Date());
           ee.setStatus("ON");
           ee.setTenantId(0L);
           ee.setBirthday("1900-01-01");
           ee.setSyncMessage("new data "+i);
           syncEmployeeList.add(ee);
      }
       long start = System.currentTimeMillis();
       syncEmployeeMapper.batchInsertData(syncEmployeeList);
       long end = System.currentTimeMillis();
       System.out.println(end-start);
  }
4. 准备完毕,开始执行

通过上述方法一次构造一万条数据,然后通过sql批量插入

image.png

1
kotlin复制代码com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (4,879,714 > 4,194,304). You can change this valueon the server by setting the 'max_allowed_packet' variable.

报错了,根据错误信息知道 构造的一万条数据的大小超过了mysql限制的大小,但是我开发环境并没有问题啊(去开发环境看了下这个值确实比较大)

查看当前环境max_allowed_packet的大小,如下 这个版本默认是4M的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sql复制代码mysql> show global variables like 'max_allowed_packet';
+--------------------+---------+
| Variable_name      | Value   |
+--------------------+---------+
| max_allowed_packet | 4194304 |
+--------------------+---------+
1 row in set (0.01 sec)
​
mysql> select 4194304/1024/1024;
+-------------------+
| 4194304/1024/1024 |
+-------------------+
|        4.00000000 |
+-------------------+
1 row in set (0.00 sec)

image.png

把这个参数改大点试试(设置10M)

1
2
ini复制代码mysql> set global max_allowed_packet = 1024*1024*10;
Query OK, 0 rows affected (0.01 sec)

命令set采用的临时修改方式,需要打开新的会话才能生效,重启SpringBoot服务然后在调用接口。(还可以通过修改mysql配置文件使其永久生效 max_allowed_packet = 1024* 1024*10)。

还可以通过修改mysql配置文件使其永久生效 max_allowed_packet = 1024* 1024*10

再次调用接口就成功了
image.png

继续加大操作数据量,修改循环条件(3万数据量),数据量的总大小在14M左右

1
kotlin复制代码com.mysql.cj.jdbc.exceptions.PacketTooBigException: Packet for query is too large (14,637,233 > 10,485,760). You can change this value on the server by setting the 'max_allowed_packet' variable.

如上还是出现了相同的max_allowed_packet 错误,packet的大小超过了max_allowed_packet的值。

所以经过测试可以确定Mybatis究竟能插入多少数据取决于mysql的max_allowed_packet大小限制,而不是其自身的限制。

3. 总结

本文着重在于测试mybatis对批量插入的数据量是否有影响,经过测试发现并没有影响,主要是mysql自身对接收数据量的大小限制,通过参数max_allowed_packet控制。

但是使用mybatis大数据量批量插入要注意效率问题,这里只是测试,并不推荐这种方式。


我是纪先生,用输出倒逼输入而持续学习,持续分享技术系列文章,以及全网值得收藏好文,欢迎关注或者关注公众号,做一个持续成长的技术人。

4. 实际问题系列的历史文章

(也可以在掘金专栏中看其他相关文章)

1. 好用工具:慢SQL分析pt-query-digest;

2. 好用工具: pt-online-schame-change;

3. 怎么防止短信被白嫖;

4. 巧用双索引避免es出现索引不存在的问题;

5. 本地复现不了问题,那就线上debug;

6. 组织架构数据同步效率优化,百倍提速;

本文转载自: 掘金

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

深入浅出SynchronousQueue队列(三) 前言 T

发表于 2021-11-28

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

前言

本文继续讲解SynchronousQueue队列的非公平策略,SynchronousQueue通过两个内部类实现了公平策略和非公平策略的无缓存阻塞队列,每种操作都需要对应的互补操作同时进行才能完成,例如,入队操作必然对应出队操作,在不涉及超时和中断的情况下,必须等待另一个线程进行出队操作,相互匹配才能执行,否则就阻塞等待。

TransferStack

不同于公平策略下的操作,只有一种状态需要注意:

取消操作:match == this;

SNode

SNode基于栈的节点实现,变量与QNode不同,其中match在两个操作匹配上之后可以通过这个变量找到其匹配的节点,节点类型mode在使用上也有所不同,其他参数可参考TransferQueue的QNode

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
java复制代码    static final class SNode {
// next指向栈中下一个元素
volatile SNode next;
// 和当前节点匹配的节点
volatile SNode match;
// 等待线程
volatile Thread waiter;
// 节点内容
Object item;
// 节点类型
int mode;

SNode(Object item) {
this.item = item;
}

// CAS更新next字段
boolean casNext(SNode cmp, SNode val) {
return cmp == next &&
UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}


// 尝试s节点与当前节点进行匹配,成功则唤醒等待线程继续执行
boolean tryMatch(SNode s) {
// match == null 表明当前节点未被其他节点匹配上
// cas更新match字段为s
if (match == null &&
UNSAFE.compareAndSwapObject(this, matchOffset, null, s)) {
Thread w = waiter;
// 当前节点等待线程未被其他线程操作
if (w != null) { // waiters need at most one unpark
// 唤醒等待线程同时将waiter置空
waiter = null;
LockSupport.unpark(w);
}
return true;
}
// 判断当前节点是否已与s进行匹配
return match == s;
}

// 尝试取消操作 将match置为this
void tryCancel() {
UNSAFE.compareAndSwapObject(this, matchOffset, null, this);
}

// 判断tryCancel是否操作成功
boolean isCancelled() {
return match == this;
}

// 获取match和next在对象中的偏移量
private static final sun.misc.Unsafe UNSAFE;
private static final long matchOffset;
private static final long nextOffset;

static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = SNode.class;
matchOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("match"));
nextOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("next"));
} catch (Exception e) {
throw new Error(e);
}
}
}

变量部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码    // 数据请求操作 如take操作 代表未被匹配上的消费者
static final int REQUEST = 0;
// 数据保存操作 如put操作 代表未被匹配上的生产者
static final int DATA = 1;
// 有节点与其匹配,相当于已经有互补操作,使用上不是直接使用,可参考后面的源码部分
static final int FULFILLING = 2;

// 栈顶指针
volatile SNode head;

private static final sun.misc.Unsafe UNSAFE;
private static final long headOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> k = TransferStack.class;
headOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("head"));
} catch (Exception e) {
throw new Error(e);
}
}

CAS操作

CAS更新栈顶指针,比较简单

1
2
3
4
typescript复制代码boolean casHead(SNode h, SNode nh) {
return h == head &&
UNSAFE.compareAndSwapObject(this, headOffset, h, nh);
}

判断街道是否匹配

判断m对应的节点是否已经被匹配,和FULFILLING进行位与操作,判断m对应的栈节点处于FULFILLING状态,即已经匹配上了,在transfer里与栈顶节点非相同操作时会入栈一个节点,此节点的mode和普通节点不一样,会通过FULFILLING|mode操作更新mode,故这里最低位来区分是保存数据还是请求数据,高位来区分此节点是否是已经找到匹配节点的节点。

1
2
arduino复制代码/** Returns true if m has fulfilling bit set. */
static boolean isFulfilling(int m) { return (m & FULFILLING) != 0; }

snode节点

创建或重置SNode节点,如果为空则创建新的SNode节点,不为空则重置节点的mode和next属性。

1
2
3
4
5
6
ini复制代码    static SNode snode(SNode s, Object e, SNode next, int mode) {
if (s == null) s = new SNode(e);
s.mode = mode;
s.next = next;
return s;
}

transfer

和公平模式下的TransferQueue.transfer入队和出队操作类似,统一使用一个方法,即实现接口中的transfer方法来完成。不同点在于3个条件分支:

栈为空或栈顶元素操作类型和当前操作类型相同,入栈阻塞等待;

栈顶非匹配互补节点(匹配互补节点:已经和其他节点匹配上了,mode值高位为1),进行匹配操作;

帮助已经匹配的栈顶节点操作;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
scss复制代码    @SuppressWarnings("unchecked")
E transfer(E e, boolean timed, long nanos) {

SNode s = null; // constructed/reused as needed
// 节点类型,是put还是take操作,即是保存数据还是请求数据
int mode = (e == null) ? REQUEST : DATA;

for (;;) {
// 获取栈顶指针
SNode h = head;
// 栈为空
// 或栈顶节点和当前操作节点为相同操作
if (h == null || h.mode == mode) {
// 设置超时时间且超时时间小于等于0
if (timed && nanos <= 0) {
if (h != null && h.isCancelled())
// 栈顶非空且栈顶节点为取消操作状态
// 出栈,尝试将栈顶节点更新
casHead(h, h.next);
else
return null;
// 创建节点,尝试更新栈顶节点
} else if (casHead(h, s = snode(s, e, h, mode))) {
// 通过awaitFulfill方法自旋阻塞找到匹配操作的节点
SNode m = awaitFulfill(s, timed, nanos);
// 取消或超时
if (m == s) {
// 清理节点,取消本次操作
clean(s);
return null;
}
// 栈顶节点更新为s的next元素
// 出栈栈顶2个节点元素,帮助更新栈顶元素为第三个节点元素即为s.next
if ((h = head) != null && h.next == s)
casHead(h, s.next);
// 判断下,如果当前是请求数据,即take操作,返回m.item值,即返回匹配节点的item
// 当前是保存数据,即put操作,返回s.item
return (E) ((mode == REQUEST) ? m.item : s.item);
}
// 与栈顶节点非相同操作,栈顶元素非匹配互补节点
} else if (!isFulfilling(h.mode)) {
// 栈顶元素处于取消操作状态
if (h.isCancelled())
// 尝试出栈更新栈顶元素
casHead(h, h.next);
// 入栈新创建的节点,同时FULFILLING|mode 位与操作
// s的mode为10或者11
else if (casHead(h, s=snode(s, e, h, FULFILLING|mode))) {
// 进入这里表明s已经为栈顶节点,而且s.next是其匹配节点
// 循环直到匹配上
for (;;) {
SNode m = s.next;
// 空则可能被其他线程匹配上了则更新头节点为null,重新进入外层循环
if (m == null) {
casHead(s, null);
// 这里s节点需置空,因为比较特殊,mode不同于普通节点
// 重新循环时根据情况重新创建节点
s = null;
break;
}
//
SNode mn = m.next;
// 尝试m与s进行匹配,实际上是更新m节点的match为s,同时唤醒m的等待线程
if (m.tryMatch(s)) {
// 成功则出栈栈顶两个元素,即更新栈顶节点
casHead(s, mn);
return (E) ((mode == REQUEST) ? m.item : s.item);
} else
// 未匹配上,可能被其他节点匹配上了,尝试更新s的next指针,再继续匹配
s.casNext(m, mn);
}
}
// 不满足上边两个条件,即此时栈顶为匹配节点,还未匹配完成,这里帮忙完成匹配出栈操作
// 注意,这里只是帮助更新head和next并不做其他操作,参考上面方法的处理
} else {
SNode m = h.next;
if (m == null)
casHead(h, null);
else {
SNode mn = m.next;
if (m.tryMatch(h))
casHead(h, mn);
else
h.casNext(m, mn);
}
}
}
}

阻塞等待唤醒(awaitFulfill)

与TransferQueue.awaitFulfill类似,在当前操作同之前操作相同时,未设置操作时间同时未被外部线程中断则需阻塞等待匹配节点唤醒当前阻塞的线程。

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
scss复制代码    SNode awaitFulfill(SNode s, boolean timed, long nanos) {
// 获取超时时间点
final long deadline = timed ? System.nanoTime() + nanos : 0L;
// 当前线程
Thread w = Thread.currentThread();
// shouldSpin判断是否需要进行自旋
int spins = (shouldSpin(s) ?
(timed ? maxTimedSpins : maxUntimedSpins) : 0);
for (;;) {
// 判断当前线程是否中断,外部中断操作,相当于取消本次操作
if (w.isInterrupted())
// 尝试将s节点的match设置为s自己
s.tryCancel();
SNode m = s.match;
// match非空则表示当前节点已经被匹配match匹配上
if (m != null)
return m;
// 超时配置处理
if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
s.tryCancel();
continue;
}
}
// 自旋spins
if (spins > 0)
spins = shouldSpin(s) ? (spins-1) : 0;
// 设置等待线程
else if (s.waiter == null)
s.waiter = w;
// 未设置超时,直接阻塞
else if (!timed)
LockSupport.park(this);
// 设置超时时间阻塞
else if (nanos > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanos);
}
}

是否需要自旋操作(shouldSpin)

判断是否需要自旋操作,满足下列情况之一即需要自旋:

栈顶节点等于s节点;

栈顶节点为空;

栈顶节点为已和其他节点匹配的节点;

1
2
3
4
ini复制代码    boolean shouldSpin(SNode s) {
SNode h = head;
return (h == s || h == null || isFulfilling(h.mode));
}

清理操作(clean)

清理操作,清理栈节点s的关联关系,同时会清理整个栈节点的取消操作节点,无cleanMe节点,比TransferQueue.clean操作要简单许多

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
ini复制代码   
void clean(SNode s) {
// item,waiter 置空
s.item = null;
s.waiter = null;

// s的下一个节点处于取消操作状态,则past指向past的下一个节点
SNode past = s.next;
if (past != null && past.isCancelled())
past = past.next;

// 头节点被取消操作则进行将next节点更新为头节点
SNode p;
while ((p = head) != null && p != past && p.isCancelled())
casHead(p, p.next);

// 头节点处理完会把栈节点中每个节点检查一遍,更新前后节点的关系
while (p != null && p != past) {
SNode n = p.next;
if (n != null && n.isCancelled())
p.casNext(n, n.next);
else
p = n;
}
}

总结

SynchronousQueue的非公平策略的内部实现就是这样,要注意的是对于mode部分状态的处理,通过高位和低位分别区分是否已匹配和是什么类型的操作(生产者还是消费者)。其实需要记住的是其操作必须是成双成对的,在无超时无中断的情况下,一个线程执行入队操作,必然需要另一个线程执行出队操作,此时两操作互相匹配,同时完成操作。

本文转载自: 掘金

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

都这么卷了,不懂MyBatis插件开发怎么行,教你实现一个M

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

MyBatis可谓是Java开发工程师必须要掌握的持久层框架,它能够让我们更容易的通过Java代码操作数据库,并且它还有很高的扩展性,我们可以自定义插件,去让MyBatis的功能变的更为强大,本篇文章我们就以打印SQL,SQL分页为例,来讲一下如何开发MyBatis的插件。

前言

如果大家对MyBatis源码不熟悉,可以阅读我的这篇文章,专门讲解MyBatis源码阅读的juejin.cn/post/701763…

如果大家想知道MyBatis插件怎么融入实际项目,请参考我的开源项目gitee.com/zhuhuijie/b…

插件部分位于base-platform/base-common/common-db-mysql下

感兴趣的点个star,持续更新中…

MyBatis 四大内置对象

  • Executor 执行器 实际用来执行SQL的对象
  • StatementHandler 数据库会话处理器 编译/处理SQL语句的
+ PreparedStatementHanler 创建PreparedStatement 最常用占位符
+ CallableStatementHandler 创建CallableStatement 执行存储过程
+ SimpleStatementHanler 创建Statement 字符串拼接,有SQL注入风险
  • ParameterHandler 参数处理器
1
2
3
4
csharp复制代码public interface ParameterHandler {
   Object getParameterObject();
   void setParameter(PreparedStatement ps)
}
  • ResultSetHandler 处理结果集
1
2
3
4
5
java复制代码public interface ResultSetHandler {
   <E> list<E> handlerResultSets(Statement stmt) throws SQLException;
   <E> Cursor<E> handlerCursorResultSets(Statement stmt) throws SQLException;
   void handlerOutputParameters(CallableStatement cs) throws SQLException;
}

MyBatis 执行SQL的过程

  • 根据配置,获取SQLSession对象
  • 通过动态代理,获取Mapper的代理对象
1
ini复制代码StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
  • 通过代理对象调用具体SQL
1
ini复制代码Student student = mapper.getStudentById(id);
+ 通过反射调用该方法
+ mapperMethod.execute(sqlSession, args);


    - INSERT sqlSession.insert()
    - UPDATE sqlSession.update()
    - DELETE sqlSession.delete()
    - SELECT sqlSession.select()


        * selectList


        executor.query() 调用CachingExecutor【装饰者模式】 真实使用SimpleExecutor--->父类BaseExcutor.query() ---> doQuery()抽象 -->SimpleExecutor.doQuery() 【模板模式】


            + Handler对象初始化


                - 创建一个委托,根据不同StatementType创建不同的对象new PreparedStatementHanler()


                    * JDBC的Statement stmt = preparedStatementHanler.instantiateStatement() ---> connection.preparedStatement()
                    * handler.parameterize(stmt) 参数处理


                        + ParameterHandler
            + resultSetHandler.handlerResultSets(preparedStatement) 封装结果
        * ...
+ 得到结果

MyBatis 插件如何开发

MyBatis插件本质上就是对MyBatis四大内置对象的增强。

它是基于MyBatis的拦截器,通过AOP的方式进行使用。

案例一 打印SQL插件:

  • 创建拦截器

注意拦截器实现的是ibatis包下的,上边的注解决定了我们的拦截器是从MyBatis的哪里进行切入的,然后通过AOP的方式进行扩展。

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
ini复制代码package com.zhj.common.db.mysql.plugins;
​
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
​
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Statement;
​
/**
* 打印SQL语句
* 1. 记录SQL语句
* 2. 记录执行的时间
* type 增强的内置对象的类型(必须是四大内置对象中的一个 StatementHandler.class(增强最多的))
* method 增强的方法名
* args{} 是形参列表,防止方法重载,找不到对应的方法
* @author zhj
*/
@Slf4j
@Intercepts({
       @Signature(
               type = StatementHandler.class,
               method = "query",
               args = {Statement.class, ResultHandler.class}
      ),
       @Signature(
               type = StatementHandler.class,
               method = "update",
               args = {Statement.class}
      )
}
)
public class PrintSQLPlugins implements Interceptor {
​
   /**
    * 拦截方法
    * @param invocation
    * @return
    * @throws Throwable
    */
   @Override
   public Object intercept(Invocation invocation) throws Throwable {
       StatementHandler statementHandler= (StatementHandler) invocation.getTarget();
       BoundSql boundSql = statementHandler.getBoundSql();
       String sql = boundSql.getSql();
       log.info("----------------------------【SQL】-------------------------------");
       log.info(sql.replace("\n",""));
       long beginTime = System.currentTimeMillis();
       Object proceed = invocation.proceed(); // 放行,执行目标对象的对应方法
       long endTime = System.currentTimeMillis();
       log.info("----------------------------【SQL执行的时长为:{} s】", BigDecimal.valueOf(endTime - beginTime).divide(BigDecimal.valueOf(1000)).setScale(6, RoundingMode.DOWN).doubleValue());
       return proceed;
  }
}
​
  • 让该插件生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
kotlin复制代码package com.zhj.common.db.mysql.config;
​
import com.zhj.common.db.mysql.plugins.PrintSQLPlugins;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
​
/**
* @author zhj
*/
@Configuration
@MapperScan("com.zhj.data.mapper")
@EnableTransactionManagement
public class DBAutoConfiguration {
​
   @Bean
   @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false)
   public PrintSQLPlugins getPrintSQLPlugins(){
       return new PrintSQLPlugins();
  }
}
​
  • 通过配置决定是否启用插件

@ConditionalOnProperty(value = “zhj.plugins.printSql.enable”, havingValue = “true”, matchIfMissing = false)

  • 导入依赖,创建Bean使插件在配置时可以自动提示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kotlin复制代码package com.zhj.common.db.mysql.entity;
​
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
​
/**
* @author zhj
*/
@Component
@ConfigurationProperties(prefix = "zhj.plugins.printSql")
@Data
public class ZhjConfigInfo {
   private Boolean enable;
}

依赖:

1
2
3
4
5
xml复制代码<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>
  • 配置文件中开启插件:
1
2
3
4
yaml复制代码zhj:
 plugins:
   printSql:
     enable: true

案例二 分页插件:

基础分页插件的实现:

  • 创建分页对象
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
java复制代码package com.zhj.common.db.mysql.page;
​
import lombok.Data;
import lombok.experimental.Accessors;
​
import java.io.Serializable;
​
/**
* 分页信息对象
* @author zhj
*/
@Data
@Accessors(chain = true)
public class Page implements Serializable {
​
   /**
    * 当前页
    */
   private Integer pageNo;
   /**
    * 每页多少条
    */
   private Integer pageSize;
   /**
    * 总页码
    */
   private Integer pageTotal;
   /**
    * 总条数
    */
   private Integer pageCount;
}
​
  • 创建分页工具

这里我们通过ThreadLocal来设置分页对象

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
csharp复制代码package com.zhj.common.db.mysql.page;
​
/**
* 分页管理器
* @author zhj
*/
public class PageUtils {
​
   private static ThreadLocal<Page> pageThreadLocal = new ThreadLocal<>();
​
   /**
    * 设置分页对象
    * @param pageNo
    * @param pageSize
    */
   public static void setPage(Integer pageNo, Integer pageSize){
       pageThreadLocal.set(new Page().setPageNo(pageNo).setPageSize(pageSize));
  }
​
   /**
    * 获取分页对象
    * @return
    */
   public static Page getPage(){
       return pageThreadLocal.get();
  }
​
   /**
    * 清理分页信息
    */
   public static void clear(){
       pageThreadLocal.remove();
  }
}
  • 创建实现分页插件的拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
java复制代码package com.zhj.common.db.mysql.plugins;
​
import com.zhj.common.db.mysql.page.Page;
import com.zhj.common.db.mysql.page.PageUtils;
import com.zhj.common.db.mysql.util.MybatisUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
​
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
​
/**
* 分页插件
* @author zhj
*/
@Slf4j
@Intercepts({
               @Signature(
                       type = StatementHandler.class,
                       method = "prepare",
                       args = {Connection.class, Integer.class} // 需要与对应版本一致
              )
})
public class PagePlugins implements Interceptor {
   @Override
   public Object intercept(Invocation invocation) throws Throwable {
       // 获取非代理对象
       StatementHandler target = MybatisUtils.getNoProxyTarget(invocation.getTarget());
       BoundSql boundSql = target.getBoundSql();
       // 拿到sql 转为小写,去掉前后空格
       String sql = boundSql.getSql().toLowerCase().trim();
       // 判断是否需要添加分页
       if (!sql.startsWith("select")) {
           return invocation.proceed();
      }
       // 获取分页参数
       Page page = PageUtils.getPage();
       if (page == null) {
           return invocation.proceed();
      }
       // 处理分页
       log.info("[需要分页的SQL: {}", sql.replace("\n",""));
       // 构建一个查询分页总条数的sql;
       Integer count = count(target, invocation, sql);
       log.info("[SQL的总条数为: " + count);
       // 处理pageNo
       if (page.getPageNo() == null || page.getPageNo() < 1)
           page.setPageNo(1);
       // 处理pageSize
       if (page.getPageSize() == null || page.getPageSize() < 1)
           page.setPageSize(10);
       // 设置分页对象
       page.setPageCount(count);
       page.setPageTotal(page.getPageCount() % page.getPageSize() == 0 ? page.getPageCount()/ page.getPageSize() : page.getPageCount()/ page.getPageSize() + 1);
       if (page.getPageNo() > page.getPageTotal())
           page.setPageNo(page.getPageTotal());
       log.info("[处理过的Page为: " + page);
       sql += " limit " + (page.getPageNo() * page.getPageSize() - 1) + "," + page.getPageSize();
       log.info("[分页处理过的SQL: {}", sql.replace("\n",""));
       // 通过反射设置BoundSql的sql
       // MyBatis提供了工具,该工具通过反射实现
       MetaObject metaObject = SystemMetaObject.forObject(boundSql);
       metaObject.setValue("sql", sql);
       return invocation.proceed();
  }
​
   /**
    * 获取sql的总条数
    * @param sql
    * @return
    */
   private Integer count(StatementHandler statementHandler, Invocation invocation, String sql) throws SQLException {
​
       // 判断是否存在排序的内容
       int orderByIndex = -1;
       if (sql.lastIndexOf("order by") != -1) {
           sql = sql.substring(0, orderByIndex);
      }
​
       // 获取查询总条数sql
       int fromIndex = sql.indexOf("from");
       String countSQL = "select count(*) " + sql.substring(fromIndex);
       log.info("[查询总条数的SQL: " + countSQL);
​
       // 执行sql
       // 获得方法的参数
       Connection connection = (Connection) invocation.getArgs()[0];
       PreparedStatement ps = null;
       ResultSet resultSet = null;
       try {
           // sql 处理器
           ps = connection.prepareStatement(countSQL);
           // 处理参数
           statementHandler.parameterize(ps);
           // 执行sql
           resultSet = ps.executeQuery();
           // 获取结果
           if (resultSet.first()) {
               return resultSet.getInt(1);
          }
      } catch (SQLException sqlException) {
           log.info("[查询总条数的SQL出现异常!!!]");
           throw sqlException;
      } finally {
           if (resultSet != null) {
               resultSet.close();
          }
           if (ps != null) {
               ps.close();
          }
      }
       return -1;
  }
}
​
  • 由于使用代理模式对MyBatis四大内置对象进行增强,当创建多个分页插件时会进行干扰,我们有时候获得的目标对象,并不是真实的目标对象,而是其它插件形成的代理对象,我们需要写一个工具类获取真实的目标对象。
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复制代码package com.zhj.common.db.mysql.util;
​
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
​
/**
* @author zhj
*/
public class MybatisUtils {
​
   /**
    * 获取非代理对象
    * @param target
    * @param <T>
    * @return
    */
   public static <T> T getNoProxyTarget(Object target) {
       MetaObject invocationMetaObject = SystemMetaObject.forObject(target);
       while (invocationMetaObject.hasGetter("h")) {
           // 说明获得的是代理对象
           target = invocationMetaObject.getValue("h.target");
           invocationMetaObject = SystemMetaObject.forObject(target);
      }
       return (T) target;
  }
}
​
  • 注入分页插件,使其生效
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
kotlin复制代码package com.zhj.common.db.mysql.config;
​
import com.zhj.common.db.mysql.plugins.PagePlugins;
import com.zhj.common.db.mysql.plugins.PrintSQLPlugins;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
​
/**
* @author zhj
*/
@Configuration
@MapperScan("com.zhj.data.mapper")
@EnableTransactionManagement
public class DBAutoConfiguration {
​
   @Bean
   @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false)
   public PrintSQLPlugins getPrintSQLPlugins(){
       return new PrintSQLPlugins();
  }
​
   @Bean
   public PagePlugins getPagePlugins(){
       return new PagePlugins();
  }
}
  • 在Controller(Service)中设置开启分页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
kotlin复制代码package com.zhj.business.controller;
​
import com.zhj.business.protocol.input.StudentInput;
import com.zhj.business.service.StudentService;
import com.zhj.common.core.result.Result;
import com.zhj.common.core.util.ResultUtils;
import com.zhj.common.db.mysql.page.PageUtils;
import com.zhj.data.entity.example.Student;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
​
import javax.validation.Valid;
import java.util.List;
​
/**
* @author zhj
*/
@Slf4j
@RestController
@RequestMapping("/student")
public class StudentController {
​
   @Autowired
   private StudentService studentService;
​
   @GetMapping("/list")
   public Result<List<Student>> list() {
       // 开启分页,可将前端传入的值设置到Page中
       PageUtils.setPage(1,2);
       List<Student> list = studentService.list();
       return ResultUtils.createSuccess(list);
  }
}
​

让分页插件更优雅:

  • 将侵入部分去掉,通过AOP的方式开启分页,并将分页信息返回
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
ini复制代码package com.zhj.common.db.mysql.aop;
​
import com.zhj.common.db.mysql.page.BasePageResult;
import com.zhj.common.db.mysql.page.Page;
import com.zhj.common.db.mysql.page.PageUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
​
import javax.servlet.http.HttpServletRequest;
​
/**
* 如果参数携带pageNo和pageSize自动开启分页
* @author zhj
*/
@Aspect
public class WebPageAOP {
​
   @Around("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller)")
   public Object pageAOP(ProceedingJoinPoint joinPoint) throws Throwable {
       // 获取参数
       ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
       HttpServletRequest request = requestAttributes.getRequest();
       String pageNo = request.getParameter("pageNo");
       String pageSize = request.getParameter("pageSize");
       if (!StringUtils.isEmpty(pageNo) && !StringUtils.isEmpty(pageSize)) {
           PageUtils.setPage(Integer.parseInt(pageNo), Integer.parseInt(pageSize));
      }
       Object proceed = null;
       try {
           proceed = joinPoint.proceed();
           Page page = PageUtils.getPage();
           if (proceed instanceof BasePageResult && page != null) {
               BasePageResult basePageResult = (BasePageResult) proceed;
               basePageResult.setPage(page);
          }
      } catch (Throwable e) {
           throw e;
      } finally {
           PageUtils.clear();
      }
       return proceed;
  }
}
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
kotlin复制代码package com.zhj.common.db.mysql.config;
​
import com.zhj.common.db.mysql.aop.PageAOP;
import com.zhj.common.db.mysql.aop.WebPageAOP;
import com.zhj.common.db.mysql.plugins.PagePlugins;
import com.zhj.common.db.mysql.plugins.PrintSQLPlugins;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
​
/**
* @author zhj
*/
@Configuration
@MapperScan("com.zhj.data.mapper")
@EnableTransactionManagement
public class DBAutoConfiguration {
​
   @Bean
   @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false)
   public PrintSQLPlugins getPrintSQLPlugins(){
       return new PrintSQLPlugins();
  }
​
   @Bean
   public PagePlugins getPagePlugins(){
       return new PagePlugins();
  }
​
   @Bean
   public WebPageAOP getWebPageAOP(){
       return new WebPageAOP();
  }
}
  • 通过注解将分页的粒度控制到更细的粒度
+ 创建注解



1
2
3
4
5
6
7
8
9
10
11
12
java复制代码package com.zhj.common.db.mysql.annotation;
​
import java.lang.annotation.*;
​
/**
* @author zhj
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Page {
}
+ Page对象增加开关
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
java复制代码package com.zhj.common.db.mysql.page;
​
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.experimental.Accessors;
​
import java.io.Serializable;
​
/**
* 分页信息对象
* @author zhj
*/
@Data
@Accessors(chain = true)
public class Page implements Serializable {
​
   /**
    * 当前页
    */
   private Integer pageNo;
   /**
    * 每页多少条
    */
   private Integer pageSize;
   /**
    * 总页码
    */
   private Integer pageTotal;
   /**
    * 总条数
    */
   private Integer pageCount;
​
   /**
    * 是否开启分页
    */
   @JsonIgnore
   private boolean enable;
}
+ 在原来的分页拦截器上增加判断条件
1
2
3
4
5
ini复制代码// 获取分页参数
Page page = PageUtils.getPage();
if (page == null || !page.isEnable()) {
   return invocation.proceed();
}
+ 通过AOP设置开关
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
kotlin复制代码package com.zhj.common.db.mysql.aop;
​
import com.zhj.common.db.mysql.page.Page;
import com.zhj.common.db.mysql.page.PageUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
​
/**
* @author zhj
*/
@Aspect
public class PageAOP {
​
   @Around("@annotation(com.zhj.common.db.mysql.annotation.Page)")
   public Object pageAOP(ProceedingJoinPoint joinPoint) throws Throwable {
       Page page = PageUtils.getPage();
       if (page != null) {
           page.setEnable(true);
      }
       try {
           return joinPoint.proceed();
      } finally {
           if (page != null) {
               page.setEnable(false);
          }
      }
  }
}
​
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
typescript复制代码package com.zhj.common.db.mysql.config;
​
import com.zhj.common.db.mysql.aop.PageAOP;
import com.zhj.common.db.mysql.aop.WebPageAOP;
import com.zhj.common.db.mysql.plugins.PagePlugins;
import com.zhj.common.db.mysql.plugins.PrintSQLPlugins;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
​
/**
* @author zhj
*/
@Configuration
@MapperScan("com.zhj.data.mapper")
@EnableTransactionManagement
public class DBAutoConfiguration {
​
   @Bean
   @ConditionalOnProperty(value = "zhj.plugins.printSql.enable", havingValue = "true", matchIfMissing = false)
   public PrintSQLPlugins getPrintSQLPlugins(){
       return new PrintSQLPlugins();
  }
​
   @Bean
   public PagePlugins getPagePlugins(){
       return new PagePlugins();
  }
​
   @Bean
   public WebPageAOP getWebPageAOP(){
       return new WebPageAOP();
  }
​
   @Bean
   public PageAOP getPageAOP(){
       return new PageAOP();
  }
}
+ 在对应的service或者dao上开启分页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala复制代码package com.zhj.business.service.impl;
​
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zhj.business.service.StudentService;
import com.zhj.common.db.mysql.annotation.Page;
import com.zhj.data.entity.example.Student;
import com.zhj.data.mapper.example.dao.StudentDao;
import org.springframework.stereotype.Service;
​
import java.util.List;
​
/**
* @author zhj
*/
@Service
public class StudentServiceImpl extends ServiceImpl<StudentDao, Student> implements StudentService {
​
   @Page
   @Override
   public List<Student> list() {
       return super.list();
  }
}

MyBatis插件开发总结

想要对框架进行扩展,首先必须得了解框架源码,只有对源码有较为深入的了解,我们才能更好的把握从哪个点进行切入扩展。本文中的两个案例都是最为简单的实现,说实话,还有很多漏洞,比如第一个打印SQL的插件我们并没有去将参数填充,也没有拿到参数,第二个案例分页,只能满足一些比较简单的场景,如果SQL过于复杂,很可能会出现Bug。这些内容都需要我们不断去学习源码,不断的去学习开源项目,积累的越多,我们写出来的工具越完美。大家可以参考GitHub上MyBatis分页的开源项目,对自己写的分页插件进行不断的完善,当然大家也可以在评论区进行交流,共同学习。

本文转载自: 掘金

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

Go语言学习查缺补漏ing Day10

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

Go语言学习查缺补漏ing Day10

本文收录于我的专栏:《让我们一起Golang》

零、前言

因为笔者基础不牢,在使用Go语言的时候经常遇到很多摸不着头脑的问题,所以笔者下定决心好好对Go语言进行查漏补缺,本【Go语言查缺补漏ing】系列主要是帮助新手Gopher更好的了解Go语言的易错点、重难点。希望各位看官能够喜欢,点点赞、关注一下呗!

一、for 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
go复制代码package main
​
import (
"fmt"
)
​
type Foo struct {
bar string
}
​
func main() {
slice1 := []Foo{
{"A"},
{"B"},
{"C"},
}
slice2 := make([]*Foo, len(slice1))
for index, value := range slice1 {
slice2[index] = &value
}
fmt.Println(slice1[0], slice1[1], slice1[2])
fmt.Println(slice2[0], slice2[1], slice2[2])
}
​

我们想要获得的结果应该是:

1
2
go复制代码{A} {B} {C}
&{A} &{B} &{C}

但是,实际上是这样的吗?来看一下输出结果:

1
2
go复制代码{A} {B} {C}
&{C} &{C} &{C}

为什么会出现这种情况?

因为当我们在for range循环中使用:=的形式迭代变量时,索引index以及值value会在每次循环都会被重用,而不是新开辟一块新的内存空间存放。所以这里slice2每一次放入的都是值value的地址,所以最后一次循环时,value是{C},而因为前面存入的地址都是value的地址,故存入的都是{C}。

有什么解决办法?

第一个解决办法自然是不存储指针,而是存值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码package main
​
import (
"fmt"
)
​
type Foo struct {
bar string
}
​
func main() {
slice1 := []Foo{
{"A"},
{"B"},
{"C"},
}
slice2 := make([]Foo, len(slice1))
for index, value := range slice1 {
slice2[index] = value
}
fmt.Println(slice1[0], slice1[1], slice1[2])
fmt.Println(slice2[0], slice2[1], slice2[2])
}
1
2
go复制代码{A} {B} {C}
{A} {B} {C}

这样虽然还是重用,但是每次value的值都被保存了,保存的不是指针,因此最后输出结果没问题。但是这个貌似输出结果不是我们想要的那样。

那么还有没有其它办法呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码package main
​
import (
"fmt"
)
​
type Foo struct {
bar string
}
​
func main() {
slice1 := []Foo{
{"A"},
{"B"},
{"C"},
}
slice2 := make([]*Foo, len(slice1))
for index := range slice1 {
slice2[index] = &slice1[index]
}
fmt.Println(slice1[0], slice1[1], slice1[2])
fmt.Println(slice2[0], slice2[1], slice2[2])
}
​

我们可以这样,虽然value的地址是固定的,但是slice1[index]的地址是不一样的。我们可以取slice1的地址。

这样的输出结果是:

1
2
go复制代码{A} {B} {C}
&{A} &{B} &{C}

二、多重赋值需要掌握的易错点

看下面这段代码,请问输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main
​
import (
"fmt"
)
​
func main() {
i := 0
s := []string{"A", "B", "C", "D"}
i, s[i+1] = 2, "Z"
fmt.Printf("s: %v \n", s)
}
​

答案是:

1
go复制代码s: [A Z C D]

为什么呢?

其实我们的多重赋值是按步骤进行的,不是同时进行,而是有前后顺序:

第一个阶段是估值阶段,然后是实施阶段:

比如:

1
go复制代码a, b = b, a

下面是估值阶段:

1
2
3
go复制代码// 估值阶段
P0 := &a; P1 := &b
R0 := a; R1 := b

然后是实施阶段:

1
2
3
4
go复制代码// 最基本形式:*P0, *P1 = R0, R1
// 实施阶段
*P0 = R0
*P1 = R1

实施阶段中的赋值操作并不会对估值阶段的结果造成影响。

而且,我们这里先计算=左边的索引表达式或取址表达式,然后计算=右边的表达式。再之后就是进行赋值操作。

所以我们这里先计算索引表达式i+1,然后进行赋值运算。

本文转载自: 掘金

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

SpringBoot(十二) springBoot

发表于 2021-11-28

springBoot

一、概述

1.什么是spring boot

Spring Boot是Spring项目中的一个子工程,与我们所熟知的Spring-framework 同属于spring的产品:

其最主要作用就是帮助开发人员快速的构建庞大的spring项目,并且尽可能的减少一切xml配置,做到开箱即用,迅速上手,让开发人员关注业务而非配置。

2.spring boot的特点

1.自动配置:不需要再关注各个框架的整合配置,springboot全部都已经配置好了。

2.起步依赖:我们在需要使用某个框架的时候,直接添加这个框架的启动器依赖即可,不需要在关注jar包的冲突和整合。

3.目的

用来简化spring应用的初始搭建以及开发过程。

  1. 为所有spring开发提供一个更快更广泛的入门体验。
  2. 零配置。没有冗余代码生成和xml强制配置,遵循“约定大于配置”
  3. 集成了大量常用的第三方库的配置,spring boot应用为这些第三方库提供了几乎可以零配置的开箱即用的能力。
  4. 提供一系列大型项目常用的非功能性,如嵌入服务器等。

4.好处

简单、快速、方便

5.优势

使用Java开发程序 , 一直困扰我们的就是臃肿、麻烦。搭建项目的过程相当复杂 , 我们需要考虑很多问题 , 主要的问题有如下两点 :

  1. 复杂的配置
  2. 混乱的依赖管理

Spring Boot帮我们解决了这个些, 我们在使用Spring Boot开发时, 不需要关注各种复杂的整合配置 , 也不用关注各个库之间的依赖及冲突问题 , Spring Boot已经默认帮我们整合配置好了 !

二、springboot之helloworld

1.需求

访问 http://localhost:8080/hello输出 “Hello Spring Boot”

2.步骤

  1. 创建Maven工程
  2. 加依赖(springboot父工程依赖 , web启动器依赖)
1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码<!--spring boot 父工程依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>
<!--web 启动器-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

配置jdk版本

1
2
3
xml复制代码<properties>
<java.version>1.8</java.version>
</properties>

思考: 为什么我们这里仅仅配置了这么一个变量 , 项目的JDK版本就会改变呢 ?

因为jdk插件已经在父工程中定义好了 , 默认会读取${java.version}变量值
3. 写启动引导类(springboot项目运行的入口)

1
2
3
4
5
6
7
8
9
java复制代码import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
  1. 编写处理器Controller
1
2
3
4
5
6
7
8
9
java复制代码import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String sayHello(){
return "hello spring boot!!" ;
}
}
  1. 启动项目

运行启动类的main方法

3.问题

1.为什么我们在添加启动器的时候不需要在启动器的坐标中指定版本?

答案:因为我们指定了项目的父工程,在spring-boot-starter-parent中已经通过Maven的版本锁定了Jar包的版本,所以就不需要再指定了。

2.为什么我们就添加一个启动器依赖,项目就可以运行起来了,运行项目所需要的Jar包从何而来?

答案:因为我们添加了这个启动器的依赖,它已经把自己运行所需要的必要包集成在这个启动器中,通过Maven的依赖传递性,将这些包都依赖到咱们的项目里了。

4.配置文件详解

springboot支持两种类型的配置文件

  • properties属性配置文件
  • yaml配置文件

配置文件必须放在项目的类加载目录下, 并且名字必须是application开头

springboot项目在运行的时候会自动加载这些配置文件。

可以在spring-boot-autoconfigure-2.2.2.RELLEASE.jar的META-INF目录下找到additional-spring-configuration-metadata.json搜索server.port,显示默认的端口是8080.

为什么可以在resources下创建application.properties文件?

我们查看springboot的启动依赖

按住ctrl键点击spring-boot-starter-parent

可以在父工程中看到

1
2
3
4
5
6
7
8
9
10
11
12
13
> xml复制代码<build>
> <resources>
> <resource>
> <filtering>true</filtering>
> <directory>${basedir}/src/main/resources</directory>
> <include>**/application*.yml</include>
> <include>**/application*.yaml</include>
> <include>**/application*.properties</include>
> </resource>
> </resources>
> </build>
>
>

5.如何主动读取配置文件到类变量

要使用@Value(“${属性名}”)

如下:

在 resource 文件夹下面新建 application.properties 配置文件

1
2
3
4
ini复制代码spring.jdbc.datasource.driverClassName=com.mysql.jdbc.driver
spring.jdbc.datasource.url=jdbc:mysql:///springboot_01
spring.jdbc.datasource.username=root
spring.jdbc.datasource.password=123456

创建类 DataSourceProperties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class DataSourceProperties {

@Value("${spring.jdbc.datasource.driverClassName}")
private String driverClassName;
@Value("${spring.jdbc.datasource.url}")
private String url;
@Value("${spring.jdbc.datasource.username}")
private String username;
@Value("${spring.jdbc.datasource.password}")
private String password;

// 生成get set 和 toString方法
}

三、YAML配置文件

1.什么是YAML?

YAML是一种配置文件格式

2.语法

1.数据结构用树形结构呈现,通过缩进来表示层级,

2.连续的项目通过减号 ” - ” 来表示

3.键值结构里面的key/value对用冒号 ” : ” 来分隔,冒号后要加上一个空格才能跟属性值。

4.YAML配置文件的扩展名是yaml 或 yml

如果要修改前面那个properties文件为yaml文件

1
2
3
4
5
6
7
yaml复制代码spring:
jdbc:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql:///springboot_01
username: root
password: 123456

yml配置文件的特征:

  1. 树状层级结构展示配置项
  2. 配置项之间如果有关系的话需要分行,空两格
  3. 配置项如果有值的话,那么需要在:之后空一格再写配置项值;

yaml与peoperties配置文件处理展示像是不相同以外,其他功能和作用都是一样的。

3.多环境profile切换配置

在实际开发中,因为开发环境的变化,我们需要修改配置文件中某一个配置项的值(比如之前himysql数据库,切换成oracle数据库)

项目开发完成需要上线时,需要把一些环境改成正式环境(开发,测试,上线,多环境切换)

解决方案:使用profiles拆分配置

spring boot项目中允许使用多个yaml配置文件。

这些文件名称必须为**application-***.yml**,并且在application.yml中激活。

具体实施

创建application-dev.yml文件如下:

1
2
3
4
5
6
7
yaml复制代码spring:
jdbc:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql:///springboot
username: root
password: 123456

创建application-pro.yml文件如下:

1
2
3
4
5
6
7
yaml复制代码spring:
jdbc:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql:///business
username: business
password: business

在application.yml文件中添加如下配置:

1
2
3
yml复制代码spring:
profiles:
active: dev

此时生效的文件就是application-dev.yml配置文件。

注意:

如果properties和yml文件都存在,不存在spring.profiles.active设置,如果有重叠属性,默认以properties文件优先.

如果设置了spring.profiles.active,并且有重叠属性,以active设置优先。

可以在两种文件中分别增加server.port属性指定不同的端口,启动项目查看控制台端口号进行测试。

四、Spring Boot 的自动配置原理

1.@SpringBootApplication注解

①@SpringBootConfiguration

含义:代表这个类就是一个配置类,本质上就是一个@Configuration注解

②ComponentScan

组件扫描,默认扫描启动类所在包以及子包的类身上的注解。

③EnableAutoConfiguration

自动配置注解,添加了此注解会自动去读取spring.factories配置文件中的自动配置类。

2.@ConfigurationProperties注解

@ConfigurationProperties是SpringBoot提供的重要注解,他可以将一些配置属性批量注入到bean对象。

注入配置属性

方式一:使用@Value一个个注入

这种方式,如果属性特别多,一个一个注入非常麻烦。

方式二:使用@ConfigurationProperties批量注入【和@EnableConfigurationProperties配合使用】

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Component
@ConfigurationProperties(prefix = "spring.jdbc.datasource")
public class DataSourceProperties2 {

private String driverClassName;
private String url;
private String username;
private String password;

// 省略getter和setter.....
}
  1. 在类上通过@ConfigurationProperties注解声明该类要读取属性配置。
  2. prefix=”spring.jdbc.datasource”读取属性文件中前缀为spring.jdbc.datasource的值。前缀和属性名称和配置文件中的key必须要一致才能注入成功
  3. Spring Boot默认读取application.properties属性文件

开启@ConfigurationProperties注解使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Controller
@EnableConfigurationProperties(DataSourceProperties2.class)
public class HelloController {

@Autowired
private DataSourceProperties2 dataSourceProperties2 ;

@RequestMapping(path = "/hello")
@ResponseBody
public String sayHello(){
System.out.println(dataSourceProperties2);
return "hello spring boot";
}
}
使用@EnableConfi

使用@EnableConfigurationProperties(DataSourceProperties2.class),开启DataSoutceProperties2身上的@ConfigurationProperties注解,他就会生效了,就能够帮我们注入数据了。

可能出现错误Spring Boot Configuration Annotation Processor not found in classpath。

解决方法:

在pom文件中加入依赖

1
2
3
4
5
6
7
> xml复制代码<dependency>
> <groupId>org.springframework.boot</groupId>
> <artifactId>spring-boot-configuration-processor</artifactId>
> <optional>true</optional>
> </dependency>
>
>

3.条件化配置注解

​ 我们看到自动配置类上有一些ConditionalXxx注解,这些注解的作用就是进行条件化选择。

​ 所谓条件化选择就是如果满足条件,该配置类就生效,如果不满足该配置类就不生效。

常用的条件化选座注解如下:

注解 作用
@ConditionalOnBean 如果存在某个Bean, 配置类生效
@ConditionalOnMissingBean 如果不存在某个Bean, 配置类生效
@ConditionalOnClass 如果存在某个类, 配置类生效
@ConditionalOnMissingClass 如果不存在某个类, 配置类生效
@ConditionalOnProperty 如果存在某个属性配置, 配置类生效
@ConditionalOnWebApplication 如果是一个web应用, 配置类生效
@ConditionalOnNotWebApplication 如果不是一个web应用, 配置类生效

因为我们配置了DispatcherServlet满足上面定义的条件,所以WebMvcAutoConfiguration会生效,那么WebMvcAutoConfiguration自动配置类中帮我们配置了什么?

  1. 视图解析器(触发public InternalResourceViewResolver defaultViewResolver()或public BeanNameViewResolver beanNameViewResolver()方法)
  2. 处理器适配器(public RequestMappingHandlerAdapter requestMappingHandlerAdapter()方法)

这些配置都是我们之前在学习SpringMVC时需要自己配置的,现在Spring Boot框架都已经帮我们提前配置好了,所以我们才能使用的那么方便。

4.自动配置原理

1.加载spring.factories

在SpringApplication类构建的时候,有这样一段初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.sources = new LinkedHashSet();
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.addConversionService = true;
this.headless = true;
this.registerShutdownHook = true;
this.additionalProfiles = new HashSet();
this.isCustomEnvironment = false;
this.lazyInitialization = false;
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}

注意 this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));点击进入getSpringFactoriesInstances方法

1
2
3
java复制代码    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return this.getSpringFactoriesInstances(type, new Class[0]);
}

紧接着跟入getSpringFactoriesInstances方法

1
2
3
4
5
6
7
java复制代码    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = this.getClassLoader();
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}

可以看到Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));这条代码。能够发现loadFactoryNames()尝试加载一些FactoryName,然后利用createSpringFactoriesInstances将这些加载到的类名进行实例化。然后继续跟进loadFactoryNames方法:

1
2
3
4
java复制代码    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

观察返回时调用的loadSpringFactories方法

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
java复制代码private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;

for(int var11 = 0; var11 < var10; ++var11) {
String factoryImplementationName = var9[var11];
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}

cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}

发现此处会利用类加载器加载一个文件:META-INF/factories。我们知道,ClassLoader默认是从classpath下读取的文件,因此,SpringBoot会在初始化的时候,加载所有classpath:META-INF/spring.factories文件,包括jar包当中的。而在Spring的一个依赖包:spring-boot-autoconfigure中,就有这样一个文件。

image-20211029163735112

springboot-1.png
我们引入的任何第三方启动器,只要实现自动配置,也都会有类似文件。

2.读取自动配置类

打开这个spring.factories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
ini复制代码# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer

# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider

可以发现以EnableAutoConfigutation接口为key的一系列配置,key所对应的值,就是所有的自动配置类,可以在当前的jar包中找到这些自动配置类:

springboot-2.png
几乎涵盖了现在主流的开源框架,我们可以从这里查看SpringMVC的自动配置类。

springboot-3.png
我们就找到了springmvc的自动配置类

3.默认属性配置

配置类找到了,那么这些默认配置的属性来自哪里?

例如:我们配置视图解析器的时候需要配置前缀后后缀,那么这些配置在哪里?

1
2
3
4
5
6
7
8
9
java复制代码
@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}

通过这段源码发现,这个配置是this.mvcProperties.getView()中获取的。查看这个方法。

1
2
3
csharp复制代码public WebMvcProperties.View getView() {
return this.view;
}

发现读取的是类变量

1
arduino复制代码    private final WebMvcProperties.View view;

这个View类型封装着前缀和后缀

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 static class View {
private String prefix;
private String suffix;

public View() {
}

public String getPrefix() {
return this.prefix;
}

public void setPrefix(String prefix) {
this.prefix = prefix;
}

public String getSuffix() {
return this.suffix;
}

public void setSuffix(String suffix) {
this.suffix = suffix;
}
}

可以看到,前缀和后缀变量的值默认是null。

4.覆盖默认属性配置

如果我们想要自己指定视图的前缀和后缀?

我们可以再看WebMvcAutoConfiguration这个类的静态内部类WebMvcAutoConfigurationAdapter上一个声明,发现这个类上面有这样一个注解@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
java复制代码@Configuration(
proxyBeanMethods = false
)
@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
@EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final ResourceProperties resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
final WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;

public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider, ObjectProvider<WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = (WebMvcAutoConfiguration.ResourceHandlerRegistrationCustomizer)resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
}

public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider.ifAvailable((customConverters) -> {
converters.addAll(customConverters.getConverters());
});
}

public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
if (this.beanFactory.containsBean("applicationTaskExecutor")) {
Object taskExecutor = this.beanFactory.getBean("applicationTaskExecutor");
if (taskExecutor instanceof AsyncTaskExecutor) {
configurer.setTaskExecutor((AsyncTaskExecutor)taskExecutor);
}
}

Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
if (timeout != null) {
configurer.setDefaultTimeout(timeout.toMillis());
}

}

public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
}

public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation();
configurer.favorPathExtension(contentnegotiation.isFavorPathExtension());
configurer.favorParameter(contentnegotiation.isFavorParameter());
if (contentnegotiation.getParameterName() != null) {
configurer.parameterName(contentnegotiation.getParameterName());
}

Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation().getMediaTypes();
mediaTypes.forEach(configurer::mediaType);
}

@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}

@Bean
@ConditionalOnBean({View.class})
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(2147483637);
return resolver;
}

@Bean
@ConditionalOnBean({ViewResolver.class})
@ConditionalOnMissingBean(
name = {"viewResolver"},
value = {ContentNegotiatingViewResolver.class}
)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager((ContentNegotiationManager)beanFactory.getBean(ContentNegotiationManager.class));
resolver.setOrder(-2147483648);
return resolver;
}

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.mvc",
name = {"locale"}
)
public LocaleResolver localeResolver() {
if (this.mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
} else {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
}

public MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
resolver.setMessageCodeFormatter(this.mvcProperties.getMessageCodesResolverFormat());
return resolver;
} else {
return null;
}
}

public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}

public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
if (!registry.hasMappingForPattern("/webjars/**")) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
}

}
}

private Integer getSeconds(Duration cachePeriod) {
return cachePeriod != null ? (int)cachePeriod.getSeconds() : null;
}

private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) {
if (this.resourceHandlerRegistrationCustomizer != null) {
this.resourceHandlerRegistrationCustomizer.customize(registration);
}

}

@Bean
@ConditionalOnMissingBean({RequestContextListener.class, RequestContextFilter.class})
@ConditionalOnMissingFilterBean({RequestContextFilter.class})
public static RequestContextFilter requestContextFilter() {
return new OrderedRequestContextFilter();
}
}

再点进去看这两个配置类WebMvcProperties.class, 配置类身上使用ConfigurationProperties读取配置,前缀是spring.mvc

1
2
3
4
5
6
java复制代码@ConfigurationProperties(
prefix = "spring.mvc"
)
public class WebMvcProperties {
//...
}

而ResourceProperties.class读取的配置前缀是spring.resources

1
2
3
4
5
6
7
java复制代码@ConfigurationProperties(
prefix = "spring.resources",
ignoreUnknownFields = false
)
public class ResourceProperties {
//...
}

所以如果我们再配置文件中配置spring.mvc前缀开头的配置,就可以将自己配置的数据注入到这个对象的属性中。注意,由于@ConfigurationProperties是批量注入,我们在写配置属性时,要和这个类的前缀+属性名完全一致,否则注入不成功。

例如

1
2
3
4
5
yaml复制代码spring:
mvc:
view:
prefix: /WEB-INF/
suffix: .jsp

或者修改端口号

1
2
yaml复制代码server:
port: 10000

5.自定义启动器

1.需求介绍

定义一个连接池启动器,当用户引入了连接池启动依赖之后,项目中就已经自动配置了连接池。

2.步骤分析

  1. 建启动器项目
  2. 添加启动器相关依赖
  3. 创建属性配置类
  4. 创建自动配置类
  5. 编写自动配置文件(spring.factories)
  6. 使用自定义的启动器

3.代码实现

1.创建项目,引入依赖

创建项目spring-boot-jdbc-starter

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!--引入spring‐boot‐starter;所有starter的基本配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!--自动配置连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>

<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2.创建属性配置类
1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.boot.context.properties.ConfigurationProperties;

@Component
@ConfigurationProperties(prefix = "spring.jdbc.datasource")
public class DataSourceProperties {
private String driverClassName ;
private String url;
private String username;
private String password;
// 生成set get toString方法
}
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
java复制代码import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;

@SpringBootConfiguration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

@Autowired
private DataSourceProperties dataSourceProperties ;

@Bean
public DataSource createDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
dataSource.setUrl(dataSourceProperties.getUrl());
dataSource.setUsername(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
return dataSource;
}
}
4.编写自动配置属性文件

在resources文件夹下面新建META-INF/spring.factories

1
2
ini复制代码# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.dyy.autoconfig.DataSourceAutoConfiguration

做完之后要注意在Maven中执行install,安装项目。

5.使用自定义启动器

在新建的项目中引入我们自己的自定义启动器依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.dyy</groupId>
<artifactId>spring-boot-jdbc-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

配置连接池信息。

新建application-datasource.yml

1
2
3
4
5
6
7
yaml复制代码spring:
jdbc:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql:///springboot_01
username: root
password: 123456

激活配置文件application.yml

1
2
3
yaml复制代码spring:
profiles:
active: datasource

进入连接池,查看连接池属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;

@RestController
public class HelloController {
@Autowired
private DataSource dataSource ;

@RequestMapping(path = "/hello")
public String sayHello() {
System.out.println(dataSource.getClass());//打印DruidDataSource数据源
return "Hello Spring Boot ! " ;
}
}
6.多种数据源

如果想让我们的启动器支持多种数据源。例如C3P0和Druid,根据配置进行选择,就可以使用条件选择进行实现。例如:如下配置中,有两个创建连接池的配置,一个是C3p0,一个是Druid,如何能够根据配置文件自动选择呢?

修改配置文件application-datasource.yml

1
2
3
4
5
6
7
8
yaml复制代码spring:
jdbc:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql:///springboot_01
username: root
password: root
type: druid # 数据源类型
  • 如果配置文件中配置了spring.jdbc.datasource.type=c3p0使用c3p0数据源
  • 如果配置文件中配置了spring.jdbc.datasource.type=druid使用druid数据源。

在项目 spring-boot-jdbc-starter , 添加创建 c3p0 方法

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
java复制代码package com.dyy.autoconfig;

import com.alibaba.druid.pool.DruidDataSource;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;

@Configuration
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguratioin {

@Autowired
private DataSourceProperties dataSourceProperties ;

@Bean
@ConditionalOnProperty(value = "spring.jdbc.datasource.type",havingValue = "druid")
public DataSource createDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
dataSource.setUrl(dataSourceProperties.getUrl());
dataSource.setUsername(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
return dataSource;
}

@Bean
@ConditionalOnProperty(value = "spring.jdbc.datasource.type",havingValue = "c3p0")
public DataSource createC3P0DataSource() throws Exception{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass(dataSourceProperties.getDriverClassName());
dataSource.setJdbcUrl(dataSourceProperties.getUrl());
dataSource.setUser(dataSourceProperties.getUsername());
dataSource.setPassword(dataSourceProperties.getPassword());
return dataSource;
}
}

我们可以使用条件选择实现 , 如下图所示

@ConditionalOnProperty(value = “spring.jdbc.datasource.type”,havingValue = “druid”)
install安装spring-boot-jdbc-starter, 运行springboot_01

修改配置文件,重新install,再次请求。

五、spring boot常用启动器

1.SpringBoot整合MVC

创建项目 springboot_02_mvc

1.依赖

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2.新建入口程序类

1
2
3
4
5
6
7
8
9
typescript复制代码package com.dyy;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}

3.新建javabean

1
2
3
4
5
6
7
java复制代码public class User {
private String username ;
private String password ;
private Integer age ;
private String sex ;
…
}

4.新建UserController

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
java复制代码import com.dyy.pojo.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.ArrayList;
import java.util.List;

@Controller
@RequestMapping(path = "/user")
public class UserController {

@RequestMapping(path = "/findAll")
@ResponseBody
public List<User> findAll(){
//查询所有
List<User> users = new ArrayList<User>();

User user1 = new User();
user1.setUsername("杨过");
user1.setPassword("123456");
user1.setAge(18);
user1.setSex("男");

User user2 = new User();
user2.setUsername("杨过");
user2.setPassword("123456");
user2.setAge(18);
user2.setSex("男");

User user3 = new User();
user3.setUsername("杨过");
user3.setPassword("123456");
user3.setAge(18);
user3.setSex("男");

users.add(user1);
users.add(user2);
users.add(user3);

return users ;
}
}

运行程序。

2.静态资源目录

之前,开发web项目,如果是普通的项目金泰资源可以放在项目的webapp目录下。

现在使用Spring Boot做开发,项目中没有webapp目录,我们的项目是一个jar工程,那么就没有webapp,我么的静态资源该放哪里?

在springboot 中有一个叫做ResourceProperties得类,里面就定义了静态资源的默认查找路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@ConfigurationProperties(
prefix = "spring.resources",
ignoreUnknownFields = false
)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};
private String[] staticLocations;
private boolean addMappings;
private final ResourceProperties.Chain chain;
private final ResourceProperties.Cache cache;

public ResourceProperties() {
this.staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
this.addMappings = true;
this.chain = new ResourceProperties.Chain();
this.cache = new ResourceProperties.Cache();
}
//...
}

可以看到默认的静态资源路径为:

  • classpath:/META-INF/resources/
  • classpath:/resources/
  • classpath:/static/
  • classpath:/public

只要静态资源放在这些目录中的任何一个,SpringMVC都会帮我们处理。我们习惯会把静态资源放在classpath:/static/目录下。在resources目录下创建index.html文件。

打开浏览器输入 : http://localhost:8080/index.html

如果想要修改默认的静态资源路径,需要在application.yml中添加

1
2
3
4
5
> javascript复制代码spring:
> resources:
> static-locations: classpath:/webapp/
>
>

重新安装启动后生效。

3.自定义的拦截器

web开发中的拦截器也是我们经常需要使用的组件,可以帮我们完成一些日志记录,数据过滤,请求过滤等等很多功能,那么在SpringBoot中该怎么配置?

在SpringMVC中配置拦截器的步骤:

  1. 编写一个拦截器(需要实现HandlerInterceptor接口)
  2. 注册拦截器(xml配置mvc:interceptors)
1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码ml
<!--配置拦截器-->
<mvc:interceptors>
<mvc:interceptor>
<!--配置拦截路径-->
<mvc:mapping path="/user/**"/>
<!--配置不拦截路径:不拦截路径是指从拦截路径中排除-->
<mvc:exclude-mapping path="/user/sayByby"></mvc:exclude-mapping>
<!--配置拦截器bean-->
<bean class="com.dyy.interceptor.LogInterceptor2"></bean>
</mvc:interceptor>
</mvc:interceptors>

因为Spring Boot没有XML配置文件了,所以在SpringBoot中使用拦截器注册的方式就不太一样了,需要借助一个WebMvcConfigurer类帮助我们注册拦截器,实现拦截器的具体步骤如下:

  1. 编写一个拦截器(实现HandlerInterceptor接口)
  2. 通过WebMvcConfigurer注册拦截器(自定义类是实现WebMvcConfigurer接口中public void addInterceptors(InterceptorRegistry registry)方法)

具体步骤如下

1.编写拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor拦截器的preHandle方法执行....");
return false;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor拦截器的postHandle方法执行....");
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor拦截器的afterCompletion方法执行....");
}
}

2.注册拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码
import com.dyy.interceptor.MyInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Autowired
private MyInterceptor myInterceptor ;

/**
* /** 拦截当前目录及子目录下的所有路径 /user/** /user/findAll /user/order/findAll
* /* 拦截当前目录下的以及子路径 /user/* /user/findAll
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/**");
}
}

打开浏览器输入 http://localhost:8888/user/findAll观察控制台。

4.整合Spring Data JPA

1.新建项目 springboot_jpa

2.添加Spring Data JPA的起步依赖

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
xml复制代码    <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- springBoot JPA的起步依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- MySQL连接驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- 配置使用redis启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

</dependencies>

3.在application.yml中配置数据库和jpa的相关属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yml复制代码logging:
level:
com.dyy.dao: debug # 配置日志
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/springboot?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
database: mysql
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update
naming_strategy: org.hibernate.cfg.ImprovedNamingStrategy
server:
port: 18081

注意:Mysql8.x版本,连接时url需要指定时区,并且驱动类包名发生了变化。

4.创建实体类配置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
import javax.persistence.*;

@Entity
@Table(name = "user")
public class User{

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "name")
private String name;

//此处省略setter和getter方法... ...
}

5.编写UserRepository

1
2
3
4
5
java复制代码import com.dyy.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User,Integer> {
}

6.编写service类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码
import com.dyy.domain.User;
import java.util.List;

public interface UserService {
List<User> findUsers();

User findUserById(Integer id);

void saveUser(User user);

void updateUser(User user);

void deleteUserById(Integer id);
}

7.service实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
java复制代码
import com.dyy.dao.UserDao;
import com.dyy.domain.User;
import com.dyy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;

/**
* 查询所有
* @return
*/
@Override
public List<User> findUsers() {
return userDao.findAll();
}
/**
* 根据id查询
* @return
*/
@Override
public User findUserById(Integer id) {
return userDao.findById(id).get();
}
/**
* 保存
* @return
*/
@Override
public void saveUser(User user) {
userDao.save(user);
}
/**
* 更新
* @return
*/
@Override
public void updateUser(User user) {
userDao.save(user);
}
/**
* 根据id删除
* @return
*/
@Override
public void deleteUserById(Integer id) {
userDao.deleteById(id);
}
}

8.编写Controller类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码
import com.dyy.domain.User;
import com.dyy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private UserService userService;

@RequestMapping("/findAll")
public List<User> findAll(){
return userService.findUsers();
}
}

六、SpringBoot应用

1.环境搭建

1.数据库准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sql复制代码create database springboot character set utf8 ;

use springboot ;

CREATE TABLE `tb_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`gender` varchar(5) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`address` varchar(32) DEFAULT NULL,
`qq` varchar(20) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`username` varchar(20) NOT NULL,
`phone` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_username_uindex` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

INSERT INTO `tb_user` VALUES (1,'黄蓉','女',38,'桃花岛','212223390222','huangrong222@qq.com','huangrong','15600003333'),(2,'黄老邪','男',58,'湖北省武汉市','212223390','huanglaoxie@qq.com','huanglaoxie','15872320405'),(3,'小龙女','男',18,'湖北省荆门市','212223390','xiaolongnv@qq.com','xiaolongnv','15600004444'),(7,'杨过','男',30,'扬州','212223390','yangguo@qq.com','yangguo','15600005555');

2.创建项目以及包结构

创建项目springboot_case

3.导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

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

<dependencies>
<!--单元测试启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

<!--通用mapper启动器依赖-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<!--JDBC启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--druid启动器依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--web启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--spring boot actuator依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!--编码工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

<!—热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

</dependencies>

<build>
<plugins>
<!--spring boot maven插件 , 可以将项目运行依赖的jar包打到我们的项目中-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

4.创建启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan(basePackages = "com.dyy.dao")
@EnableTransactionManagement
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}

2.数据访问层

1.编写配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码server:
port: 10001
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///springboot
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
type-aliases-package: com.dyy.pojo

2.编写实体类User

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
import java.io.Serializable;

@Entity
@Table(name = "tb_user")
public class User implements Serializable {
private Integer id;
private String name;
private String gender;
private Integer age;
private String address;
private String qq;
private String email;
private String username;
private String phone;
//getter setter toString…
}

3.mapper接口和映射配置

1
2
3
4
5
6
java复制代码
import tk.mybatis.mapper.common.Mapper;
import java.util.List;

public interface UserMapper extends Mapper<User> {
}

4.编写测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {

@Autowired
private UserMapper userMapper ;

@Test
public void findAll() {
List<User> users = userMapper.selectAll();
System.out.println(users);
}
}

3.业务层

1.编写接口

1
2
3
4
5
6
7
8
java复制代码public interface UserService {

/**
* 查询所有用户信息
* @return
*/
public List<User> findAll();
}

2.编写实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
import com.dyy.dao.UserMapper;
import com.dyy.pojo.User;
import com.dyy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;

@Override
@Transactional(readOnly = true ,propagation = Propagation.SUPPORTS)
public List<User> findAll() {
return userMapper.selectAll();
}
}

3.编写测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

@Autowired
private UserService userService;

@Test
public void findAll() {
List<User> users = userService.findAll();
System.out.println(users);
}
}

注

Spring Boot整合单元测试,需要在测试类上添加两个注解:

  1. @RunWith(SpringRunner.class)指定Junit核心运行类
  2. @SpringBootTest指定这是一个Spring Boot的测试类,运行时会自动加载Spring Boot运行环境

4.表现层

1.引入依赖

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!--Web起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--编码工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>

2.新建工具类

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
java复制代码
import java.io.Serializable;

public class Result implements Serializable {
private boolean status ; //响应状态 true false
private String msg ; // 响应信息
private Object data ; //处理成功的响应数据

public static Result ok(Object data){
Result result = new Result();
result.setStatus(true);
result.setData(data);
return result ;
}

public static Result error(String msg){
Result result = new Result();
result.setStatus(false);
result.setMsg(msg);
return result ;
}

// 生成set get tostring方法

}

3.编写表现层代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Controller
@RequestMapping(path = "/user")
public class UserController {

@Autowired
private UserService userService;

/**
* 查询所有用户信息
* @return
*/
@RequestMapping(path = "/findAll")
@ResponseBody
public Result findAll() {
List<User> users = userService.findAll();
return Result.ok(users);
}
}

4.代码测试

使用postman进行测试

5.页面展示

在resources目录下创建static目录 , 创建页面展示、list.html:

  • 页面异步请求的端口和服务器端口一致
  • 页面异步请求访问的路径和对应的表现层控制方法路径要致
  • 页面异步请求参数名称和和对应的表现层控制方法参数一致

修改之后, 访问页面即可 : localhost:10001/list.html

6.缓存优化

1.缓存需求

问题:用户数据是不经常变化的数据,如果这些数据每次都去数据库中进行查询,效率比较低,对数据库造成很大的压力。

解决:缓存,用户第一次查询数据的时候,京数据存入到缓存中,之后再查询数据直接从缓存中获取即可,不需要再查询数据库。【此处使用redis缓存数据库存储数据】

2.依赖

1
2
3
4
5
xml复制代码<!--springboot整合redis启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.编写配置文件

1
2
3
4
yml复制代码spring:
redis: # 配置redis
host: localhost
port: 6379

4.修改业务层实现类代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码
import com.dyy.mapper.UserMapper;
import com.dyy.pojo.User;
import com.dyy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper ;
@Autowired
private RedisTemplate redisTemplate ;

@Override
@Transactional(readOnly = true ,propagation = Propagation.SUPPORTS)
public List<User> findAll() {
//从缓存中查询数据 规定存储用户信息使用string类型进行存储, 存储的key就是userList
List<User> userList = (List<User>) redisTemplate.boundValueOps("userList").get();
//如果缓存中没有数据, 查询数据库 , 将查询到的数据放入缓存
if(userList==null){
userList = userMapper.findAll();
redisTemplate.boundValueOps("userList").set(userList);
System.out.println("从数据库中查询...");
}else {
System.out.println("从缓存中查询.....");
}

//如果缓存中有数据, 直接返回
return userList ;
}
}

七、SpringBoot其他组件

1.SpringBoot Actuator组件

作用

Spring Boot Actuator是SpringBoot自带的一个组件 , 可以帮助我们监控和管理Spring Boot应用,比如健康检查、审计、统计和HTTP追踪等。

如何使用

1.引入SpringBoot Actuator起步依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2.配置SpringBoot Actuator参数
1
2
3
4
5
6
7
8
9
10
11
yml复制代码management:
endpoints:
web:
exposure:
include: '*' # 对外暴露的访问入口 , 默认是/health和/info
base-path: /monitor # 默认是actuator
endpoint:
health:
show-details: ALWAYS # 显示所有健康状态
server:
port: 9999
3.启动项目获取系统信息

目启动之后就可以通过发送http请求获取系统健康数据了 , 例如 : http://localhost:9999/monitor/health , 返回数据如下 :

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
json复制代码{
"status": "UP",
"details": {
"db": {
"status": "UP",
"details": {
"database": "MySQL",
"hello": 1
}
},
"diskSpace": {
"status": "UP",
"details": {
"total": 355816562688,
"free": 129251151872,
"threshold": 10485760
}
},
"redis": {
"status": "UP",
"details": {
"version": "2.8.9"
}
}
}
}

常用的访问路径

HTTP 方法 路径 描述
GET /autoconfig 提供了一份自动配置报告,记录哪些自动配置条件通过了,哪些没通过
GET /configprops 描述配置属性(包含默认值)如何注入Bean
GET /beans 描述应用程序上下文里全部的Bean,以及它们的关系
GET /dump 获取线程活动的快照
GET /env 获取全部环境属性
GET /env/{name} 根据名称获取特定的环境属性值
GET /health 报告应用程序的健康指标,这些值由HealthIndicator的实现类提供
GET /info 获取应用程序的定制信息,这些信息由info打头的属性提供
GET /mappings 描述全部的URI路径,以及它们和控制器(包含Actuator端点)的映射关系
GET /metrics 报告各种应用程序度量信息,比如内存用量和HTTP请求计数
GET /metrics/{name} 报告指定名称的应用程序度量值
POST /shutdown 关闭应用程序,要求endpoints.shutdown.enabled设置为true
GET /trace 提供基本的HTTP请求跟踪信息(时间戳、HTTP头等)

我们可以通过发送这些请求,获取系统状态信息。

2.SpringBoot Admin组件

上面我们讲了Spring Boot Actuator , 可以通过http协议获取系统状态信息 , 但是返回的是JSON格式数据, 看起来不太方面, 而且还需要记忆路径, 比较麻烦 , Spring Boot Admin给我们提供了更加友好的可视化界面来查看这些信息 !

Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序。 应用程序作为Spring Boot Admin Client向Spring Boot Admin Server注册 , Client会定时向Server发送数据, Server使用友好的界面展示数据。

1.SpringBoot Admin服务端

1.创建项目springboot-admin-server
2.依赖
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

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

<groupId>com.dyy</groupId>
<artifactId>springboot-admin-server</artifactId>
<version>1.0-SNAPSHOT</version>

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

<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>
</project>
3.配置application.yml
1
2
3
4
5
yml复制代码spring:
application:
name: admin-server
server:
port: 8769

我们将Spring Boot Admin端口号设置为8769.

4.启动类
1
2
3
4
5
6
7
java复制代码@SpringBootApplication
@EnableAdminServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}

@EnableAdminServer的作用是开启管理服务。

2.SpringBoot Admin客户端

1.创建springboot-admin-client
2.依赖
1
2
3
4
5
xml复制代码<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.0</version>
</dependency>
3.配置

向admin-server注册的地址为http://localhost:8769,最后暴漏自己的actuator所有的端口信息,具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yml复制代码server:
port: 9999
spring:
application:
name: admin-client
boot:
admin:
client:
url: http://localhost:8769 # 指定注册地址 , Spring Boot Admin Server地址
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: ALWAYS

这个注册的地址一定要和SpringBoot Admin Server地址匹配。

4.启动测试

分别开启客户端(应用程序)和服务端,访问http://localhost:8769,可以看到这样一个界面。

springboot-4.png

八、Spring Boot项目打包部署

1.项目打包

1.在pom中配置Spring Boot 项目的maven插件

1
2
3
4
5
6
7
8
9
xml复制代码<build>
<plugins>
<!-- 打jar包时如果不配置该插件,打出来的jar包没有清单文件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

2.运行maven的打包命令

3.打包之前我们需要跳过测试

如果不跳过测试,那么我们编写的测试类都会被maven自动执行,可能会出现错误,导致打包不成功。

springboot-5.png

4.执行之后可以在控制台看到打包的日志信息,其中有生成的包的位置

打开指定目录就可以发现有一个jar包存在 , 仔细观察其实我们会发现 , 在target目录下其实会存在二个jar包 , 一个是springboot_02-1.0-SNAPSHOT.jar一个是springboot_02-1.0-SNAPSHOT.jar.original , 那么这两个jar包有什么区别呢?

我们如果是普通项目打包那么就只会得到一个jar包 , 这个jar包中不包含项目的一些依赖jar包

但是我们现在是一个Spring Boot项目 , 我们希望打完的包能够直接运行, 所以项目中就必须包含他的依赖jar包 , 我们之前在pom.xml中配置一个Spring Boot的maven插件可以在普通包的基础上将我们项目的一些运行及依赖信息打进jar包里面 , 打完包之后将原来的普通包改名为xxx.jar.original , 新打的包为xxx.jar .

也就是说

  1. .jar.original 是普通jar包,不包含依赖
  2. .jar 是可执行jar包,包含了pom中的所有依赖,可以直接用java -jar 命令执行
  3. 如果是部署,就用.jar , 如果是给别的项目用,就要给.jar.original这个包

2.项目运行

打开命令行运行打出来的包:使用命令:java -jar包全名

1
复制代码java -jar springboot_02-1.0-SNAPSHOT.jar

本文转载自: 掘金

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

Java线程池实践原理学习(二)

发表于 2021-11-28

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

一、线程池内部探究

1.1 线程池生命周期管理

ThreadPoolExecutor 的运行状态有5种,分别为:

运行状态 状态描述
RUNNING 能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
STOP 不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程
TIDYING 所有的任务都已终止,workCount(有效线程数)为0
TERMINATED 在terminated()方法执行完后进入该状态

image.png

1.2任务调度

任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。

首先,所有任务的调度都是由 execute 方法完成的,这部分完成的工作是:检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行,或是缓冲到队列中执行,亦或是直接拒绝该任务。其执行过程如下:

  1. 首先检测线程池运行状态,如果不是 RUNNING,则直接拒绝,线程池要保证在 RUNNING 的状态下执行任务。
  2. 如果 workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果 workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果 workerCount >= corePoolSize && workerCount < maximumPoolSize- poolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果 workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满 , 则根据拒绝策略来处理该任务 , 默认的处理方式是直接抛异常。
* `corePoolSize`:


**线程池的基本大小,即在没有任务需要执行的时候线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。**
* `maximumPoolSize`


**线程池中允许的最大线程数**
* `poolSize`


**线程池中当前线程的数量**

image.png

1.3 任务缓冲

线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列 (BlockingQueue) 是一个支持两个附加操作的队列。
这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。
    阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

image.png

1.4 任务申请

任务的执行有两种可能:

  1. 一种是任务直接由新创建的线程执行。
  2. 另一种是线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。
    第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

image.png

如果线程池现在不应该持有那么多线程,则会返回 null 值。工作线程 Worker 会不断接收新任务去执行,而当工作线程 Worker 接收不到任务的时候,就会开始被回收。

1.5 任务拒绝

任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到 maximumPoolSize 时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。拒绝策略是一个接口,其设计如下:

1
2
3
csharp复制代码public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

如下为JDK提供的四种拒绝策略

image.png

二、Worker线程管理

2.1 Worker线程

线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程 Worker。

1
2
3
4
scala复制代码private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
final Thread thread;//Worker持有的线程
Runnable firstTask;//初始化的任务,可以为null
}

Worker 这个工作线程,实现了 Runnable 接口,并持有一个线程 thread,一个初始化的任务 firstTask。thread 是在调用构造方法时通过 **ThreadFactory ** 来创建的线程,可以用来执行任务。firstTask 用它来保存传入的第一个任务,这个任务可以有也可以为 null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是 null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

image.png

线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张 Hash 表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作 来控制线程的生命周期。这个时候重要的就是如何判断线程是否在运行。

Worker 是通过继承AQS,使用AQS来实现独占锁这个功能。没有使用可重入锁ReentrantLock,而是使用 AQS,为的就是实现不可重入的特性去反应线程现在的执行状态。

  1. lock 方法一旦获取了独占锁,表示当前线程正在执行任务中。
  2. 如果正在执行任务,则不应该中断线程。
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
  4. 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

2.2 Worker线程增加

addWorker方法有两个参数:firstTask、core。firstTask 参数用于指定新增的线程 执行的第一个任务,该参数可以为空;core 参数为 true 表示在新增线程时会判断当 前活动线程数是否少于 corePoolSize,false 表示新增线程前需要判断当前活动线程 数是否少于 maximumPoolSize,其执行流程如下图所示:

image.png

2.3 Worker线程回收

线程池中线程的销毁依赖 JVM 自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。Worker 被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当 Worker 无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

2.4 Worker线程执行任务

在 Worker 类中的 run 方法调用了 runWorker方法来执行任务,runWorker 方法的 执行过程如下:

  1. while 循环不断地通过 getTask() 方法获取任务。
  2. getTask() 方法从阻塞队列中取任务。
  3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。
  4. 执行任务。
  5. 如果 getTask 结果为 null 则跳出循环,执行 processWorkerExit() 方法,销毁线程。

image.png

本文转载自: 掘金

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

哈希表の奇妙冒险

发表于 2021-11-28

哈希表是常见的数据结构,其本质就是用空间换取时间。

各种语言对哈希表的实现各有不同,本文主要以Java和Go以及Redis的哈希表做主要陈述。

哈希表的实现

Java中的实现

熟悉的Java的同学都知道,HashMap是老朋友了,经常使用到且非常好用。相比起以前的HashTable,HashMap支持null作为键值插入,作为键只能有一个,作为值可以有多个。

JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。

image.png

JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

image.png

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

并发安全的Map

我们知道HashMap在并发下是不安全的,在高并发的情况下:

Rehash过程将数据迁移到新桶的过程中,数组链表中的链表形成循环链表,在后面的get操作时会无限循环

image.png

详细细节可以参考
blog.csdn.net/bjwfm2011/a…

Java 7 中 ConcurrentHashMap 的存储结构如上图,ConcurrnetHashMap 由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,你也可以认为 ConcurrentHashMap 默认支持最多 16 个线程并发。

image.png

Java 8 中的 ConcurrentHashMap 使用 Synchronized 锁加 CAS 的机制来确保并发安全。结构与Java 8 的HashMap Node 数组 + 链表 / 红黑树 类似。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

关于如何保证并发安全可以参考blog.csdn.net/taurus_7c/a…

Go中的实现

参考视频www.bilibili.com/video/BV1Sp…

bmap是具体存kv键值对的结构,上面8bit存的是tophash(哈希值的高8位),接下来是8个key紧凑和8个value紧凑。

image.png

  • count: 键值对数目
  • B: 桶数目是2的多少次幂
  • buckets: 指向bmap也就是具体的桶
  • old buckets: 指向旧桶,扩容时用
  • nevacuate: 即将迁移的旧桶编号

Go语言使用渐进式的方式扩容,与Redis一样,这是为了rehash的时候不产生瞬时的抖动影响性能。Go语言中的负载因子为6.5,即count/2^B > 6.5时扩容成两倍。如果B>4那么认为用到溢出桶的概率比较大,会产生2^(B-4)个溢出桶,溢出桶是为了避免rehash,且溢出桶与前面的2^B桶地址连续。

image.png

并发安全的Map

参考colobu.com/2017/07/11/…

go 1.9之前的解决Map并发不安全是额外绑定一个锁,封装成一个新的struct或者单独使用锁都可以。

与Java处理类似,Go语言社区广泛使用分段锁策略将 key 分散到固定数量的 shard 中避免 rehash 操作。shard 是有锁保护的 map, 当 shard 进行 rehash 时会阻塞shard内的读写,但不会对其他 shard 造成影响。

具体源码实现参考studygolang.com/articles/18…

Go语言在1.9版本推出了sync.Map

参考colobu.com/2017/07/11/…

其优点有:

  1. 空间换时间。 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。
  2. 使用只读数据(read),避免读写冲突。
  3. 动态调整,miss次数多了之后,将dirty数据提升为read。
  4. double-checking。
  5. 延迟删除。 删除一个键值只是打标记,只有在提升dirty的时候才清理删除的数据。
  6. 优先从read读取、更新、删除,因为对read的读取不需要锁。

它使用了冗余的数据结构read、dirty。dirty中会包含read中为删除的entries,新增加的entries会加入到dirty中。

image.png

  • mu (Mutex): 当涉及到dirty数据的操作的时候,需要使用这个锁
  • read (atomic.Value): 一个只读的数据结构,因为只读,所以不会有读写冲突。
  • dirty (map[interface{}]*entry): 需要加锁,因为对它的操作可能会有读写竞争。
  • misses (int): 如果read中不包含这个entry, 会尝试从dirty中读取,这个时候会将misses + 1,当misses累积到 dirty 的长度的时候, 就会将dirty提升为read,避免从dirty中miss太多次。因为操作dirty需要加锁。

虽然read和dirty有冗余数据,但这些数据是通过指针指向同一个数据,所以尽管Map的value会很大,但是冗余的空间占用还是有限的。

如果查询的键值正好存在于read中,无须加锁,直接返回,理论上性能优异。即使不存在于read中,经过miss几次之后,dirty会被提升为read,又会从read中查找。所以对于更新/增加较少,加载存在的key很多的case, 性能基本和无锁的map类似。双检查避免加锁的时候dirty提升为read, 这个时候read可能被替换了查找不到。

修改(新增、更新和删除)都是先从操作read开始的,不满足条件再加锁,然后操作dirty。如果这个entry不存在于read中,并且dirty中有新数据,则加锁尝试从dirty中修改。删除要双检查的,从dirty中直接删除即可,就当它没存在过,但是如果是从read中删除,并不会直接删除,而是打标记。

Redis中的实现

参考www.w3cschool.cn/hdclil/lun1…

image.png

  • table: 是一个数组, 数组中的每个元素都是一个指向 dictEntry 结构的指针, 每个 dictEntry 结构保存着一个键值对。
  • size: 记录了哈希表的大小, 也即是 table 数组的大小
  • used: 记录了哈希表目前已有节点(键值对)的数量。
  • sizemask: 等于 size - 1 , 这个属性和哈希值一起决定一个键应该被放到 table 数组的哪个索引上面。

image.png

ht有两个dictht, 第二个ht[1]是扩容时候用的,和Go语言一样采用渐进式扩容。 扩容时可以看成,ht[0]的元素慢慢的被取下放到ht[1]里面。当扩容完毕后,原ht[1]升级为新ht[0], 原ht[0]直接为空的充当新的ht[1]。

在扩容期间,会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

本文转载自: 掘金

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

CPU、内存分段分页机制了解

发表于 2021-11-28

CPU

CPU.png

进程上下文

操作系统为进程分配的CPU以及内存资源就是这个进程的上下文


内存地址

一、虚拟内存地址:进程使用的内存地址

二、物理内存地址:实际存在硬件里面的空间地址

进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存

操作系统是如何管理虚拟地址与物理地址之间的关系?

主要有两种方式,分别是内存分段和内存分页


内存管理

一、内存分页存储:在页式存储管理中,将程序的逻辑地址划分为固定大小的页(page),而物理内存划分为同样大小的帧,程序加载时,可以将任意一页放入内存中任意一个帧,这些帧不必连续,从而实现了离散分离

(1)页是信息的物理单位,分页是为实现离散分配方式;

(2)以消减内存的外零头,提高内存的利用率;但是会有内零头;

(3)页的大小固定且由系统确定

在 Linux 下,每一页的大小为 4KB。 虚拟地址与物理地址之间通过页表来映射

当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行

对于一个内存地址转换,其实就是这样三个步骤:

  1. 把虚拟内存地址,切分成页号和偏移量;
  2. 根据页号,从页表里面,查询对应的物理页号;
  3. 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

内存分页.png

二、内存分段存储:在段式存储管理中,将程序的地址空间划分为若干段(segment),如代码段,数据段,堆栈段;这样每个进程有一个二维地址空间,相互独立,互不干扰

(1)没有内零头,会产生外零头

区分:

  1. 目的不同:分页是由于系统管理的需要而不是用户的需要,它是信息的物理单位;分段的目的是为了能更好地满足用户的需要,它是信息的逻辑单位,它含有一组其意义相对完整的信息;
  2. 大小不同:页的大小固定且由系统决定,而段的长度却不固定,由其所完成的功能决定;
  3. 地址空间不同: 段向用户提供二维地址空间;页向用户提供的是一维地址空间;
  4. 信息共享:段是信息的逻辑单位,便于存储保护和信息的共享,页的保护和共享受到限制;
  5. 内存碎片:页式存储管理的优点是没有外碎片(因为页的大小固定),但会产生内碎片(一个页可能填充不满);而段式管理的优点是没有内碎片(因为段大小可变,改变段大小来消除内碎片)。但段换入换出时,会产生外碎片(比如4k的段换5k的段,会产生1k的外碎片)。

内存分段.png

三、段页式内存管理(每个程序一张段表,每个段建立一张页表)

  1. 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
  2. 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页

步骤:

  1. 第一次访问段表,得到页表起始地址;
  2. 第二次访问页表,得到物理页号;
  3. 第三次将物理页号与页内位移组合,得到物理地址。

段页式内存管理.png


内存交换空间(Linux中的Swap)

解决外部内碎片问题:将某个进程占用的内存写到硬上,然后再重硬盘读回内存,但是读回时是紧接在已经被占用的内存的后面,从而腾出更加多内存空间

内存分段:对于多进程的系统来说,用分段的方式,内存碎片是很容易产生的,产生了内存碎片,那不得不重新 Swap 内存区域,这个过程会产生性能瓶颈。 因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。 所以,如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。

内存分页:如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高


内存零头

  1. 外零头:内存中存在一些区域不归任何进程所有,由于区域太小,无法满足其他进程所申请的内存大小。
  2. 内零头:指已分配给进程的内存空间但未被使用的那一部分区域。

本文转载自: 掘金

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

Nginx(十一)

发表于 2021-11-28

一、简介

Nginx (“engine x”) 是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,中国大陆使用nginx网站用户有:百度、京东、新浪、网易、腾讯、淘宝等。

1.Nginx应用场景

​ Nginx可以作为静态页面的web服务器,同时还支持CGI协议的动态语言,比如perl、php等。但是不支持java,Java程序只能同各国与tomcat配合完成。nginx专为性能优化而开发,性能是最重要的考量,实现上非常注重效率,能经受高负载的而考验,有报告表明能支持高达 50,000个并发连接数。

lnmp.org/nginx.html

news.netcraft.com/archives/ca…

2.反向代理

​ Nginx不仅可以做反向代理,实现负载均衡。还能用作正向代理来进行上网等功能。

​ 正向代理:如果把局域网外的Internet想想成一个巨大的资源库,则局域网中的客户端要访问Internet,则需要通过代理服务器来访问,这种代理服务就称为正向代理。

​ 反向代理:其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,再返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴漏的是代理服务器地址,隐藏了真实服务器IP地址。

3.负载均衡

​ 客户端发送多个请求给服务器,服务器处理请求,有一些可能要与数据库进行交互,服务器处理完毕后,再将结果返回给客户端。

​ 这种架构模式对于早期的系统相对单一,并发请求相对较少的情况下是比较适合的,成本也低。但是随着信息数量的不断增长,访问量和数据量的飞速增长,以及系统业务的复杂度增加,这种架构会造成服务器响应客户端的请求日益缓慢,并发量特别大的时候,还容易造成服务器直接崩溃。

​ 如何解决?

​ 首先可以升级服务器的配置,比如提高CPU执行频率,加大内存等提高及其的物理性能来结局这个问题,但是对于比如双十一这样的级别,某个热销商品的并发量是非常庞大的,此时最顶级的物理配置都是不能满足需求的。

​ 那么也就是说纵向解决问题的办法行不同了,那么我们可以考虑横向增加服务器的数量,这个时候集群的概念产生了,单个服务器解决不了,我们就增加服务器的数量,然后将请求分发到各个服务器上,将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是负载均衡。

4.动静分离

​ 为了加快网站的解析速度,可以把动态页面和静态页面由不同的服务器来解析,加快解析速度。降低原来单个服务器的压力。

二、安装

1.下载

​ nginx.org/ 下载 nginx-1.12.2.tar.gz

2.需要的安装包

pcre-8.37.tar.gz

openssl-1.0.1t.tar.gz

zlib-1.2.8.tar.gz

nginx-1.12.2.tar.gz

3.在线安装nginx

第一步,安装pcre

第一步 联网下载pcre

wget downloads.sourceforge.net/project/pcr…

第二步 解压压缩文件

使用命令 tar -zxvf pcre-8.37.tar.gz

第三步 ./configure完成后,回到pcre目录下执行make,最后执行make install

第四步 pcre-config –version 检查版本

第二步,安装openssl 、zlib 、 gcc 依赖

yum -y install make zlib zlib-devel gcc-c++ libtool openssl openssl-devel

第三步,安装nginx

使用命令解压

./configure

make && make install

进入目录 /usr/local/nginx/sbin 执行./nginx 启动服务

第四步,关闭防火墙,访问nginx

在windows系统中访问linux中nginx,默认不能访问的,因为防火墙问题

关闭防火墙-开放访问的端口号,80端口

查看开放的端口号

firewall-cmd –list-all

设置开放的服务或端口号

firewall-cmd –add-service=http –permanent

firewall-cmd –add-port=80/tcp –permanent

重启防火墙

firewall-cmd –reload

查看是否安装完成,可以在虚拟机上的浏览器中输入127.0.0.1:80,如果跳转成功就是安装完成了。也可以在主机上输入虚拟机ip地址:80,如果能访问,也算安装成功。

4.离线安装

第一步 安装pcre

解压缩pcre-xx.tar.gz包

进入解压缩目录,执行./configure

注意:如果提示错误,需要提前安装gcc++

1
2
3
4
5
bash复制代码执行  cd  /run/media/root/CentOS 7 x86_64/Packages(已经从中拷贝出来了26个安装包,直接使用准备好的文件夹rpmgcc安装即可)
rpm -Uvh *.rpm --nodeps --force
检查安装后版本
gcc -v
g++ -v

./configure完成后,回到pcre目录下执行make,再执行make install

第二步 安装openssl

解压缩openssl-xx.tar.gz包

进入解压缩目录,执行./config

make && make install

第三步 安装zlib

解压缩zlib-xx.tar.gz包

进入解压缩目录,执行./configure

make && make install

第四步 安装nginx

解压缩nginx-xx.tar.gz包

进入解压缩目录,执行./configure

make && make install

第五步 关闭防火墙,开放端口

三、Nginx常用命令和配置文件

1.常用命令

  1. 启动命令【在/usr/local/nginx/sbin目录下执行 ./nginx 】
  2. 关闭命令【在/usr/local/nginx/sbin目录下执行 ./nginx -s stop】
  3. 重新加载命令【在/usr/local/nginx/sbin目录下执行 ./nginx -s reload】
  4. 查看版本【在/usr/local/nginx/sbin目录下执行 ./nginx -v】
  5. 查看进程【ps -ef|grep nginx】

2.nginx.conf配置文件

​ nginx安装目录(/usr/local/nginx/conf/nginx.conf)下,其默认的配置文件都放在这个目录的conf目录下,而主配置文件nginx.conf也在其中,后续对nginx的使用基本上都是对次配置文件进行相应的修改。

​ 我们可以将nginx.conf配置文件分为三部分;

  1. 全局块

从配置文件开始到events块之间的内容,主要会设置一些影响nginx服务器整体运行的配置指令,主要包括配置运行nginx服务器的用户(组),允许生成的worker process数,进程PID存放路径,日志存放路径类型以及配置文件的引入。

1
ini复制代码worker_processes 1;

这是Nginx服务器并发处理服务器的关键配置,worker_processes值越大,可以支持的并发处理量也越多,但是会受到硬件、软件等设备的制约。
2. events块

比如

1
2
3
ini复制代码events{
worker_connections 1024;
}

events块设计的指令主要影响Nginx服务器和用户的网络连接,常用的设置包括是否开启对多work process下的网络连接进行序列化,是否允许同时接收多个网络连接,选取那种时间驱动模型来处理连接请求,每个work process可以同时支持的最大连接数等。

上述例子就表示每个work process支持的最大连接数是1024。

这部分的配置对Nginx的i性能影响比较大,在实际中应该灵活配置。
3. http块

者是nginx服务器配置中最频繁的部分,代理、缓存和日志定义等绝大多数共呢个和第三方木块的配置而都在这里。

需要注意的是:http块也可以包括http全局块、server块。

①、http 全局块

  http全局块配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。

②、server 块

  这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。

  每个 http 块可以包括多个 server 块,而每个 server 块就相当于一个虚拟主机。

  而每个 server 块也分为全局 server 块,以及可以同时包含多个 location 块。

  1、全局 server 块

  最常见的配置是本虚拟机主机的监听配置和本虚拟主机的名称或IP配置。

  2、location 块

  一个 server 块可以配置多个 location 块。

这块的主要作用是基于 Nginx 服务器接收到的请求字符串

(例如 server_name/uri-string),对虚拟主机名称(也可以是IP别名)之外的字符串(例如 前面的 /uri-string)进行匹配,对特定的请求进行处理。地址定向、数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行。

四、反向代理

1.location指令说明

该指令用于匹配 URL。

语法如下:

1、= :用于不含正则表达式的 uri 前,要求请求字符串与 uri 严格匹配,如果匹配成功,就停止继续向下搜索并立即处理该请求。

2、~:用于表示 uri 包含正则表达式,并且区分大小写。

3、~*:用于表示 uri 包含正则表达式,并且不区分大小写。

4、^~:用于不含正则表达式的 uri 前,要求 Nginx 服务器找到标识 uri 和请求字符串匹配度最高的 location 后,立即使用此 location 处理请求,而不再使用 location 块中的正则 uri 的请求字符串做匹配。

注意:如果 uri 包含正则表达式,则必须要有 ~ 或者 ~* 标识。

2.实例

实现效果

使用nginx反向代理,根据访问的路径跳转到不同端口的服务中

nginx监听端口为9000,

访问 http://127.0.0.1:9000/edu/ 直接跳转到127.0.0.1:8080

访问 http://127.0.0.1:9000/vod/ 直接跳转到127.0.0.1:8081

准备

第一步,准备两个tomcat,一个8080端口,一个8081端口,并准备好测试的页面

webapps/edu/index.html

webapps/vod/index.html

第二步,修改nginx的配置文件,在http块中添加server{}

1
2
3
4
5
6
7
8
9
10
bash复制代码server{
listen 9000;
server_name localhost;
location /edu/{
proxy_pass http://localhost:8080;
}
location /vod/{
proxy_pass http://localhost:8081;
}
}

五、负载均衡

1.nginx分配服务器策略

​ 负载均衡就是将负载分摊到不同的服务单元,既保证服务的可用性,有保证响应足够快,给用户很好的体验。nginx提供了几种分配方式(策略):

(1)轮询(默认)

每个请求按照时间顺序逐一分配到不同的后端服务器,如果后端服务器down调,能自动剔除。

(2)weight

weight代表权,默认是1,权重越高被分配的客户端越多。

指定轮询几率,weight和访问比例成正比,用于后端服务器其性能不均的情况。

例如:

1
2
3
4
5
ini复制代码upstream server_pool{   
server 192.168.5.21 weight=1;
server 192.168.5.22 weight=2;
server 192.168.5.23 weight=3;
}

(3)ip_hash

每个请求按访问ip的hash结果分配这样每个方可固定访问一个后端服务器,可以解决session的问题。例如:

1
2
3
4
5
ini复制代码upstream server_pool{   
ip_hash;
server 192.168.5.21:80;
server 192.168.5.22:80;
}

(4)fair(第三方)

​ 按后端服务器的响应时间来分配请求,响应时间短的优先分配。

1
2
3
4
5
ini复制代码upstream server_pool{   
server 192.168.5.21:80;
server 192.168.5.22:80;
fair;
}

(5)其他参数

注:

1
2
3
4
5
ini复制代码upstream myServer{
server 192.168.6.100:8080 down;
server 192.168.6.100:8081 weight=2;
server 192.168.6.100:8082 backup;
}
  1. down

表示当前的server暂时不参与负载。
2. weight

默认1.weight越大,负载的权重就越大
3. backup

其他所有非backup及其down或者忙的时候,请求backup机器。所以这台压力会最轻。

2.实例

实现效果

浏览器地址栏输入地址 http://192.168.6.100/edu/index.html,负载均衡效果,将请求平均分配到8080和8081两台服务器上。

准备工作

(1)准备两台tomcat服务器,一台8080,一台8081

(2)在两台tomcat里面webapps目录中,创建名称是edu文件夹,在edu文件夹中创建页面index.html(让index.html内容不一样,查看效果),用于测试

在nginx配置文件中进行负载均衡的配置

upstream称为上游服务器,即真实处理请求的业务服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码upstream myTomcatServer{
server 192.168.6.100:8080;
server 192.168.6.100:8081;
}
server{
listen 80;
server_name localhost;
location / {
root html;
proxy_pass http://myTomcatServer;
index index.html index.htm;
}
}

六、动静分离

​ Nginx动静分离简单来说就是把动态跟静态分开,不能简单理解成只是单纯把静态页面和动态页面物理分离。严格意义上说应该是动态请求和静态请求分开,可以理解成使用Nginx处理静态页面,Tomcat处理动态页面。动静分离从目前实现角度讲大致分为两种:

  1. 纯粹把静态文件独立成单独的域名,放在独立的服务器上,也是目前主流推崇的方案。
  2. 动态跟静态文件混合在一起发布,通过Nginx分开。

通过location指定不同的后缀名实现不同的请求转发。

通过expires参数设置,可以设置浏览器缓存过期时间,减少服务器之前的请求和流量。

具体expires定义:是给资源设定一个过期时间,也就是说无需去服务端验证,直接通过浏览器自身确认是否过期即可,所以不会产生额外流量。

这种方法非常适合不经常变动的资源。(如果是经常更新的文件,不建议使用Expires来缓存)。设置参数值为3d,表示在这3天之内访问这个URL,发送一个请求,比对服务器该文件最后更新时间没有变化,则不会从服务器抓取,返回状态码304,如果有修改,则直接从服务器重新下载,返回状态码200.

实例

配置

1.静态资源准备

在/root目录下创建一个data目录,data目录下创建一个image目录,里面放一张图片。

2.nginx配置

找到nginx安装目录,打开/usr/local/nginx/conf/nginx.conf配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ini复制代码server{
listen 80;
server_name localhost;


location /images/ {
root /root/data/;
autoindex on;
}

location /edu/ {
root html;
proxy_pass http://myTomcatServer;
index index.html index.htm;
}
}

添加监听端口、访问服务器名字;重点是添加location;

最后检查Nginx配置是否正确即可,然后测试动静分离是否成功。

测试

1.浏览器中输入地址

http://192.168,6,100/image/bj.jpg

2.如果出现403Forbidden,是权限原因

解决办法:

修改/usr/local/nginx/conf/nginx.conf配置文件

1
2
ini复制代码user root;
worker_processes 1;

修改user为root角色

3.因为配置文件最后配置了autoindex on,所以访问/image/目录时可以列出目录中的内容,注意最后的/不能少。

七、Nginx原理和优化参数配置

1.Nginx原理

nginx-1.png

nginx-2.png

master-workers机制的优点

​ 首先,对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在变成以及问题查找时,也会方便很多。

​ 其次,采用独立的进程,可以让他们互相之间不会影响,一个进程退出后,其他进程还在工作,服务器不会中断,master进程则很快启动新的worker进程。

​ 当然,worker进程的异常退出,肯定是程序有bug了,异常退出,会导致当前worker上的所有请求失败,不过不会影响到所有请求,所以降低了风险。

需要设置多少个worker

​ Nginx同redis类似都采用了io多路复用机制,每个worker都是一个独立的进程,但每个进程里只有一个主线程,通过异步非阻塞的方式来处理请求,即使是上千万个请求也不在话下。每个worker的线程可以把一个cpu的性能发挥到极致。

​ 所以worker数和服务器的cpu数相等时最为适宜的。设置少了会浪费cpu,设置多了会造成cpu频繁切换上下文带来的损耗。

1
2
3
4
5
6
7
8
yaml复制代码
#设置worker数量
worker_processes 4
#work绑定cpu(4 work绑定4cpu)。
worker_cpu_affinity 0001 0010 0100 1000

#work绑定cpu (4 work绑定8cpu中的4个) 。
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000
1
2
yaml复制代码#连接数
worker_connections 1024

这个值表示每个worker进程所能建立连接的最大值,所以一个nginx能建立的最大连接数,应该是worker_connections*worker_processes。当然,这里说的是最大连接数,对于请求本地资源来说,能够支持的最大并发数量是worker_connections*worker_processes,如果是支持http1.1的浏览器每次访问要占两个连接,所以普通的静态访问最大并数是:worker_connections*worker_process/2,而如果是Http作为反向代理来说,最大并发数量应该是worker_connections*worker_processes/4。

【这是因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接】

面试题:

第一个:发送请求,占用了woker的几个连接数?

动态4个,静态2个。

第二个:nginx有一个master,有四个woker,每个woker支持最大的连接数1024,支持的最大并发数是多少?

(1*4*1024/2)个静态资源并发数,(1*4*1024/4)个动态资源的并发数

2.Nginx.conf配置详解

nginx-3.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
ini复制代码#安全问题,建议用nobody,不要用root.
#user nobody;

#worker数和服务器的cpu数相等是最为适宜
worker_processes 2;

#work绑定cpu(4 work绑定4cpu)
worker_cpu_affinity 0001 0010 0100 1000

#work绑定cpu (4 work绑定8cpu中的4个) 。
worker_cpu_affinity 0000001 00000010 00000100 00001000

#error_log path(存放路径) level(日志等级) path表示日志路径,level表示日志等级,
#具体如下:[ debug | info | notice | warn | error | crit ]
#从左至右,日志详细程度逐级递减,即debug最详细,crit最少,默认为crit。

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;

events {
#这个值是表示每个worker进程所能建立连接的最大值,所以,一个nginx能建立的最大连接数,应该是worker_connections * worker_processes。
#当然,这里说的是最大连接数,对于HTTP请求本地资源来说,能够支持的最大并发数量是worker_connections * worker_processes,
#如果是支持http1.1的浏览器每次访问要占两个连接,
#所以普通的静态访问最大并发数是: worker_connections * worker_processes /2,
#而如果是HTTP作为反向代理来说,最大并发数量应该是worker_connections * worker_processes/4。
#因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。

worker_connections 1024;

#这个值是表示nginx要支持哪种多路io复用。
#一般的Linux选择epoll, 如果是(*BSD)系列的Linux使用kquene。
#windows版本的nginx不支持多路IO复用,这个值不用配。
use epoll;

# 当一个worker抢占到一个链接时,是否尽可能的让其获得更多的连接,默认是off 。
multi_accept on; //并发量大时缓解客户端等待时间。
# 默认是on ,开启nginx的抢占锁机制。
accept_mutex on; //master指派worker抢占锁
}
http {
#当web服务器收到静态的资源文件请求时,依据请求文件的后缀名在服务器的MIME配置文件中找到对应的MIME Type,再根据MIME Type设置HTTP Response的Content-Type,然后浏览器根据Content-Type的值处理文件。

include mime.types; #/usr/local/nginx/conf/mime.types

#如果 不能从mime.types找到映射的话,用以下作为默认值-二进制
default_type application/octet-stream;

#日志位置
access_log logs/host.access.log main;

#一条典型的accesslog:
#101.226.166.254 - - [21/Oct/2013:20:34:28 +0800] "GET /movie_cat.php?year=2013 HTTP/1.1" 200 5209 "http://www.baidu.com" "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MDDR; .NET4.0C; .NET4.0E; .NET CLR 1.1.4322; Tablet PC 2.0); 360Spider"

#1)101.226.166.254:(用户IP)
#2)[21/Oct/2013:20:34:28 +0800]:(访问时间)
#3)GET:http请求方式,有GET和POST两种
#4)/movie_cat.php?year=2013:当前访问的网页是动态网页,movie_cat.php即请求的后台接口,year=2013为具体接口的参数
#5)200:服务状态,200表示正常,常见的还有,301永久重定向、4XX表示请求出错、5XX服务器内部错误
#6)5209:传送字节数为5209,单位为byte
#7)"http://www.baidu.com":refer:即当前页面的上一个网页
#8)"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; #.NET CLR 3.0.30729; Media Center PC 6.0; MDDR; .NET4.0C; .NET4.0E; .NET CLR 1.1.4322; Tablet PC 2.0); 360Spider": agent字段:通常用来记录操作系统、浏览器版本、浏览器内核等信息

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

#开启从磁盘直接到网络的文件传输,适用于有大文件上传下载的情况,提高IO效率。
sendfile on; //大文件传递优化,提高效率

#一个请求完成之后还要保持连接多久, 默认为0,表示完成请求后直接关闭连接。
#keepalive_timeout 0;
keepalive_timeout 65;

#开启或者关闭gzip模块
#gzip on ; //文件压缩,再传输,提高效率

#设置允许压缩的页面最小字节数,页面字节数从header头中的Content-Length中进行获取。
#gzip_min_lenth 1k;//超过该大小开始压缩,否则不用压缩

# gzip压缩比,1 压缩比最小处理速度最快,9 压缩比最大但处理最慢(传输快但比较消耗cpu)
#gzip_comp_level 4;

#匹配MIME类型进行压缩,(无论是否指定)"text/html"类型总是会被压缩的。
#gzip_types types text/plain text/css application/json application/x-javascript text/xml

#动静分离
#服务器端静态资源缓存,最大缓存到内存中的文件,不活跃期限
open_file_cache max=655350 inactive=20s;

#活跃期限内最少使用的次数,否则视为不活跃。
open_file_cache_min_uses 2;

#验证缓存是否活跃的时间间隔
open_file_cache_valid 30s;

upstream myserver{
# 1、轮询(默认)
# 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
# 2、指定权重
# 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
#3、IP绑定 ip_hash
# 每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
#4、备机方式 backup
# 正常情况不访问设定为backup的备机,只有当所有非备机全都宕机的情况下,服务才会进备机。当非备机启动后,自动切换到非备机
# ip_hash;
server 192.168.161.132:8080 weight=1;
server 192.168.161.132:8081 weight=1 backup;
#5、fair(第三方)公平,需要安装插件才能用
#按后端服务器的响应时间来分配请求,响应时间短的优先分配。
#6、url_hash(第三方)
#按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。

# ip_hash;
server 192.168.161.132:8080 weight=1;
server 192.168.161.132:8081 weight=1;

#fair

#hash $request_uri
#hash_method crc32

}

server {
#监听端口号
listen 80;

#服务名
server_name 192.168.161.130;

#字符集
#charset utf-8;

#location [=|~|~*|^~] /uri/ { … }
# = 精确匹配
# ~ 正则匹配,区分大小写
# ~* 正则匹配,不区分大小写
# ^~ 关闭正则匹配

#匹配原则:

# 1、所有匹配分两个阶段,第一个叫普通匹配,第二个叫正则匹配。
# 2、普通匹配,首先通过“=”来匹配完全精确的location
# 2.1、 如果没有精确匹配到, 那么按照最大前缀匹配的原则,来匹配location
# 2.2、 如果匹配到的location有^~,则以此location为匹配最终结果,如果没有那么会把匹配的结果暂存,继续进行正则匹配。
# 3、正则匹配,依次从上到下匹配前缀是~或~*的location, 一旦匹配成功一次,则立刻以此location为准,不再向下继续进行正则匹配。
# 4、如果正则匹配都不成功,则继续使用之前暂存的普通匹配成功的location.
#不是以波浪线开头的都是普通匹配。
location / { # 匹配任何查询,因为所有请求都以 / 开头。但是正则表达式规则和长的块规则将被优先和查询匹配。

#定义服务器的默认网站根目录位置
root html;//相对路径,省略了./ /user/local/nginx/html 路径

#默认访问首页索引文件的名称
index index.html index.htm;

#反向代理路径
proxy_pass http://myserver;

#反向代理的超时时间
proxy_connect_timeout 10;

proxy_redirect default;
}
#普通匹配
location /images/ {
root images ;
}
# 反正则匹配
location ^~ /images/jpg/ { # 匹配任何以 /images/jpg/ 开头的任何查询并且停止搜索。任何正则表达式将不会被测试。
root images/jpg/ ;
}
#正则匹配
location ~*.(gif|jpg|jpeg)$ {
#所有静态文件直接读取硬盘
root pic ;

#expires定义用户浏览器缓存的时间为3天,如果静态页面不常更新,可以设置更长,这样可以节省带宽和缓解服务器的压力
expires 3d; #缓存3天
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

本文转载自: 掘金

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

1…139140141…956

开发者博客

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