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

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


  • 首页

  • 归档

  • 搜索

Python 时间操作之dateutil模块 复习回顾 1

发表于 2021-11-09

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

复习回顾

关于时间相关的模块,强大的Python提供了许多内置模块。

  • time 模块:重点处理系统时间戳计算
  • calendar 模块:处理通用日历操作
  • zoneinfo 模块:Python 3.9 支持对IANA时区代码处理
  • datetime 模块:提供6个大类对时间日期更人性化操作

image.png

本期,我们将对时间操作第三方模块之一的dateutil模块相关方法学习,Let’s go~

  1. dateutil 模块概述

dateutil 模块由Gustavo Niemeyer 在2003年编写而成,经历四位维护人员进行维护,目前已经更迭得到最新的2.8.2版本

dateutil 模块对Python 内置的datetime模块进行扩展时区和解析。

查看dateutil模块包含7个模块,其中parser和rrule是主要模块

image.png

  • dateutil 模块特点

+ 能够快速计算出相对时间例如下周、下个月、明年
+ 对指定两个日期或者日期对象进行计算出相对间隔
+ 能对多种时区文件进行解析例如UTC时区、tzinfo时区、Windows注册表时区
+ 支持包括RFC或者其他任何字符串格式的日期进行解析
  • dateutil 模块获取

dateuitl 模块为Python 第三方库,因此需要使用pip工具进行下载

1
python复制代码pip install python-dateutil

image.png

  • dateutil 模块使用

+ dateutil 模块需要使用 from..import 导入
  1. dateutil 相关方法

  • datetutil 模块提供7个模块

方法 作用
dateutil.parser 将字符串解析成datetime
dateutil.rrule 将参数输出datetime.datetime格式的时间
dateutil.relativedelta 时间偏移量
dateutil.easter 复活节日期计算
dateutil.tz 对datetime.tzinfo抽象类时区实现
dateutil.utils 提供便利且实用功能来处理日期时间
dateutil.zoneinfo 重建zoneinfo tar 内部时区信息(ftp.iana.org/tz)
* dateutil 模块提供主要的方法
——————
+ dateutil.parser.parse(string)将字符串解析成


    - string 可以为任何一个字符串
    - 时间字符串形式可以有逗号、斜杠、横杆等
+ datetimedateutil.rrule.rrule() 将参数输出datetime.datetime格式的时间


rrule方法参数说明



| 参数 | 含义 |
| --- | --- |
| freq | 单位,可选的值为YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY,,MINUTELY,SECONDLY |
| dtstart | 开始时间,时间格式datetime.datatime类型 |
| interval | 间隔 |
| wkst | 周开始时间 |
| count | 生产时间的个数 |
| until | 结束时间,时间格式为datetime.dateatime类型 |
| bysetpos | 必须为整数或者整数序列,设置匹配的周期频率 |
| bymonth | 设置匹配的月份 |
| bymonthday | 设置匹配每月的日期 |
| byyearday | 设置匹配每年的天数 |
| byaster | 设置匹配的复活节,星期天为0 |
| byweekno | 设置匹配第几周 |
| byweekday | MO,TU,WE,TH,FR,SA,SU |
| byhour | 设置匹配小时 |
| byminute | 设置匹配分钟 |
| bysecond | 设置匹配秒数 |
| cache | 必须为布尔值,其实很启动缓存 |
+ dateutil.relativedelta.relativedelta()时间偏移量



| 参数 | 说明 |
| --- | --- |
| year, month, day, hour, minute, second, microsecond | 绝对单位,不会执行运算,会直接替换原始日期时间对应的值 |
| years, months, weeks, days, hours, minutes, seconds, microseconds | 相对信息,可以为正数或复负数,会对原始日期时间进行相对值进行计算 |
| weekday | 工作日增量值,可以为正数或者负数,例如MO(1),0=MO |
| leepdays | 给定日期中计算出润日 |
| yearday,nlyearday | 设置年份为闰年或者非闰年,转换成日/月/闰日 |

PS: dateutil 模块官方手册
可以查看详细的用法案例

  1. 小试牛刀

我们学习了dateutil模块相关的方法,来进行实操一下吧~

  • 使用parse解析时间字符串,rrule获取时间列表
1
2
3
4
5
6
7
python复制代码from dateutil.rrule import *

def get_two_datelist(sdate,edate):

return list(rrule(DAILY,dtstart=parse(sdate),until=parse(edate)))

print(get_two_datelist("2021-11-1","2021-11-10"))
1
2
3
4
5
6
7
8
9
10
11
12
input复制代码# 终端输出结果

[datetime.datetime(2021, 11, 1, 0, 0),
datetime.datetime(2021, 11, 2, 0, 0),
datetime.datetime(2021, 11, 3, 0, 0),
datetime.datetime(2021, 11, 4, 0, 0),
datetime.datetime(2021, 11, 5, 0, 0),
datetime.datetime(2021, 11, 6, 0, 0),
datetime.datetime(2021, 11, 7, 0, 0),
datetime.datetime(2021, 11, 8, 0, 0),
datetime.datetime(2021, 11, 9, 0, 0),
datetime.datetime(2021, 11, 10, 0, 0)]
  • 获取指定间隔的时间列表
1
2
3
4
5
python复制代码  def get_two_date_interval(sdate,edate):

return relativedelta(parse(sdate),parse(edate))

print(get_two_date_interval("2021-10-21","2021-11-10"))

image.png

总结

本期,我们对 dateutil 模块对字符串进行解析、时间间隔相关的操作进行学习。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

Go语言 sqlx 简单介绍

发表于 2021-11-09

sql操作的系统函数 如果你觉得用起来比较啰嗦的话 你还可以选择开源库,这里简单介绍下sqlx这个开源库

sqlx 的连接数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码// 定义一x全局对象 这是并发安全的对象 注意这个是sqlx了 不是sql
var db *sqlx.DB

func initMysql() (err error) {
dsn := "root:1234qwer@tcp(127.0.0.1:3306)/go_test"
// 原生的 是open 这里直接用connect 就行了 里面包含了ping的操作
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
panic(err)
}

db.SetConnMaxLifetime(time.Second * 10)
// 设置最大的连接数 默认是无限制 如果超出限制了 就会排队等待
db.SetMaxOpenConns(200)
// 设置最大的空闲连接数 默认是无限制 业务量小的时候 可以把多余的连接释放掉,只保留一定数量的连接数
db.SetMaxIdleConns(10)
return nil
}

sqlx的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码// 这里注意首字母要大写了
// 因为sqlx是用的反射来对结构体进行赋值,所以是跨包的,
// 那自然这个结构体的 field 要首字母大写了
type user struct {
Id int `db:"id"`
Age int `db:"age"`
Name string `db:"name"`
}

func queryRow() {
sqlStr := "select id,age,name from user where id=?"
var u user
//queryRow 之后 一定要执行scan 否则 持有的数据库连接 不会释放
err := db.Get(&u, sqlStr, 1)
if err != nil {
fmt.Println(err)
}
fmt.Println("result:", u)
}

可以看出来,这查询就比之前的原生的查询要简单很多,使用很方便。
其实这里的原理和java中的json序列化很像,都是利用反射。

只不过在java中 我们一般是利用注解 来标识 field和 json中key的关系
而 这里是用的`` 符号 仅此而已

查询多行也是一样的

1
2
3
4
5
6
7
8
9
10
go复制代码func queryRow() {
sqlStr := "select id,age,name from user where id=?"
var u user
//queryRow 之后 一定要执行scan 否则 持有的数据库连接 不会释放
err := db.Get(&u, sqlStr, 1)
if err != nil {
fmt.Println(err)
}
fmt.Println("result:", u)
}

也是能简化不少 我们的操作

crud 操作

sqlx的crud操作和sql里面的操作是一样的 这里不再重复演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func insert() {
sqlStr := "insert into user(name,age) values(?,?)"
ret, err := db.Exec(sqlStr, "李彦宏", 99)
if err != nil {
fmt.Println(err)
return
}
id, err2 := ret.LastInsertId()
if err2 != nil {
fmt.Println("get lastid error:", err2)
return
}
fmt.Println("insert success,id: ", id)
}

简化操作

在之前每次插入或者更新数据的时候 如果参数过多,那我们的占位符和真正的数据 很有可能就写错了,而且可读性不佳

sqlx提供了NamesExec操作 可以简化我们的操作 以kv的形式 来操作数据

1
2
3
4
5
6
7
8
9
10
11
go复制代码func betterInsert() {
sqlStr := `insert into user(name,age) values(:name,:age) `
_, err := db.NamedExec(sqlStr, map[string]interface{}{
"name": "大强子",
"age": 17,
})
if err != nil {
panic(err)
}
return
}

同样的 查询语句也一样有类似的方法NamedXXX 有兴趣的可以自行查找sqlx 相关的文档。这里只是抛砖引玉一下

大家要谨记,这些api没必要一个个过,只需要记住一个类似的,然后其余的时候只要知道用的时候 去哪里查询文档 即可

事务的操作

sqlx的事物操作 与原生的 事务操作 有所不同,主要区别就是 sqlx 依赖defer 来做回滚和提交的操作

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
go复制代码func transactionDemo() error {
tx, err := db.Begin()
if err != nil {
fmt.Println("begin trans failed")
return err
}

defer func() {
// 如果这个函数有panic 则 回滚以后再panic
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
// 如果有错误 也回滚
fmt.Println("rollback")
tx.Rollback()
} else {
err = tx.Commit()
fmt.Println("commit")
}
}()

sqlstr1 := "update user set age=130 where id=?"
_, err = tx.Exec(sqlstr1, 1)
if err != nil {
return err
}

sqlstr2 := "update user set age=130 where id=?"
_, err = tx.Exec(sqlstr2, 2)
if err != nil {
return err
}

err = tx.Commit()
if err != nil {
return err
}
return nil

}

我个人是比较喜欢这种defer的写法的 因为不容易出错 特别是当你的事务特别复杂的时候

sqlx的批量插入

有时候我们会经常碰到批量插入数据的场景,最简单的做法 当然是 写一个for循环 每次都插入一条

直到循环结束,但是这样会重复连接数据库 造成不必要的压力。

所以通常我们会一次性插入

sql语句 形如:

image.png

当然在程序里面我们要使用占位符来处理,那他的参数如下:

image.png

显然我们要 完成类似的操作 是要做一些 字符串拼接的操作的

这样写起来很麻烦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
go复制代码
func insertUsers(users []user) error {
// 存放 (?, ?) 的slice
valueStrings := make([]string, 0, len(users))
// 存放values的slice
valueArgs := make([]interface{}, 0, len(users)*2)
// 遍历users准备相关数据
for _, u := range users {
// 此处占位符要与插入值的个数对应
valueStrings = append(valueStrings, "(?, ?)")
valueArgs = append(valueArgs, u.Name)
valueArgs = append(valueArgs, u.Age)
}
// strings.Join(valueStrings, ",") ===》(?, ?),(?, ?),(?, ?)
// 自行拼接要执行的具体语句 形如:INSERT INTO user (name, age) VALUES (?, ?),(?, ?),(?, ?)
stmt := fmt.Sprintf("INSERT INTO user (name, age) VALUES %s",
strings.Join(valueStrings, ","))
fmt.Println(valueArgs)
_, err := db.Exec(stmt, valueArgs...)
return err
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
css复制代码users := []user{
{
Name: "kk1",
Age: 11,
},
{
Name: "kk2",
Age: 12,
},
{
Name: "kk1",
Age: 13,
},
}
insertUsers(users)

简化版的写法

1
2
3
4
go复制代码func insertMoreUsers(users []user) error {
_, err := db.NamedExec("INSERT INTO user (name, age) VALUES (:name, :age)", users)
return err
}

in函数简单介绍

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码func searchByIDs(ids []int)(users []user, err error){
//把id 填到sql语句中
query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?)", ids)
if err != nil {
return
}
// sqlx.In 返回带 `?` 的查询语句, 记得要用rebind 重新绑定下
query = db.Rebind(query)

err = db.Select(&users, query, args...)
return
}

本文转载自: 掘金

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

SpringBoot整合 mybatisPlus-入门篇 引

发表于 2021-11-09

引言

最近在准备一期SpringBoot整合大全系列文章,同时也会有视频放出(视频还在酝酿中),如果大家觉得有帮助,记得点赞加收藏哦。 话不多说,咱们直接进入正题。 ​

代码已经上传到码云:gitee.com/lezaiclub/s…,欢迎白嫖

开整

引入需要的依赖

前面创建springboot项目就不说了

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
xml复制代码   <dependencies>
        <!--        springboot相关包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!--        springboot相关包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--        数据源链接池 不是此部分必备包-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--        springboot相关包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--        json序列化相关包 不是本部分关键包-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.70</version>
        </dependency>
        <!--        mybatis相关包 必备包-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>
        <!--        mysql数据库驱动 必备包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
        <!--        lombok 不用写写get和set,不是本部分必备包-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
    </dependencies>

扫描mapper包路径

在springboot启动类上添加注解

1
2
3
4
5
6
7
8
less复制代码@SpringBootApplication
// 扫描指定包路径
@MapperScan("com.aims.mybatisplus.dao")
public class SpringbootMybatisPlusApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMybatisPlusApplication.class, args);
    }
}

创建表语句

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `member` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `create_by` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `member_name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '会员名称',
  `member_type` tinyint(3) DEFAULT NULL COMMENT '会员类型',
  `member_phone` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '手机号',
  `member_level` tinyint(3) DEFAULT NULL COMMENT '会员等级',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='会员表';

创建实体

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
arduino复制代码package com.aims.mybatisplus.model.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * @description member
 * @author AI码师
 * @date 2021-11-07
 */
@Data
// 如果表名和实体名称一样 则不需要使用这个注解
@TableName("member")
public class Member implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(type = IdType.AUTO)
    /**
    * 主键
    */
    private Long id;

    /**
    * 创建人
    */
    private String createBy;

    /**
    * 创建时间
    */
    private Date createTime;

    /**
    * 更新人
    */
    private String updateBy;

    /**
    * 更新时间
    */
    private Date updateTime;

    /**
    * 会员名称
    */
    private String memberName;

    /**
    * 会员类型
    */
    private int memberType;

    /**
    * 手机号
    */
    private String memberPhone;

    /**
    * 会员等级
    */
    private int memberLevel;

    public Member() {}
}

创建操作数据库的mapper层

你会发现mapper里面啥都不用写,开心的起飞,只要继承BaseMapper就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.aims.mybatisplus.dao;

import com.aims.mybatisplus.model.entity.Member;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author AI码师
 * @description memberMapper
 * @date 2021-11-07
 */
@Mapper
public interface MemberMapper extends BaseMapper<Member> {
// 用上

}

配置数据库链接

1
2
3
4
5
6
7
8
9
10
11
c复制代码spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springboot-integration?useUnicode=true&characterEncoding=utf-8
    username: aims
    password: aims
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
// 下面这个是开启sql打印的,不是必须要配置的
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

到此 集成mybatisPlus已经完成了,我们现在写几个简单的接口来演示下mapper相关的api ​

添加相关接口

查询接口

通过lambada表达式轻松可以实现相关sql操作

1
2
3
4
5
6
7
8
less复制代码   @GetMapping
    public String get(@RequestParam("name") String name) {
        
        LambdaQueryWrapper<Member> lambda = new QueryWrapper<Member>().lambda();
        lambda.eq(Member::getMemberName, name);
        Member member = memberMapper.selectOne(lambda);
        return Objects.isNull(member) ? "没找到信息" : JSON.toJSONString(member);
    }

新增接口

添加操作更是简单到无语,后续会出一篇文章讲解这方面的常规操作语法,敬请期待!

1
2
3
4
5
less复制代码    @PostMapping
    public int post(@RequestBody Member member) {
        int effectNum = memberMapper.insert(member);
        return effectNum;
    }

本文转载自: 掘金

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

Java中的CoIIection和CoIIections有什

发表于 2021-11-09

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

写在前面

我们在Java开发的过程中总会使用到集合,比如Map、List、ArrayList、HashMap、LinkedList等等等,很多个集合类。

而有的时候我们也会使用到两个类,那就是Collection和Collections类,虽然只是一个字母之差,功能可是完全不一样,这也是我们今天要学习的知识。

CoIIection类和CoIIections类有什么区别?

相同点:

Collection和CoIIections类同属于java.util包下的文件。

好像除了这个共同点,剩下的就是名字有点像了。

不同点:

Collection是所有集合类,比如List、Map等的顶级接口,很多集合抽象类都是继承这个接口;

并且这个接口中制定了一系列的规范,比如add、remove、set、equals等集合中必备功能方法,所以这些在所有的集合中都存在,也都能调用成功。

这个接口在日常开发中使用到的情况并不多,更多的是在类图中的顶部一直被人们看到。

Collections类是集合类中的工具类,这个类我们在日常开发中经常会使用到,其中一些常用方法也给我们日常开发中提供了很大的便利。

比如该类中的sort排序方法,我们在开发中就经常用到。

再比如类中的reverse反转方法,该方法可能用到的比较少一点,其功能是可以根据元素的自然顺序进行降序排序。

还有fill方法,可以将指定元素替换指定到集合或者列表中的所有元素。

Collections类还有很多的工具方法,大家也可以去Collections源码中查看一番,相信会收获更多的知识。

总结

CoIIection类和CoIIections类有什么区别?如果面试官问起这个问题,其实也并不是这两个方法就一定有着千丝万缕的关系,只不过长得像了点,才会引起大家的发问,淡定回答就好。

本文转载自: 掘金

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

ldif 数据转成正确的组织结构再探

发表于 2021-11-09

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

上次文章最后有说到按照之前的思路来转化组织结构是有坑的,我们现在还只是对接 AD域,ldap 协议的其他产品在细节上还会有些许不同

我们是不能直接粗暴的认为 cn 就是对应标识一个用户, cn 是 common name 的意思,他也可以表示我们理解的用户组

orgnizationalUnit 和 container 也可以是组的意思,但是对于 AD 域来说,在他们上面能够配置的属性有差别

那么对于同步组织结构,我们实际上是可以如何做的呢?不能粗暴的按照之前的方式来实现,我们可以如何实现?

先过滤组织

我们可以在 ldap 服务器中可以看到, cn 也可以是组的意思,cn 下面也可以是 ou

因此单单的通过 dn 是无法分辨出哪个 dn 代表的是组,哪个 dn 代表的是用户的

因此在 ldap 中,我们想要获取我们认为的组织结构,那么就需要有一定的方法

  • 先过滤组
  • 再过滤用户
  • 拼装整个组织结构

过滤组织

我们就可以使用例如这样的过滤条件:(|(objectClass=organizationalUnit)(objectClass=organizationalRole))

筛选出来的 dn 全部都是我们认为的组,根据之前的逻辑将这个组生成一棵树即可,是一棵多叉树

例如效果可能是这样的,先生成一棵树,树的基本雏形就有了

再过滤用户

过滤用户的时候 可以是这样的

(|(objectClass=Person)(objectClass=user))

当然,这些过滤用户都是可以自己修改的,只是我们的逻辑是,先过滤组,再过滤用户

我们过滤的用户可能有这些

我们这里要注意,这些用户不是所有都要挂到我们的树上面的,需要检验他们是不是我们期望的组

拼装整个组织结构

拼装之后,结果可能是这样的,新增的节点是对应的用户,蓝色的是我们正确加入组织结构里面的用户

橙色的也是我们通过上述 条件 (|(objectClass=Person)(objectClass=user)) 筛选出来的用户,但是不属于我们之前筛选的组里面的成员

因此,不能把 J 和 K 加入到我们的组织结构中

则最终我们的组织结构是这样的才对

对于编码的实现原理,和上一篇的类似,只是在生成树的时候需要调整一下即可,处理 DN 的时候,处理组的 DN 和 处理 用户的 DN 需要分开过滤,分开处理,最后做拼装

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

从Java8到Java17(三)

发表于 2021-11-09

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

上一篇文章讲了Java9 API层面的一些升级迭代,这篇文章将专门来讲讲模块化。模块化每次提及都是Java的重大更新,但其实真实用户寥寥无几。随着容器的普及,减少的那一点jar包的体积更是可以忽略不计。先来介绍一下什么是模块化。

模块化简介

大家都知道Java的package,需要的时候引入(import)一下对应的类,module在package的上面一层,一个模块都会有一个module-info.java的描述文件,来描述当前模块需要引入哪些package和对哪些package可见。

1
2
3
4
5
java复制代码module com.foo.bar {
requires org.baz.qux;
exports com.foo.bar.alpha;
exports com.foo.bar.beta;
}

这么做的好处,首先对于jdk本身来说我不需要的package我就不要了引入了,不像现在会默认塞一个rt.jar进来,这个东西有60M多,但大部分的内容都没用到。然后没有在module-info.java中声明的exports,即使是public的类在外部也无法被使用,这就进一步增强了安全性。

jdk身先士卒首先将rt.java拆成了很多个小的jmod文件,但好像也仅限于此,热门框架跟进的不积极。基于现有的Spring boot+maven的管理模式,大到依赖管理小到bean的管理都井井有条。如果各大框架进一步拆解自己的变成一个个模块,那可能确实打出来的jar会小一些,但可惜多数是没有响应的。

模块和maven不是一个东西

这个依赖管理让很多人一眼就想到了maven,但两者解决的不是一个问题。maven只能管到jar包的依赖,而module会深入到类中去做更精细的依赖管理和权限控制。而maven本身还有更重要的活去干:编译、测试、打包、发布等。

鉴于确实没多少人用,我也很难下结论说他好还是不好,只是现有的项目想迁移成本是极高的,新的项目不用module也可以实现大部分的功能,只是jar会比较大一些,不过在容器本身的体积面前确实也不算什么。

本文转载自: 掘金

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

Garbage First 收集器

发表于 2021-11-09

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

简述

Garbage First(简称G1)收集器是垃圾收集器技术发展史上的里程碑,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1垃圾收集器主要面向服务端应用。G1垃圾收集器本来是被期望在短期内替换掉CMS垃圾收集器,不过由于CMS的代码和HotSpot的内存管理、执行、编译、监控等子系统都有千丝万缕的联系。所以在规划JDK10功能目标的时候HotSpot提出了统一垃圾收集器接口,将内存回收的行为和实现分离,垃圾收集器都重构成基于这套接口的一种实现。以此为基础日后要移除或者加入某一款收集器就会容易很多。

面向局部收集

G1垃圾收集器的设计者主要是为了实现一个停顿时间模型(指定M毫秒时间内,消耗在垃圾收集上的时间大概率不超过N毫秒)这样的目标。

具体的实现的话,G1收集器跳出了新生代和老年代的牢笼,而是面向堆内存的任何部分来组成回收集进行回收,衡量标准是哪块内存中存放的垃圾多,回收哪块内存的收益最大,这也称为G1收集器的Mixed GC模式。

基于Region的内存布局

G1收集器虽然也是根据分代收集理论设计的,但是其堆内存的布局与其他收集器有较大的差异。其把连续的Java堆划分成大小相等的独立区域,每个Region都可以当做新生代的Eden空间、Survivor空间或者老年代区域,收集器对扮演不同角色的区域采用不同的策略进行处理。

此外Region中还有一类Humongous区域,专门用来存储大对象,如果对象超过了一个Region则会用n个连续的Humongous Region来进行存储。

因为G1中将Region作为单次回收的最小单位,所以G1可以建立可预测的停顿时间模型。每次回收收集的都是单个Region的整数倍。

运行过程

首先先介绍下TAMS指针,该指针是在并发回收阶段,新分配的地址必须要在这两个指针位置上,G1收集器默认在这个地址以上的对象是被隐式标记过的,也就是默认他们是存活的,不纳入回收范围。

G1收集器的运行过程大致分为四个步骤:

  • 初始标记:仅仅是标记下GC Root直接关联的对象,并修改TAMS指针的值。这个阶段需要暂停线程,不过耗时很短。
  • 并发标记:从GC Root开始对堆中的对象进行可达性分析,递归扫描整个堆的对象图,找出要回收的对象,该阶段耗时很长,不过可以与用户线程并发执行。扫描完成后还要重新处理原始快照下有引用变化的对象。
  • 最终标记:对用户线程进行一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的原始快照记录。
  • 筛选回收:负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,并根据用户期望时间来制定回收计划,可以自由选择任何多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这个过程由多条收集器线程并行完成并需要暂停用户线程,因为有对象的移动。

本文转载自: 掘金

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

一次关于Spring Batch的实践

发表于 2021-11-09

背景

随着机构码用码量越来越多目前为止有4百万,统计异常接口响应时间会变慢,而且统计异常只统计48小时内,count效率实在太低。

最后考虑使用采集的方式,将异常数据采集到另一张异常表,这样子的话count效率会高些。不仅要采集异常表,还要根据月份汇总归档表也就是分表的概念,这样也能减轻单表的压力。

Spring Batch

目前,Spring Batch是批处理框架为数不多的优秀框架,是一个轻量级、完善的批处理框架。提供了大量可重用的组件,包括了日志、追踪、事务、任务统计、任务重启、跳过、重复、资源管理。

Spring Batch是一个批处理应用框架,不是调度框架,需要调度框架来构建完成批处理任务。项目中使用定时器来调度。

业务场景

  • 周期性的提交批处理
  • 把一个任务并行处理
  • 处理时跳过部分记录

Spring batch 4个主要组件

  • JobLauncher :任务启动器,程序的入口
  • Job:一个具体的任务
  • Step:一个具体的执行步骤,一个Job有多个Step
  • JobRepository:存储数据的仓库,在任务执行的时候,需要它来记录任务状态信息,可以看做是一个数据库的接口

Spring Batch基本层级结构

  • 运行的基本单位是一个Job,一个Job就做一件批处理的事情
  • 一个Job包含多个Step,Step就是每个Job要执行的单个步骤
  • Step会有Tasklet,它是一个任务单元,属于可以重复利用的对象(分表、创建新表)
  • Step会有Chunk,需要定一个多大的数据量是一个chunk,chunk里就是不断循环的一个流程(读数据,处理数据,写数据)

结合项目来说明如何使用SpringBatch

  • 读取数据库的定时任务表,通过反射,执行相应定时任务
  • 进入SringBatch部分
  • JobLauncher.run(XXXJob1,JobParameters) 开始启动任务了
  • XXX1Job任务有3个Step(1.建立表+月份 2.读取原表写入到 表+月份 3.删除原表已归档数据)
  • XXX2Job任务有3个Step(1.获取采集48小时时间 2.读取原表异常数据 写入异常表)

Tasklet 与 chunk 总结

tasklet更适合一个步骤到另一个步骤场景,chunk实现处理分页读,或我们不想再内存中保留大量数据场景,项目中用于创建分表和获取时间放入上下文的操作用tasklet,像比如读取表1000条写入表这种利用chunk

相关博客

www.infoq.cn/article/ana…

本文转载自: 掘金

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

C++ 并发与多线程笔记(3) 线程传参详解,detach(

发表于 2021-11-09

一、传递临时对象作为线程参数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cpp复制代码#include <iostream>
#include <thread>

void myprint(const int &i, char *pmybuf) {
std::cout << "i = " << i << std::endl;
std::cout << "pmybuf = " << pmybuf << std::endl;
return;
}

int main()
{
int my = 1;
int &may = my;
char buf[] = "I love china";

std::thread obj(myprint, my, buf);
obj.join();
std::cout << "main thread finished!!" << std::endl;
return 0;
}

1. 要避免的陷阱

  • 如果线程从主线程detach了,i不是my真正的引用,实际上值传递(复制了一份),即使主线程运行完毕了,子线程用i仍然是安全的,但仍不建议传递引用,推荐改为const int i。
  • 如果是直接使用函数的话,这里i的地址和传入参数的地址一致。*
  • pmybuf 还是指向原来的字符串,所以这么写不安全,在detach时绝对会有问题。
  • 例:
    参数 my
1
2
3
4
5
6
bash复制代码0x00eff940 {1}
0x00eff940 {1}
直接传入函数
0x00eff940 {1}
使用线程
0x01010a9c {1}

参数buf

1
2
3
4
5
6
7
8
9
bash复制代码0x00b7f750 "I love china"
函数传入
0x00b7f750 "I love china"
&pmybuf
0x00b7f65c {0x00b7f750 "I love china"}
线程
0x00b7f750 "I love china"
&pmybuf
0x00cef99c {0x00b7f750 "I love china"}

2. 要避免的陷阱2

  • buf到底在什么时候转换成string,事实上存在,mybuf都被回收了(main函数都执行完了),系统才用mybuf去转string的可能性。
  • 推荐先创建一个临时对象thread myThread(myPrint, mvar, string(mybuf));就绝对安全了。。。。
    使用该方法(创建临时对象的方法)调用了拷贝构造函数,生成了一个临时对象。
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
cpp复制代码#include <iostream>
#include <thread>
#include <string>
using namespace std;

void myPrint(const int i, const string& pmybuf)
{
cout << i << endl;
cout << pmybuf << endl;
}

int main()
{
int mvar = 1;
int& mvary = mvar;
char mybuf[] = "this is a test";
//如果detach了,这样仍然是不安全的
//因为存在主线程运行完了,mybuf被回收了,系统采用mybuf隐式类型转换成string
//推荐先创建一个临时对象thread myThread(myPrint, mvar, string(mybuf));就绝对安全了。。。。
thread myThread(myPrint, mvar, mybuf);
myThread.join();
//myThread.detach();

cout << "Hello World!" << endl;
}

事实1: 只要用临时构造的A类对象作为参数传递给线程。那么就一定能够在主线程执行完毕前把线程函数的第二个参数构建出来,从而确保即便detach子线程也安全运行。

3. 总结

  • 若传递int这种简单类型参数,建议都是值传递,不要用引用;
  • 如果传递类对象,避免隐式类型转换,全部都在创建线程这一行就构建出临时对象,然后在函数参数里,用引用来接,否则还会创建出一个对象;
  • 建议不使用detach(),只使用join():这样就不存在局部变量失效导致线程对内存的非法引用问题;

二、临时对象作为线程参数继续讲

1. 线程id概念

  • id是个数字,每个线程(不管是主线程还是子线程)实际上都对应着一个数字,而且每个线程对应的这个数字都不一样
  • 线程id可以用C++标准库里的函数来获取。std::this_thread::get_id()来获取

2. 临时对象构造时机捕获
如果使用隐式类型转换:

1
cpp复制代码std::thread myobj(myprint,mvar);

比较致命的问题: 在子线程中构造A类的对象

如果使用临时对象的方法

1
cpp复制代码std::thread myobj(myprint,A(mvar));

用了临时对象后,所有的A类对象都在main()函数中就已经构建完毕了

三、传递类对象智能指针作为线程参数

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
cpp复制代码#include <iostream>
#include <thread>
using namespace std;

class A {
public:
mutable int m_i; //m_i即使实在const中也可以被修改
A(int i) :m_i(i) {}
};

void myPrint(const A& pmybuf)
{
pmybuf.m_i = 199;
cout << "子线程myPrint的参数地址是" << &pmybuf << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
A myObj(10);
//myPrint(const A& pmybuf)中引用不能去掉,如果去掉会多创建一个对象
//const也不能去掉,去掉会出错
//即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,
//所以在子线程中修改m_i的值不会影响到主线程
//如果希望子线程中修改m_i的值影响到主线程,可以用thread myThread(myPrint, std::ref(myObj));
//这样const就是真的引用了,myPrint定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
thread myThread(myPrint, myObj);
myThread.join();
//myThread.detach();

cout << "Hello World!" << endl;
}
  • myPrint(const A& pmybuf)中引用不能去掉,如果去掉会多创建一个对象
  • const也不能去掉,去掉会出错
  • 即使是传递的const引用,但在子线程中还是会调用拷贝构造函数构造一个新的对象,所以在子线程中修改m_i的值不会影响到主线程
  • 如果希望子线程中修改m_i的值影响到主线程,可以用thread myThread(myPrint, std::ref(myObj));
  • 这样const就是真的引用了,myPrint定义中的const就可以去掉了,类A定义中的mutable也可以去掉了
    std::ref()可以将真引用传入函数

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpp复制代码#include <iostream>
#include <thread>
#include <memory>
using namespace std;

void myPrint(unique_ptr<int> ptn)
{
cout << "thread = " << std::this_thread::get_id() << endl;
}

int main()
{
unique_ptr<int> up(new int(10));
//独占式指针只能通过std::move()才可以传递给另一个指针
//传递后up就指向空,新的ptn指向原来的内存
//所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了
thread myThread(myPrint, std::move(up));//由于这里使用move所以当运行到这里后 之前的up里值不存在,子线程函数执行完之后才会被释放
myThread.join();
//myThread.detach();

return 0;
}
  • 独占式指针只能通过std::move()才可以传递给另一个指针
  • 传递后up就指向空,新的ptn指向原来的内存
  • 所以这时就不能用detach了,因为如果主线程先执行完,ptn指向的对象就被释放了

四、用成员函数指针做线程函数

本文转载自: 掘金

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

SpringMVC 异常拦截和处理流程

发表于 2021-11-09

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

这一篇只关注一个小点 , 学习一下 SpringMVC 是如何进行异常拦截和处理的

二 . 使用

以最基础的 Exception 拦截的使用为例 , 常见的使用方式为 :

1
2
3
4
java复制代码@ExceptionHandler(Exception.class)
public void deaCommonException(Exception exception, HttpServletResponse response) {
logger.info("------> 处理通用异常 <-------");
}

当在请求中触发了异常之后 , 就会被该通用拦截器拦截到 , 最终给前端抛出 500 异常 .

那么整个流程里面 , 代码层面是怎么处理的呢 ?

三 . 源码梳理

3.1 拦截的入口

方法是在 DispatcherServlet # doDispatch 中进行核心的处理 , 当方法中出现异常了 , 自然也能在其中被 catch 处理掉 , 其主要流程为 :

  • Step 1 : 调用方法执行
  • Step 2 : 抛出异常后被 catch 捕获 , 记录该异常 , 并不往更外层抛出
  • Step 3 : processDispatchResult 中如果发现存在异常 , 则进行异常的处理

doDispatch

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
java复制代码protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {


try {
ModelAndView mv = null;
// 准备异常接收对象
Exception dispatchException = null;

try {
// 省略主流程处理 , 主要是 handler 的调用
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

}

// 此处接收 exception , 映射给接收对象
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 核心逻辑 , 对 Exception 进行处理
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

processDispatchResult

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
java复制代码private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;

if (exception != null) {
// 视图处理类 ,特定条件的异常会转发给特定的处理视图 , 可以在处理程序处理期间的任何时间抛出
if (exception instanceof ModelAndViewDefiningException) {
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
// Step 1 : 获取当前请求的 handler 类 , 业务处理类
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// Step 2 : 核心 , 处理主流程处理
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}


if (mv != null && !mv.wasCleared()) {
// 渲染给定的ModelAndView -> resolveViewName
render(mv, request, response);
if (errorView) {

// request.removeAttribute("javax.servlet.error.status_code");
// request.removeAttribute("javax.servlet.error.exception_type");
// request.removeAttribute("javax.servlet.error.message");
// request.removeAttribute("javax.servlet.error.exception");
// request.removeAttribute("javax.servlet.error.request_uri");
// request.removeAttribute("javax.servlet.error.servlet_name");
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
return;
}

if (mappedHandler != null) {
mappedHandler.triggerAfterCompletion(request, response, null);
}
}

3.2 异常的处理主流程

当异常被捕获到 , 并且通过 processHandlerException 发起异常处理流程后 , 会通过如下流程开始依次处理异常 :

  • DispatcherServlet # processHandlerException : 处理起点
  • HandlerExceptionResolverComposite # resolveException
  • AbstractHandlerExceptionResolver # resolveException
  • AbstractHandlerMethodExceptionResolver # doResolveException
  • ExceptionHandlerExceptionResolver # doResolveHandlerMethodException
  • ServletInvocableHandlerMethod # invokeAndHandle : 调用具体方法
  • 调用最后的 @ExceptionHandler 处理方法处理异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {

// 成功和错误响应可能使用不同的内容类型
// HandlerMapping.class.getName() + ".producibleMediaTypes";
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
// -
// - DefaultErrorAttributes
// - HandlerExceptionResolverComposite
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}

throw ex;
}

AnnotationConfigServletWebServerApplicationContext_parent.png

循环 Exception resolve 列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

if (this.resolvers != null) {
// 遍历 resolvers 处理类 , 常见的有以下几种 , 添加默认异常解析器
// - ExceptionHandlerExceptionResolver
// - ResponseStatusExceptionResolver
// - DefaultHandlerExceptionResolver
for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (mav != null) {
return mav;
}
}
}
return null;
}

注意 , 这里调用的 resolveException 均为父类 AbstractHandlerExceptionResolver , 由父类再调用子类 doResolveException .

调用 Method 处理方法

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复制代码protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {

// 通过业务方法和异常类型从集合中获取去
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
}

// setHandlerMethodArgumentResolvers + setHandlerMethodReturnValueHandlers

ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();

// 此处会调用具体的代理方法 , 会根据是否存在 Throwable cause 分别调用2个不同的方法
exceptionHandlerMethod.invokeAndHandle

//如果完全处理了请求 ,这直接返回 ModelAndView
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
}
else {
// 省略返回处理的 ModelAndView 或者 RedirectAttributes 对象
}
}

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
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
java复制代码protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {

Class<?> handlerType = null;

// 如果 HandlerMethod 存在 , 则先尝试处理
if (handlerMethod != null) {
// 控制器类本身的局部异常处理程序方法 , 此处是具体的业务方法
handlerType = handlerMethod.getBeanType();
// 从缓存中获取当前异常类对应的 ExceptionResolver , 不存在则 new ExceptionHandlerMethodResolver , 并且缓存
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);

Method method = resolver.resolveMethod(exception);

// 如果控制器本身存在存在局部异常处理方法 , 则直接返回
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
}

// 代理类需要获取实际类
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}

// 此处遍历所有的异常类
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();

if (advice.isApplicableToBeanType(handlerType)) {
// Exception 处理类 , 包含核心的类信息
ExceptionHandlerMethodResolver resolver = entry.getValue();
//
Method method = resolver.resolveMethod(exception);

if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
}
}
}

return null;
}

// 从缓存中尝试获取Method
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
Method method = this.exceptionLookupCache.get(exceptionType);
if (method == null) {
method = getMappedMethod(exceptionType);
// 不存在则获取后添加到缓存中
this.exceptionLookupCache.put(exceptionType, method);
}
return method;
}

private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();

// 遍历所有的 Method Mapper
// PS : 这里是第二个循环 ,第一个循环遍历 Class , 第二个循环遍历 Method
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
if (!matches.isEmpty()) {
matches.sort(new ExceptionDepthComparator(exceptionType));
// 返回匹配的第一个方法 ?
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}

四 . 补充 : @ExceptionHandler 的扫描和加载

这一部分来看一下 , @ExceptionHandler 是如何被扫描到容器中的.

1
2
3
4
5
6
7
java复制代码// 在 ExceptionHandlerExceptionResolver 中存在2个Map 用于存放对应的关联关系

// 用于保存 Class 对应的 Resolver , 对应业务Controller 对应 Resolver
Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =new ConcurrentHashMap<>(64);

// 对应 Error 处理类对应 resolver
Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =new LinkedHashMap<>();

4.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
java复制代码// C- ExceptionHandlerExceptionResolver
public void afterPropertiesSet() {
// 初始化 Advice , 处理 ResponseBodyAdvice
initExceptionHandlerAdviceCache();

if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}

private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}

// 所有的 Error 处理类
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
// 通过 BeanType 构建 Resolver
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);

// 将 Resolver 存入缓存
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
}

总结

时间有限 , 整体来说还是过一下流程 ,方便后续的问题排查

image.png

本文转载自: 掘金

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

1…384385386…956

开发者博客

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