2024年2月

开心一刻

今天我突然顿悟了,然后跟我妈聊天

我:妈,我发现一个饿不死的办法

妈:什么办法

我:我先养个狗,再养个鸡

妈:然后了

我:我拉的狗吃,狗拉的鸡吃,鸡下的蛋我吃,如此反复,我们三都饿不死

妈:你整那么多中间商干啥,你就自己拉的自己吃得了,还省事

我又顿悟了,回到:也是啊

说句很重要的心里话:祝大家在2024年,身体健康,万事如意!

场景重温

为了让大家更好的明白问题,先做下相关准备工作

环境准备

数据库:
MySQL
8.0
.
30

,表:
tbl_order


DROP TABLE IF EXISTS`tbl_order`;CREATE TABLE`tbl_order`  (
`id`
bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`order_no`
varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '业务名',
`created_at`
datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`updated_at`
datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最终修改时间',PRIMARY KEY(`id`) USING BTREE
) ENGINE
= InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '订单' ROW_FORMAT =Dynamic;--------------------------------Records of tbl_order------------------------------ INSERT INTO `tbl_order` VALUES (1, '123456', '2023-04-20 07:37:34.000', '2023-04-20 07:37:34.720');INSERT INTO `tbl_order` VALUES (2, '654321', '2023-04-20 07:37:34.020', '2023-04-20 07:37:34.727');

View Code

基于
JDK1.8

druid 1.1.12

mysql-connector-java 8.0.21

Spring 5.2.3.RELEASE

完整代码:
druid-timeout

毫秒位数捉摸不透

直接运行
com.qsl.DruidTimeoutTest#main
,会看到如下结果

数据库表中的值:
2023-04-20 07:37:34.000
运行出来后是
2023-04-20 07:37:34.0

2023-04-20 07:37:34.720
对应
2023-04-20 07:37:34.72

2023-04-20 07:37:34.020
对应
2023-04-20 07:37:34.02

2023-04-20 07:37:34.727
对应
2023-04-20 07:37:34.727

毫秒位数时而1位,时而2位,时而3位,搞的我好乱呐

原因分析

大家注意看这个代码

获取列值,
sqlRowSet.getObject(i)
返回的类型是
Object
,我们调整下输出:
System.out.println(obj.getClass().getName() + " " + obj);

此时输出结果如下

可以看到,
java
程序中,此时的时间类型是
java.sql.Timestamp

有了这个依托点,原因就很好分析了

Timestamp的toString

我们知道,
java
中直接输出对象,会调用对象的
toString
方法,如果自身没有重写
toString
则会沿用
Object

toString
方法

我们先来看一下
Object

toString
方法

粗略看一下,返回值明显不是
2023-04-20 07:37:34.0
这种时间字符串格式

那说明什么?

说明
Timestamp
肯定重写了
toString
方法嘛

java.sql.Timestamp#toString
内容如下


/*** Formats a timestamp in JDBC timestamp escape format.
* <code>yyyy-mm-dd hh:mm:ss.fffffffff</code>,
* where <code>ffffffffff</code> indicates nanoseconds.
* <P>
*
@returna <code>String</code> object in
* <code>yyyy-mm-dd hh:mm:ss.fffffffff</code> format
*/@SuppressWarnings("deprecation")publicString toString () {int year = super.getYear() + 1900;int month = super.getMonth() + 1;int day = super.getDate();int hour = super.getHours();int minute = super.getMinutes();int second = super.getSeconds();
String yearString;
String monthString;
String dayString;
String hourString;
String minuteString;
String secondString;
String nanosString;
String zeros
= "000000000";
String yearZeros
= "0000";
StringBuffer timestampBuf;
if (year < 1000) {//Add leading zeros yearString = "" +year;
yearString
= yearZeros.substring(0, (4-yearString.length())) +yearString;
}
else{
yearString
= "" +year;
}
if (month < 10) {
monthString
= "0" +month;
}
else{
monthString
=Integer.toString(month);
}
if (day < 10) {
dayString
= "0" +day;
}
else{
dayString
=Integer.toString(day);
}
if (hour < 10) {
hourString
= "0" +hour;
}
else{
hourString
=Integer.toString(hour);
}
if (minute < 10) {
minuteString
= "0" +minute;
}
else{
minuteString
=Integer.toString(minute);
}
if (second < 10) {
secondString
= "0" +second;
}
else{
secondString
=Integer.toString(second);
}
if (nanos == 0) {
nanosString
= "0";
}
else{
nanosString
=Integer.toString(nanos);//Add leading zeros nanosString = zeros.substring(0, (9-nanosString.length())) +nanosString;//Truncate trailing zeros char[] nanosChar = new char[nanosString.length()];
nanosString.getChars(
0, nanosString.length(), nanosChar, 0);int truncIndex = 8;while (nanosChar[truncIndex] == '0') {
truncIndex
--;
}

nanosString
= new String(nanosChar, 0, truncIndex + 1);
}
//do a string buffer here instead. timestampBuf = new StringBuffer(20+nanosString.length());
timestampBuf.append(yearString);
timestampBuf.append(
"-");
timestampBuf.append(monthString);
timestampBuf.append(
"-");
timestampBuf.append(dayString);
timestampBuf.append(
" ");
timestampBuf.append(hourString);
timestampBuf.append(
":");
timestampBuf.append(minuteString);
timestampBuf.append(
":");
timestampBuf.append(secondString);
timestampBuf.append(
".");
timestampBuf.append(nanosString);
return(timestampBuf.toString());
}

View Code

注意看注释:
yyyy-mm-dd hh:mm:ss.fffffffff
,说明精度是到纳秒级别,不只是到毫秒哦!

该方法很长,我们只需要关注
fffffffff
的处理,也就是如下代码

nanos
类型是
int

private
int
nanos;

,用来存储秒后面的那部分值

数据库表中的值:
2023-04-20 07:37:34.000
对应的
nanos
的值是 0,
2023-04-20 07:37:34.720
对应的
nanos
的值是多少了?

不是、不是、不是
720
,因为它的格式是
fffffffff
,所以应该是
720000000


2023-04-20 07:37:34.020
对应的
nanos
的值又是多少?

不是、不是、不是
200000000
,而是
20000000
,因为
nanos

int
类型,不能以0开头

再回到上述代码,当
nanos
等于 0 时,
nanosString
即为字符串0,所以
2023-04-20 07:37:34.000
对应
2023-04-20 07:37:34.0


nanos
不等于 0 时

1、先将
nanos
转换成字符串
nanosString

nanosString
的位数与
nanos
一致

2、
nanosString
前补0,
nanos
的位数与 9 差多少就前补多少个0

例如
2023-04-20 07:37:34.020
对应的
nanos

20000000
,只有8位,前补1个0,则
nanosString
的值是
020000000

3、去掉末尾的0

020000000
去掉末尾的0,得到
02

原因是不是找到了?

总结下就是:
java.sql.Timestamp#toString
会格式化掉
nanosString
末尾的0!(注意:
nanos
的值是没有变的)

是不是很精辟

但是问题又来了:为什么要格式化末尾的0?

说实话,我没有找到一个确切的、准确的说明

只是自己给自己编造了一个勉强的理由:简洁化,提高可读性

去掉
nanosString
末尾的 0,并没有影响时间值的准确性,但是可以简化整个字符串,末尾跟着一串0,可读性会降低

如果非要保留末尾的0,可以自定义格式化方法,想保留几个0就保留几个0

类型对应

MySQL
类型和
JAVA
类型是如何对应的,是不是很想知道这个问题?

那就安排起来,如何寻找了?

别慌,我有葵花宝典:
杂谈篇之我是怎么读源码的,授人以渔

为了节约时间,我就不带你们一步一步
debug
了,直接带你们来到关键点
com.mysql.cj.protocol.a.ColumnDefinitionReader#read

里面有如下关键代码

为了方便你们跟源码,我把此刻的堆栈信息贴一下

我们继续跟进
unpackField
,会发现里面有这样一行代码

恭喜你,只差临门一脚了

按住
ctrl
键,鼠标左击
MysqlType
,欢迎来到
类型对应
世界:
com.mysql.cj.MysqlType

其构造方法

我们暂时只需要关注:
mysqlTypeName

jdbcType

javaClass

接下来我们找到
MySQL

DATETIME

此处的
Timestamp.
class

就是
java.sql.Timestamp

其他的对应关系,大家也可以看看,比如

额外拓展

TIMESTAMP范围

回答这个问题的时候,一定要说明前提条件

MySQL8
,范围是
'1970-01-01 00:00:01' UTC to '2038-01-19 03:14:07' UTC

JDK8

Timestamp
构造方法

入参是
long
类型,其最大值是
9223372036854775807
,1 年是
365*24*60*60*1000=31536000000
毫秒

也就是
long
最大可以记录
6269161692
年,所以范围是
1970 ~ (1970 + 6269161692)
,不会有
2038年问题

MySQL

TIMESTAMP

JAVA

Timestamp
是对应关系,并不是对等关系,大家别搞混了

关于不允许使用java.sql.Timestamp

阿里巴巴的开发手册中明确指出不能用:
java.sql.Timestamp

为什么
mysql-connector-java
还要用它?

可以从以下几点来分析

1、
java.sql.Timestamp
存在有存在的道理,它有它的优势

1.1 精度到了纳秒级别

1.2 被设计为与
SQL TIMESTAMP
类型兼容,这意味着在数据库交互中,使用
Timestamp
可以减少数据类型转换的问题,提高数据的一致性和准确性

1.3 时间方面的计算非常方便

2、在某些特定情况下才会触发
Timestamp

bug
,我们不能以此就完全否定
Timestamp

况且
JDK9
也修复了

3、
MySQL

TIMESTAMP
如果不对应
java.sql.Timestamp
,那该对应
JAVA
的哪个类型?

MySQL的DATETIME为什么也对应java.sql.Timestamp

MySQL

TIMESTAMP
对应
java.sql.Timestamp
,对此我相信大家都没有疑问

为何
MySQL

DATETIME
也对应
java.sql.Timestamp
?

我反问一句,不对应
java.sql.Timestamp
对应哪个?

LocalDateTime
?试问
JDK8
之前有
LocalDateTime
吗?

不过
mysql-connector-java
还是做了调整,我们来看下

我把
mysql-connector-java
的源码
clone
下来了,更方便我们查看提交记录

找到
com.mysql.cj.MysqlType#DATETIME
,在其前面空白处右击

鼠标左击
Annotate with Git Blame
,会看到每一行的最新修改提交记录

我们继续左击
DATETIME
的最新修改提交记录

可以看到详细的提交信息

双击
MysqlType.java
,可以看到修改内容

可以看到
MySQL

DATETIME
对应的
JAVA
类型从
java.sql.Timestamp
调整成了
java.time.LocalDateTime


mysql-connector-java
哪个版本开始生效的了?

它是开源的,那就直接在
github
上找
mysql-connector-java

issue

Bug#102321

但是你会发现搜不到

这是因为
mysql-connector-java
调整成了
mysql-connector-j
,相关
issue
没有整合

那么我们就换个方式搜,就像这样

回车,结果如下

也没有搜到!!!

但你去点一下左侧的
Commits
,你会发现有结果!!!

Commits
不是 0 吗,怎么有结果,谁来都懵呀

这绝对是
github

Bug
呀(这个我回头找下官方确认下,不深究!)

我们点击
Commits
的这个搜索结果,会来到如下界面

答案已经揭晓


8.0.24
开始,
MySQL

DATETIME
对应的
JAVA
类型从
java.sql.Timestamp
调整成
java.time.LocalDateTime

总结

java.sql.Timestamp

1、设计初衷就是为了对应
SQL TIMESTAMP
,所以不管是
MySQL
还是其他数据库,其
TIMESTAMP
对应的
JAVA
类型都是
java.sql.Timestamp

2、
MySQL

TIMESTAMP

2038年
问题,是因为它的底层存储是 4 个字节,并且最高位是符号位,至于其他类型的数据库是否有该问题,得看具体实现

3、在清楚使用情况的前提下(不触发
JDK8 BUG
)是可以使用的,有些场景使用
java.sql.Timestamp
确实更方便

DATETIME对应类型

SQL DATETIME
对应的
JAVA
类型,没有统一标准,需要看具体数据库的
jdbc
版本

比如
mysql-connector-java

8.0.24
之前,
DATETIME
对应的
JAVA
类型是
java.sql.Timestamp
,而
8.0.24
及之后,对应的是
java.time.LocalDateTime

至于其他数据库的
jdbc
是如何对应的,就交给你们了,可以从最新版本着手去分析

【SpringBootStarter】

目的

  1. 了解
    SpringBoot Starter
    相关概念以及开发流程
  2. 实现自定义
    SpringBoot Starter
    (全局加解密)
  3. 了解测试流程
  4. 优化

最终引用的效果:

<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>globalValidation-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

了解SpringBoot Starter相关概念以及开发流程

SpringBoot Starter

SpringBoot Starter
作用将一组相关的依赖打包,简化项目的配置和初始化过程,通过特定的
Starter
开发者可以快速的实现特定功能模块的开发和扩展。

自定义
Starter
能够促进团队内部资源的复用,保持项目间的一致性,提升协作效率并且有助于构建稳定、高效的大型系统。

开发流程

注入SpringBoot的方式

在刚开始开发
Starter
的时候,首先考虑的是怎么能注入到
SpringBoot中

这部分涉及到部分
SpringBoot
的自动装配原理,不太清楚的朋友可以补习下;

注入
SpringBoot
需要配置文件,在项目中的
resources
资源目录中创建该目录和文件。

demo-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── DemoBean.java
                └── DemoBeanConfig.java
        └── resources
                └── META-INF
                    └── spring.factories


spring.factories
中我们指定一下自动装配的配置类,格式如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xbhog.DemoBeanConfig
/**
 * @author xbhog
 * @describe:
 */
@Slf4j
@Configuration
public class DemoBeanConfig {

    @Bean
    public DemoBean getDemo() {
        log.info("已经触发了配置类,正在初始化DemoBean...");
        return new DemoBean();
    }
}
@Slf4j
public class DemoBean {
    public void getDemo(){
      log.info("方法调用成功");
    }
}

这样就可以将设置的包扫描路径下的相关操作打包到
SpringBoot
中。

SpringBoot
主类启动器:初始化的操作,感兴趣的朋友可以研究下

完成后,我们可以打包该项目,然后在测试工程红进行Maven的引入、测试。

测试

新建
Spring
测试工程,引入依赖:

<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>demo-spring-boot-starter</artifactId>
    <version>1.0</version>
</dependency>
@RestController
public class BasicController implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    
    /**两种引入方式都可以
    @Autowired
    private DemoBean demoBean;*/

    @GetMapping("/configTest")
    public void configTest() {
        DemoBean demoBean = applicationContext.getBean(DemoBean.class);
        demoBean.getDemo();
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

请求地址后,可以观察控制台,如下日志表示
SpringBoot Starter
可以使用了。

到此,一个简单的
Starter
开发完成了,后续可以围绕工程,根据需求和业务,对通用功能(接口操作日志、异常、加解密、白名单等)进行封装,最后打到
Maven
仓库中进行使用。

自定义SpringBoot Starter(全局加解密)

来源

在之前金融系统开发中,需要对接多个第三方的服务且数据安全性要求比较高;在接口评审阶段需要双方在数据传输的时候进行接口加解密;起初在第一个服务对接的时候,将相关的加解密操作写到工具类中;随着后续服务的增多,代码的侵入越来越严重。

封装

选择通过
Starter
进行功能的封装;好处:引用方便,开发迭代方便,团队复用度高且对业务没有侵入。

开发

思路:通过配置文件初始化,让配置类注解
@ComponentScan
扫描到的
Bean等
注入到
SpringBoot
中,通过自定义注解和``RequestBodyAdvice/ResponseBodyAdvice
组合拦截请求,在
BeforBodyRead/beforeBodyWrite`中进行数据的前置处理,解密后映射到接口接收的字段或对象。

接口上的操作有两种方式:

  1. 注解+
    AOP
    实现
  2. 注解+
    RequestBodyAdvice/ResponseBodyAdvice

这里我选择的第二种的
RequestBodyAdvice/ResponseBodyAdvice
,抛砖引玉一下。

【注】
第二种存在的局限性是:只能针对
POST
请求中的
Body
数据处理,无法针对
GET
请求进行处理。

项目结构:

encryAdecry-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── advice
                │   ├──ResponseBodyEncryptAdvice.java
                │   └──RequestBodyDecryptAdvice.java
                ├── annotation
                │   └──SecuritySupport
                ├── handler
                │    ├──impl
                │    │   └──SecurityHandlerImpl.java
                │    └──SecurityHandler
                └── holder
                │    ├──ContextHolder.java
                │    ├──EncryAdecryHolder.java
                │    └──SpringContextHolder.java
                └──GlobalConfig.java
        └── resources
                └── META-INF
                    └── spring.factories

项目处理流程图:

核心代码:

@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
    log.info("进入【RequestBodyDecryptAdvice】beforeBodyRead的操作,方法:{}",parameter.getMethod());
    SecuritySupport securitySupport = parameter.getMethodAnnotation(SecuritySupport.class);
    assert securitySupport != null;
    ContextHolder.setCryptHolder(securitySupport.securityHandler());
    String original = IOUtils.toString(inputMessage.getBody(), Charset.defaultCharset());
    //todo
    log.info("该流水已插入当前请求流水表");
    String handler = securitySupport.securityHandler();
    String plainText = original;
    if(StringUtils.isNotBlank(handler)){
        SecurityHandler securityHandler = SpringContextHolder.getBean(handler, SecurityHandler.class);
        plainText = securityHandler.decrypt(original);
    }
    return new MappingJacksonInputMessage(IOUtils.toInputStream(plainText, Charset.defaultCharset()), inputMessage.getHeaders());
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
    log.info("进入【ResponseBodyEncryptAdvice】beforeBodyWrite的操作,方法:{}",returnType.getMethod());
    String cryptHandler = ContextHolder.getCryptHandler();
    SecurityHandler securityHandler = SpringContextHolder.getBean(cryptHandler, SecurityHandler.class);
    assert body != null;
    return securityHandler.encrypt(body.toString());
}


Starter
中的全局加解密默认采用的
国密非对称加密SM2
,在开发过程中遇到了该问题
InvalidCipherTextException: invalid cipher text

【原因】 私钥和公钥值不是成对存在的,每次调用
SmUtil.sm2()
会生成不同的随机密钥对。

【解决】在该
Starter
中采用
@PostConstruct
修饰方法,在项目运行中只会初始化运行一次该方法,保证了
SmUtil.sm2()
只会调用一次,不会生成不同的随机秘钥对。


ISSUES#1890
】详细请看该地址:
https://hub.fgit.cf/dromara/hutool/issues/1890

/**
 * @author xbhog
 * @date 2024/02/01 13:23
 **/
@Slf4j
@Component
public class EncryAdecryHolder {
    public static SM2 sm2 = null;
    @PostConstruct
    public void encryHolder(){
        KeyPair pair = SecureUtil.generateKeyPair("SM2");
        byte[] privateKey = pair.getPrivate().getEncoded();
        byte[] publicKey = pair.getPublic().getEncoded();
        log.info("生成的公钥:{}",publicKey);
        log.info("生成的私钥:{}",privateKey);
        sm2= SmUtil.sm2(privateKey, publicKey);
    }
}

除了默认的加密方式,还可以通过
SecurityHandler
接口进行扩展,扩展出来的
impl
可以在
@SecuritySupport(securityHandler="xxxxxx")
中指定。

/**
 * @author xbhog
 * @describe: 全局加解密注解
 * @date 2023/6/8
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecuritySupport {
    /*securityHandlerImpl*/
    String securityHandler() default "securityHandlerImpl";

    String exceptionResponse() default "";

}

测试

复用之前的测试项目,引用打包的
mavne
依赖:

<dependency>
    <groupId>com.xbhog</groupId>
    <artifactId>encryAdecry-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

启动项目,初始化公私钥。


测试接口代码如下:

@Slf4j
@RestController
public class BasicController implements ApplicationContextAware {
    @Resource(name = "demoSecurityHandlerImpl")
    private SecurityHandler encryAdecry;
    private ApplicationContext applicationContext;

    // http://127.0.0.1:8080/hello?name=lisi
    //@SecuritySupport(securityHandler = "demoSecurityHandlerImpl")
    @SecuritySupport
    @PostMapping("/hello")
    public String hello(@RequestBody String name) {
        return "Hello " + name;
    }

    @GetMapping("/configTest")
    public String configTest(@RequestParam("name") String name) {
        /*DemoBean demoBean = applicationContext.getBean(DemoBean.class);
        demoBean.getDemo();*/
        return encryAdecry.encrypt(name);
        //return MD5.create().digestHex16(name);
    }
}

优化

优化后的项目结构:

encryAdecry-spring-boot-starter
└── src
    └── main
        └── java
            └── com.xbhog
                ├── advice
                │   ├──ResponseBodyEncryptAdvice.java
                │   └──RequestBodyDecryptAdvice.java
                ├── annotation
                │   └──SecuritySupport
                ├── handler
                │    ├──impl
                │    │   └──EncryAdecryImpl.java
                │    └──SecurityHandler
                └── holder
                │    ├──ContextHolder.java
                │    └──SpringContextHolder.java
                ├──GlobalProperties.java
                └──GlobalConfig.java
        └── resources
                └── META-INF
                    └── spring.factories

增加配置类,用于绑定外部配置(
properties

YAML
)到Java对象的的一种机制;

@Data
@ConfigurationProperties(GlobalProperties.PREFIX)
public class GlobalProperties {
    /**
     * 默认前缀
     */
    public static final String PREFIX = "encryption.type";
    /**
     * 加解密算法
     */
    private String algorithmType;

    /**
     * 加解密key值
     */
    private String key;
}

注解修改:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SecuritySupport {
    /**
     * 项目默认加解密实现类encryAdecryImpl
     * */
    String securityHandler() default "encryAdecryImpl";

}

重写
Starter
默认的加解密方式:

@Slf4j
@Component
public class EncryAdecryImpl implements SecurityHandler {

    @Resource
    private GlobalProperties globalProperties;
    private static volatile SM2 sm2;

    @Override
    public String encrypt(String original) {
        log.info("【starter】具体加密的数据{}",original);
        return sm2.encryptBase64(original, KeyType.PublicKey);
    }

    @Override
    public String decrypt(String original) {
        String decryptData = StrUtil.utf8Str(sm2.decryptStr(original, KeyType.PrivateKey));
        log.info("【starter】具体解密的数据:{}",decryptData);
        return decryptData;
    }

    @PostConstruct
    @Override
    public void init() {
        log.info("======>获取映射的加密算法类型:{}",globalProperties.getAlgorithmType());
        //传的是加密算法
        KeyPair pair = SecureUtil.generateKeyPair(globalProperties.getAlgorithmType());
        byte[] privateKey = pair.getPrivate().getEncoded();
        byte[] publicKey = pair.getPublic().getEncoded();
        sm2= SmUtil.sm2(privateKey, publicKey);
    }
}

原文 | Damian Edwards

翻译 | 郑子铭

组件更新

组件包现在有单独的图标

大多数 Aspire 组件的 NuGet 包现在都具有代表性图标,以便在 NuGet 包管理器对话框中更轻松地识别它们,并给体验带来一些视觉差异:

MySqlConnector 的组件和托管支持

感谢社区成员
Bradley Grainger
(流行的
MySQL MySqlConnector 客户端库
的作者),现在有一个适用于 MySQL 的 Aspire 组件,支持配置、DI、跟踪、日志记录和运行状况检查,支持从以下位置对 MySQL 数据库进行可观察且弹性的访问:您的服务应用程序。

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire components.
builder.AddServiceDefaults();
builder.AddMySqlDataSource("catalog");

ar app = builder.Build();

app.MapGet("/catalog", async (MySqlConnection db) =>
{
    const string sql = """
        SELECT Id, Name, Description, Price
        FROM catalog
        """;

    return await db.QueryAsync<CatalogItem>(sql);
});

app.MapDefaultEndpoints();

app.Run();

此外,还添加了对 MySQL 资源的 Aspire.Hosting 支持,使得启动 MySQL 容器进行本地开发或连接到现有 MySQL 实例变得比以往更加容易。

var builder = DistributedApplication.CreateBuilder(args);

var catalogDb = builder.AddMySqlContainer("mysql")
    // Mount the SQL scripts directory into the container so that the init scripts run.
    .WithVolumeMount("../DatabaseContainers.ApiService/data/mysql", "/docker-entrypoint-initdb.d", VolumeMountType.Bind)
    .AddDatabase("catalog");

MongoDB 的组件和托管支持

另一项社区贡献是由
Ailton Pinto
做出的,通过新的 Aspire.MongoDB.Driver 包为 Aspire 带来了对 MongoDB 的支持。该组件使用
MongoDB.Driver
客户端库,与 MySQL 组件一样,支持配置、DI、跟踪、日志记录和运行状况检查。

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire components.
builder.AddServiceDefaults();
builder.AddMongoDBClient("mydatabase");

ar app = builder.Build();

app.MapGet("/", async (IMongoClient client) =>
{
    // Use the client here
    ...
});

app.MapDefaultEndpoints();

app.Run();

在托管方面,您可以使用 AddMongoDBContainer 配置 MongoDB 容器,或使用 AddMongoDBConnection 指向现有实例。

var builder = DistributedApplication.CreateBuilder(args);

var mongodb = builder.AddMongoDBContainer("mongodb")
    .AddDatabase("mydatabase");

var myService = builder.AddProject<Projects.MyService>()
    .WithReference(mongodb);

Azure SDK 组件现在默认启用分布式跟踪

Azure SDK 的 Aspire 组件现在默认启用分布式跟踪(Azure 服务总线组件除外)。使用这些组件连接到当前支持的 Azure 服务将自动为应用程序的分布式跟踪输出做出贡献,有助于使其更易于观察。

部署更新

.NET Aspire 的一大功能是部署工具能够添加对您在 Aspire AppHost 项目中描述和组合的分布式应用程序的支持。这个领域还处于早期阶段,但我们已经看到了一些令人兴奋的工作。

Aspir8:社区开发的工具,用于将 .NET Aspire 应用程序部署到 Kubernetes

我们看到了很多对支持将 Aspire 应用程序部署到 Kubernetes 感兴趣的人的反馈。在社区驱动开发的另一个精彩展示中,
Aspirate (Aspir8)
是由 GitHub 用户
prom3theu5
创建的。此 .NET 全局工具利用
Aspire 清单
自动将 Aspire 应用程序部署到 Kubernetes 集群。请务必查看
项目自述文件
以了解更多详细信息,并按照
Aspire 存储库中的 GitHub 问题
进行操作。

Azure 开发人员 CLI (azd) Aspire 改进

Azure 开发人员 CLI (azd)
是一种开源工具,可以让您的应用程序更轻松地在 Azure 云中运行。我们正在努力确保 azd 能够以最快、最简单的方式在几分钟内将 Aspire 应用程序配置并部署到 Azure。在此版本中,我们仍然主要针对 Azure 容器应用程序。

在此版本中,我们添加了一些新功能以更好地支持 Aspire 部署:

  • 部署使用 AsDockerFileInManifest 配置的 Dockerfile 项目。
  • 部署Dapr相关组件。这将在
    下面的下一节
    中更详细地解释。
  • 初步支持管道配置,以便为 Aspire 应用程序配置 CI/CD 部署变量。

还有其他一些值得注意的改进:

  • 默认情况下为预配的 Azure 容器注册表启用管理员用户身份验证。对于未完全迁移到 RBAC 的订阅中的经典管理员用户,这可以避免与授权相关的失败。
  • 仅当使用 dotnetpublish 构建和发布 .NET 项目时,不再需要安装 docker 工具。

通过
安装或更新 Azure 开发人员 CLI
获取最新的 azd 版本 (1.5.1)。

Dapr 支持更新

对于那些想要将 Aspire 与
Dapr
一起使用的用户,预览版 2 中进行了许多改进。

不再需要指定 Dapr 应用程序 ID

Dapr 应用程序 ID 现在将默认为其各自项目指定的资源名称。此外,Dapr sidecar 的资源名称现在源自其各自项目的名称,而在预览版 1 中,资源名称是应用程序 ID 本身。这使得项目资源名称和应用程序 ID 具有相同的值。如果需要,您仍然可以将应用程序 ID 显式设置为显式值。

var builder = DistributedApplication.CreateBuilder(args);

// The Dapr application ID will default to "servicea".
builder.AddProject<Projects.DaprServiceA>("servicea")
       .WithDaprSidecar();

// The Dapr application ID is explicitly set to "serviceb-dapr".
builder.AddProject<Projects.DaprServiceB>("serviceb")
       .WithDaprSidecar("serviceb-dapr");

Dapr 组件是一流的 Aspire 资源

.NET Aspire Preview 2 引入了
Dapr 组件
作为一流的资源,这使得 Aspire 在运行 Dapr 应用程序时能够做出更明智的决策,并启用其他工具(例如用于部署的工具)。

在预览版 1 中,您可以使用 DaprSidecarOptions 为 sidecar 一起配置 Dapr 资源,其中 sidecar 将加载资源目录中找到的所有组件。

var builder = DistributedApplication.CreateBuilder(args);

// Configure service A's sidecar to load components from the resources path.
builder.AddProject<Projects.DaprServiceA>("servicea")
       .WithDaprSidecar(
            new DaprSidecarOptions
            {
                AppId = "service-a",
                ResourcesPaths = ImmutableHashSet<string>.Create("<path to resources directory>")
            });

// Configure service B's sidecar to load components from the resources path.
builder.AddProject<Projects.DaprServiceB>("serviceb")
       .WithDaprSidecar(
            new DaprSidecarOptions
            {
                AppId = "service-b",
                ResourcesPaths = ImmutableHashSet<string>.Create("<path to resources directory>")
            });

在预览版 2 中,您可以创建单独的 Dapr 组件资源,并从实际使用它们的项目中引用它们(通过 WithReference())。 Aspire 将确保 sidecar 配置为加载其引用的组件。

var builder = DistributedApplication.CreateBuilder(args);

var stateStore = builder.AddDaprComponent(
    "statestore",
    "state.redis",
    new DaprComponentOptions { LocalPath = "<path to state store YAML file>" });

var pubSub = builder.AddDaprComponent(
    "pubsub",
    "pubsub.redis",
    new DaprComponentOptions { LocalPath = "<path to pub-sub YAML file>" });

builder.AddProject<Projects.DaprServiceA>("servicea")
       .WithDaprSidecar()
       .WithReference(stateStore)
       .WithReference(pubSub);

builder.AddProject<Projects.DaprServiceB>("serviceb")
       .WithDaprSidecar()
       .WithReference(pubSub);

对于基本组件,例如状态存储和发布-订阅,您无需创建或指定本地组件 YAML 路径。相反,AddDaprStateStore() 和 AddDaprPubSub() 方法创建“通用”类型的 Dapr 组件资源,这表明 Aspire 应在应用程序运行时代表 Dapr sidecar 配置适当的组件。

var builder = DistributedApplication.CreateBuilder(args);

var stateStore = builder.AddDaprStateStore("statestore");
var pubSub = builder.AddDaprPubSub("pubsub");

builder.AddProject<Projects.DaprServiceA>("servicea")
       .WithDaprSidecar()
       .WithReference(stateStore)
       .WithReference(pubSub);

builder.AddProject<Projects.DaprServiceB>("serviceb")
       .WithDaprSidecar()
       .WithReference(pubSub);

在前面的示例中,如果 Dapr 在计算机上完全初始化,Aspire 将配置 sidecar 以使用由 Dapr 默认 Redis 容器支持的 Redis 组件。相反,如果 Dapr 被初始化为“slim”,Aspire 将配置 sidecar 以使用内存组件。声明单个 Dapr 组件资源的真正好处是,它使本地开发和部署工具能够就如何配置 Dapr 做出更好的决策。

支持使用 Azure 开发人员 CLI (azd) 将 Dapr 应用程序部署到 Azure 容器应用程序 (ACA)

预览版 2 将特定于 Dapr 的资源写入应用程序清单,这使得 Azure 开发人员 CLI (azd) 等工具能够在部署期间做出特定于 Dapr 的决策。 AZD 现在可用于部署和配置使用 Dapr 到 Azure 容器应用程序 (ACA) 的 .NET Aspire 应用程序。每个具有 Dapr sidecar 的项目都将在其相应的 ACA 应用程序中启用 Dapr,并且其 Dapr 设置将反映以下 DaprSidecarOptions 属性(如果设置):

  • 应用程序编号
  • 应用程序端口
  • 应用程序协议
  • 启用Api日志记录
  • Http最大请求大小
  • HttpReadBufferSize
  • 日志级别

如果您的应用程序声明通用状态存储和发布-订阅 Dapr 组件引用,azd 还将使用 Redis 插件配置 ACA 环境,并生成和部署 Dapr 组件配置,以便从您的应用程序中使用它。这意味着无需任何显式配置或配置后备存储即可部署基本 Dapr 应用程序。

[!重要] 如果您的应用程序声明的 Dapr 组件类型超出了通用状态存储和发布-订阅类型,则仍必须在 ACA 环境中手动配置这些类型。

社区贡献

尽管自我们宣布 .NET Aspire 并发布
存储库
以来只过去了很短的时间,但我们已经
看到了 .NET 社区令人难以置信的参与和贡献
。从上述预览版 2 中的新组件和 Aspir8 部署工具,到
仍在开发中的众多社区贡献的组件
(请留意预览版 3...)、详细问题以及人们在
讨论
中互相帮助,人们正在参与塑造Aspire 将成为什么。我们谨向所有尝试过 Aspire 并花额外时间做出贡献的人致以诚挚的“感谢”。

下一步是什么?

我们计划每月发布新的 Aspire 预览版,并努力在 2024 年第二季度发布稳定的 8.0 版本。请返回 .NET 博客了解未来版本的详细信息,或参与
GitHub 上的 Aspire 项目
。官方示例可在
dotnet/aspire-samples
存储库中找到。

概括

再次感谢您对 .NET Aspire 的回复。我们很高兴努力使使用 .NET 构建分布式应用程序成为一种很棒的体验,并且希望您
尝试预览版 2
并让我们知道您的想法。

原文链接

Announcing .NET Aspire Preview 2

知识共享许可协议

本作品采用
知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议
进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接:
http://www.cnblogs.com/MingsonZheng/
),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (
MingsonZheng@outlook.com
)

五、用go语言,对区间树 T 和一个区间 i ,请修改有关区间树的过程来支持新的操作 INTERVALSEARCH-EXACTLY(T,i) ,它返回一个指向 T 中结点 x 的指针,使得 x.int. low==i.low 且 x.int.high == i.high ;或者,如果不包含这样的区间时返回 T.nil 。所有的操作(包括INTERVAL-SEARCH-EXACTLY)对于包含 n 个结点的区间树的运行时间都应为 O(lgn)。

文心一言:

为了实现INTERVAL-SEARCH-EXACTLY操作,我们可以在区间树中添加一个辅助函数来递归地搜索与给定区间相匹配的节点。以下是一个可能的实现:

type IntervalTreeNode struct {  
    Interval   Interval  
    Left, Right *IntervalTreeNode  
    Ptr         *IntervalTreeNode // 指向与当前节点区间完全匹配的子节点  
}  
  
func (t *IntervalTreeNode) SearchExactly(i Interval) *IntervalTreeNode {  
    if t == nil || i.Start < t.Interval.Start || i.End > t.Interval.End {  
        return t  
    }  
    if t.Ptr != nil && t.Ptr.Interval == i {  
        return t.Ptr  
    }  
    if i.Start <= t.Interval.Start {  
        left := t.Left.SearchExactly(i)  
        if left != nil {  
            t.Ptr = left  
            return left  
        }  
    }  
    if i.End >= t.Interval.End {  
        right := t.Right.SearchExactly(i)  
        if right != nil {  
            t.Ptr = right  
            return right  
        }  
    }  
    return nil  
}

在这个实现中,我们添加了一个Ptr字段来存储指向与当前节点区间完全匹配的子节点的指针。在搜索过程中,我们首先检查当前节点是否与给定区间匹配,如果不匹配,则返回nil。如果当前节点与给定区间部分匹配,我们分别在左子树和右子树中递归地搜索完全匹配的节点,并更新Ptr字段来存储找到的节点指针。最后,返回找到的节点指针或nil。

现在,我们可以使用以下代码来调用INTERVAL-SEARCH-EXACTLY操作:

func INTERVAL-SEARCH-EXACTLY(T *IntervalTreeNode, i Interval) *IntervalTreeNode {  
    result := T.SearchExactly(i)  
    if result != nil {  
        return result  
    } else {  
        return T.nil // 返回空指针或表示无效的节点指针  
    }  
}

请注意,这个实现假设了T.nil是一个预定义的空指针或无效节点指针,用于表示没有找到与给定区间完全匹配的节点。你可以根据你的具体需求来定义T.nil的值。

在这里插入图片描述

在这里插入图片描述

讯飞星火:

在Go语言中,我们可以使用二分搜索来解决这个问题。我们首先找到区间树的根节点,然后比较根节点的区间和给定的区间。如果根节点的区间等于给定的区间,我们就返回根节点。万分抱歉,我可能还需要反应一会,给我点时间,谢谢。

在这里插入图片描述

360智脑:

以下是 Go 语言实现的对区间树 T 和一个区间 i 进行 INTERVAL-SEARCH-EXACTLY 操作的算法:

type IntervalTreeNode struct {
    LeftChild  *IntervalTreeNode 
    RightChild *IntervalTreeNode 
    Max        int 
    // 其他属性 
}
func IntervalSearchExactly(T *IntervalTreeNode, i int) *IntervalTreeNode {
    // 如果树为空,返回 nil 
    if T == nil {
        return nil 
    }
    // 如果当前节点的低端点不等于查询区间的低端点,返回 nil 
    if T.Max != i {
        return nil 
    }
    // 如果当前节点的高端点不等于查询区间的高端点,返回 nil 
    if T.Max != i {
        return nil 
    }
    // 如果当前节点的区间与查询区间完全相等,返回当前节点 
    return T 
}

注意,以上代码仅为示例,具体实现可能需要根据实际需求进行调整。

在这里插入图片描述

智谱清言,代码正常运行:

为了支持新的操作
INTERVALSEARCH-EXACTLY(T, i)
,我们需要对区间树(Interval Tree)的结构和操作进行扩展。在Go语言中实现这一操作,我们需要遵循以下步骤:

1.定义区间树的数据结构,包括区间的定义(low 和 high)以及树节点(可能包含指向子节点的指针和区间信息)。

2.实现区间树的插入、删除和查询操作,确保所有操作的运行时间为 O(lgn)。

3.实现
INTERVALSEARCH-EXACTLY(T, i)
操作,它将搜索具有精确边界匹配的区间。

以下是一个简化的Go语言实现,它展示了如何定义区间树的数据结构,以及如何实现
INTERVALSEARCH-EXACTLY
操作:

package main
import (
	"fmt"
)
// Interval 定义了一个区间
type Interval struct {
	Low   int
	High  int
	Left  *Interval
	Right *Interval
}
// IntervalTreeNode 是区间树的节点
type IntervalTreeNode struct {
	Interval
	Parent *IntervalTreeNode
	Left   *IntervalTreeNode
	Right  *IntervalTreeNode
}
// NewIntervalTreeNode 创建一个新的区间树节点
func NewIntervalTreeNode(interval Interval) *IntervalTreeNode {
	return &IntervalTreeNode{
		Interval: interval,
	}
}
// IntervalTree 是区间树的封装
type IntervalTree struct {
	Root *IntervalTreeNode
}
// Insert 向区间树中插入一个新的区间
func (t *IntervalTree) Insert(interval Interval) {
	// 插入逻辑,确保树操作时间复杂度为 O(lgn)
	// ...
}
// SearchExactly 搜索具有精确边界匹配的区间
func (t *IntervalTree) SearchExactly(i Interval) *IntervalTreeNode {
	return searchExactly(t.Root, i)
}
// searchExactly 在区间树中递归搜索精确匹配的区间
func searchExactly(node *IntervalTreeNode, i Interval) *IntervalTreeNode {
	if node == nil {
		return nil
	}
	if i.Low == node.Interval.Low && i.High == node.Interval.High {
		return node
	}
	// 根据区间的边界值选择左子树或右子树
	if i.Low < node.Interval.Low {
		return searchExactly(node.Left, i)
	} else if i.Low > node.Interval.Low {
		return searchExactly(node.Right, i)
	}
	// 如果 low 相同,需要进一步检查 high
	if i.High < node.Interval.High {
		return searchExactly(node.Left, i)
	} else if i.High > node.Interval.High {
		return searchExactly(node.Right, i)
	}
	// 如果 high 也相同,则找到了匹配的区间
	return node
}
func main() {
	// 创建区间树
	tree := &IntervalTree{}
	// 插入区间
	// tree.Insert(Interval{Low: 1, High: 5})
	// tree.Insert(Interval{Low: 2, High: 7})
	// tree.Insert(Interval{Low: 3, High: 9})
	// 搜索精确匹配的区间
	i := Interval{Low: 4, High: 6}
	node := tree.SearchExactly(i)
	if node != nil {
		fmt.Printf("Found interval: %#v\n", node.Interval)
	} else {
		fmt.Println("No interval found")
	}
}

请注意,上述代码中省略了区间树的插入(
Insert
方法)实现,因为这通常涉及到平衡二叉搜索树(如AVL树或红黑树)的复杂逻辑,以保持树的平衡并确保所有操作的运行时间为 O(lgn)。完整的实现还需要考虑如何维护区间树的结构,以便在每次插入或删除操作后,树都能保持平衡。
`
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

通义千问:

在Go语言中,为了支持
INTERVALSEARCH-EXACTLY
操作,可以在现有区间树的基础上进行如下修改。这里假设我们已经有了一个实现了基本区间搜索功能的区间树结构。首先,我们需要对区间树节点进行扩展,增加一个标志位表示该节点代表的区间是否精确匹配目标区间。接下来,提供一个递归的
exactMatchSearch
函数,该函数在O(log n)时间内找到与给定区间完全相同的区间节点。

type Interval struct {
    Low, High int // 区间端点
}

type Node struct {
    Interval Interval
    IsExactMatch bool // 新增标志位,表示该节点区间是否精确匹配目标区间
    Left, Right *Node
}

// INTERVALSEARCH-EXACTLY 函数
func IntervalSearchExactly(root *Node, i Interval) *Node {
    if root == nil {
        return nil
    }

    if root.Interval.Low == i.Low && root.Interval.High == i.High {
        root.IsExactMatch = true // 标记为精确匹配
        return root
    }

    leftResult := IntervalSearchExactly(root.Left, i)
    if leftResult != nil {
        return leftResult
    }

    rightResult := IntervalSearchExactly(root.Right, i)
    if rightResult != nil {
        return rightResult
    }

    return nil // 未找到完全匹配的区间
}

// 初始化函数,用于创建和插入新区间时清零IsExactMatch标志
func insertInterval(root **Node, interval Interval) {
    // 插入新区间的逻辑...
    (*root).IsExactMatch = false // 清零精确匹配标志
}

// (可选)在删除节点时,也需要重置其祖先节点的IsExactMatch标志
func deleteNode(root **Node, i Interval) {
    // 删除节点的逻辑...
    resetAncestorExactMatch(root, i)
}

// 重置祖先节点的IsExactMatch标志
func resetAncestorExactMatch(root **Node, deletedInterval Interval) {
    current := *root
    for current != nil {
        if current.Interval.Low == deletedInterval.Low && current.Interval.High == deletedInterval.High {
            current.IsExactMatch = false
            break
        }
        if deletedInterval.Low <= current.Interval.Low {
            current = current.Left
        } else {
            current = current.Right
        }
    }
}

注意:上述实现中,
IsExactMatch
标志仅用于加速后续的精确匹配查询,插入和删除操作仍需维持原有的O(log n)时间复杂度。在实际使用时,你可能还需要根据你的区间树实现细节进行适当的调整。同时,由于区间树本身并不保证存在唯一区间,所以即使找到了一个精确匹配的区间,也不能排除树中还存在其他同样大小的区间。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

天工:

在Go语言中,区间树通常用数组来表示,其中每个元素对应一个节点。我们可以使用两种类型的查询:

  1. 查询与给定区间i完全相同的节点。这可以通过从根开始,递归地检查子节点的区间是否与i完全相同。如果相同,我们就递归地检查那个子节点。

  2. 查询包含给定区间的节点。这可以通过从根开始,递归地检查子节点的区间是否包含给定的区间。如果包含,我们就递归地检查那个子节点。

这两种查询的时间复杂度都是O(logn)。所以,如果我们做一次查询,总的时间复杂度就是O(logn)。

以下是Go语言实现的示例代码:

type IntervalTree struct {
    root   *IntervalTreeNode
    ranges []Interval
}

type IntervalTreeNode struct {
    low, high int
    left, right *IntervalTreeNode
}

func (it *IntervalTree) Insert(interval Interval) {
    // ... 插入操作 ...
}

func (it *IntervalTree) ExactSearch(low, high int) *IntervalTreeNode {
    var result *IntervalTreeNode
    it.exactSearch(it.root, low, high, &result)
    return result
}

func (it *IntervalTree) exactSearch(node *IntervalTreeNode, low, high, result *int) {
    if low == node.low && node.high == high {
        *result = node.index
    }

    if low <= node.low {
        it.exactSearch(node.left, low, high, result)
    }

    if node.high <= high {
        it.exactSearch(node.right, low, high, result)
    }
}

在上述代码中,
ExactSearch
函数可以用于查询与给定区间i完全相同的节点。
Insert
函数用于插入新的区间。注意,这只是一个简化的示例,实际应用中可能需要更复杂的逻辑来处理插入和查询操作。

在这里插入图片描述

在这里插入图片描述

灵小智:

对于给定的区间树 T 和一个区间 i,我们可以通过修改区间树的查询过程来支持新的操作 INTERVAL-SEARCH-EXACTLY(T, i)。以下是一种使用 Go 语言实现的示例:
type Interval struct {
    low  int
    high int
}

type Node struct {
    interval Interval
    max      int
    left     *Node
    right    *Node
    parent   *Node
}

type IntervalTree struct {
    root *Node
    nil  *Node
}

// 创建一个新的区间树
func NewIntervalTree() *IntervalTree {
    nilNode := &Node{interval: Interval{}, max: -1}
    return &IntervalTree{root: nilNode, nil: nilNode}
}

// 向区间树中插入一个区间
func (tree *IntervalTree) Insert(interval Interval) {
    newNode := &Node{interval: interval, max: interval.high, left: tree.nil, right: tree.nil, parent: tree.nil}
    current := tree.root
    var prev *Node

    // 找到插入的位置
    for current != tree.nil && current != nil {
        prev = current
        if interval.low < current.interval.low {
            current = current.left
        } else {
            current = current.right
        }
    }

    // 插入新节点
    newNode.parent = prev
    if prev == tree.nil {
        tree.root = newNode
    } else if interval.low < prev.interval.low {
        prev.left = newNode
    } else {
        prev.right = newNode
    }

    // 更新父节点的最大值
    tree.updateMax(newNode)
}

// 在区间树中搜索与给定区间完全匹配的节点
func (tree *IntervalTree) IntervalSearchExactly(interval Interval) *Node {
    return tree.intervalSearchExactlyHelper(tree.root, interval)
}

// 递归地在区间树中搜索与给定区间完全匹配的节点
func (tree *IntervalTree) intervalSearchExactlyHelper(node *Node, interval Interval) *Node {
    if node == tree.nil || (node.interval.low == interval.low && node.interval.high == interval.high) {
        return node
    }

    if node.left != tree.nil && node.left.max >= interval.low {
        return tree.intervalSearchExactlyHelper(node.left, interval)
    } else {
        return tree.intervalSearchExactlyHelper(node.right, interval)
    }
}

// 更新节点及其祖先节点的最大值
func (tree *IntervalTree) updateMax(node *Node) {
    for node != tree.nil && node != nil {
        if node.max < node.interval.high {
            node.max = node.interval.high
        }
        node = node.parent
    }
}

可以使用以下代码来测试上述区间树的功能:

func main() {
    t := NewIntervalTree()
    
    t.Insert(Interval{low: 10, high: 20})
    t.Insert(Interval{low: 5, high: 15})
    t.Insert(Interval{low: 30, high: 40})
    t.Insert(Interval{low: 25, high: 35})
    
    i := Interval{low: 10, high: 20}
    result := t.IntervalSearchExactly(i)
    
    if result != t.nil {
        fmt.Println("Found matching interval:", result.interval)
    } else {
        fmt.Println("No matching interval found")
    }
}

以上代码演示了如何创建一个区间树,并在其中插入几个区间。然后,我们使用
IntervalSearchExactly
方法搜索与给定区间完全匹配的节点,并打印结果。请注意,上述代码只是示例,你可以根据自己的需求进行修改和扩展。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

chatgpt,代码不能运行:

在Go语言中,可以使用自定义的数据结构来表示区间树,并实现INTERVALSEARCH-EXACTLY操作。下面是一个示例代码:

package main

import (
	"fmt"
)

type Interval struct {
	low  int
	high int
}

type Node struct {
	interval       Interval
	maxHigh        int
	left, right, p *Node
}

type IntervalTree struct {
	root *Node
	nil  *Node // 哨兵节点,表示NIL叶子节点
}

// 创建新的节点
func newNode(interval Interval) *Node {
	return &Node{
		interval: interval,
		maxHigh:  interval.high,
		left:     nil,
		right:    nil,
		p:        nil,
	}
}

// 插入节点到区间树中(基于红黑树)
func (it *IntervalTree) insert(node *Node) {
	y := it.nil // 父节点指针初始化为哨兵节点NIL

	x := it.root
	for x != it.nil { // 查找插入位置,并更新maxHigh值
		x.maxHigh = max(x.maxHigh, node.interval.high)
		y = x

		if node.interval.low < x.interval.low || (node.interval.low == x.interval.low && node.interval.high <= x.interval.high) {
			x = x.left // 在左子树中查找插入位置
		} else {
			x = x.right // 在右子树中查找插入位置
		}
	}

	node.p = y // 设置父节点指针

	if y == it.nil { // 树为空,设置根节点为新插入的节点
		it.root = node
	} else if node.interval.low < y.interval.low || (node.interval.low == y.interval.low && node.interval.high <= y.interval.high) {
		y.left = node // 插入为左子节点
	} else {
		y.right = node // 插入为右子节点
	}

	node.left = it.nil  // 设置左子节点和右子节点为哨兵节点NIL
	node.right = it.nil

	node.maxHigh = max(node.maxHigh, node.interval.high)

	it.insertFixup(node) // 调整红黑树平衡性
}

// 调整红黑树平衡性(插入时)
func (it *IntervalTree) insertFixup(z *Node) {
	for z.p != it.nil && z.p.p != it.nil && z.p.color == "red" {
		if z.p == z.p.p.left { // 父节点是祖父节点的左孩子
			y := z.p.p.right

			if y != it.nil && y.color == "red" { // 情况1:叔节点为红色
				z.p.color = "black"
				y.color = "black"
				z.p.p.color = "red"
				z = z.p.p
			} else { // 叔节点为黑色或NIL,情况2或3
				if z == z.p.right { // 情况2:z是其父亲的右孩子
					z = z.p
					it.leftRotate(z)
				}
				
				// 情况3:z是其父亲的左孩子
				z.p.color = "black"
				z.p.p.color = "red"
				it.rightRotate(z.p.p)
			}
		} else { // 父节点是祖父节点的右孩子,与左孩子对称
			y := z.p.p.left

			if y != it.nil && y.color == "red" {
				z.p.color = "black"
				y.color = "black"
				z.p.p.color = "red"
				z = z.p.p
			} else {
				if z == z.p.left {
					z = z.p
					it.rightRotate(z)
				}

				z.p.color = "black"
				z.p.p.color = "red"
				it.leftRotate(z.p.p)
			}
		}
	}

	it.root.color = "black" // 根节点必须为黑色
}

// 左旋操作
func (it *IntervalTree) leftRotate(x *Node) {
	y := x.right
	x.right = y.left

	if y.left != it.nil {
		y.left.p = x
	}

	y.p = x.p

	if x == it.root { // 更新根节点指针
		it.root = y
	} else if x == x.p.left { // 更新父节点的左子树或右子树指针
		x.p.left = y
	} else {
		x. p.right= y
    }

    y.left=x 
    x. p=y 

    //更新maxHigh值  
     x.maxHigh=max(x.interval.high,max(x.left.maxHigh,x.right.maxHigh))  
     y.maxHigh=max(y.interval.high,max(y. left .max High,y.r ight . max High))
}

// 右旋操作  
func (it *IntervalTree) rightRotate(y *Node) {  
     x := y.left  
     y.left = x.right  
 
     if x.right != it.nil {  
         x. right.p = y  
     }  
 
     x.p = y.p  
 
     if y == it.root { // 更新根节点指针
         it.root = x  
     } else if y == y.p.left { // 更新父节点的左子树或右子树指针
         y.p.left = x  
     } else {  
         y. p. r ight=x 
    }
    
    x. right=y 
    y .p=x 

   //更新maxHigh值   
    y.maxHigh=max(y.interval.high,max(y.left.maxHigh,y.right.maxHigh))   
    x.maxHigh=max(x.interval.high,max(x.left.maxHigh,x.right.maxHigh))   
}  

// INTERVAL-SEARCH-EXACTLY操作:查找T中与给定区间i完全相同的区间
func (it *IntervalTree) intervalSearchExactly(node *Node, i Interval) *Node {
	if node == it.nil || (node.interval.low == i.low && node.interval.high == i.high) {
		return node
	}

	if i.low < node.interval.low || (i.low == node.interval.low && i.high <= node.interval.high) {
		return it.intervalSearchExactly(node.left, i)
	}

	return it.intervalSearchExactly(node.right, i)
}

// 辅助函数:返回两个整数中的较大值
func max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// 测试代码
func main() {
	it := &IntervalTree{
		nil:  &Node{color: "black"},
		root: nil,
	}

	intervals := []Interval{
		{low: 2, high: 8},
		{low: 5, high: 10},
		{low: 12, high: 15},
	}

	for _, interval := range intervals {
		node := newNode(interval)
		it.insert(node)
    }
    
    // 测试 INTERVAL-SEARCH-EXACTLY 操作
	exactInterval := Interval{low: 5, high: 10}
	foundNode := it.intervalSearchExactly(it.root, exactInterval)

	if foundNode == it.nil {
		fmt.Println("No exact interval found.")
	} else {
		fmt.Printf("Exact interval found: [%d, %d]\n", foundNode.interval.low, foundNode.interval.high)
    }
}

上述代码演示了如何使用红黑树实现区间树,并在其中添加了INTERVAL-SEARCH-EXACTLY操作,它可以在O(log n)的时间内返回与给定区间完全相同的节点。通过对树结构的插入和旋转操作来维持平衡性,从而保证了O(log n)的时间复杂度。

请注意,为简化示例,我省略了节点颜色属性、调整平衡性的过程等一些边界情况的处理。你可以根据需要进行适当修改和扩展以满足更复杂的需求。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

F

在这里插入图片描述

在这里插入图片描述

kimi,代码不能运行:

在 Go 语言中,为了支持 INTERVALSEARCH-EXACTLY 操作,我们需要对区间树的搜索过程进行修改。这个操作的目标是找到与给定区间 i 完全匹配的节点。如果不存在这样的节点,我们返回 T.nil。以下是一个可能的实现:

package main

import (
	"fmt"
)

type IntervalNode struct {
    intLow, intHigh int // 区间的低端点和高端点
    left, right       *IntervalNode
}

var TNil *IntervalNode // 哨兵节点,表示不存在的节点

func INTERVALSEARCH_EXACTLY(root *IntervalNode, i int) *IntervalNode {
    if root == TNil {
        return TNil
    }

    // 如果当前节点的区间与给定区间 i 完全匹配,返回当前节点
    if root.intLow == i && root.intHigh == i {
        return root
    }

    // 如果当前节点的低端点大于 i,那么匹配的区间只能在左子树中
    if root.intLow > i {
        return INTERVALSEARCH_EXACTLY(root.left, i)
    }

    // 如果当前节点的高端点小于 i,那么匹配的区间只能在右子树中
    if root.intHigh < i {
        return INTERVALSEARCH_EXACTLY(root.right, i)
    }

    // 如果当前节点的区间包含 i,那么我们需要在左右子树中继续搜索
    leftResult := INTERVALSEARCH_EXACTLY(root.left, i)
    rightResult := INTERVALSEARCH_EXACTLY(root.right, i)

    // 如果左右子树中都没有找到匹配的区间,返回 TNil
    if leftResult == TNil && rightResult == TNil {
        return TNil
    }

    // 如果左右子树中只有一个结果,返回那个结果
    if leftResult == TNil {
        return rightResult
    }
    if rightResult == TNil {
        return leftResult
    }

    // 如果左右子树都有结果,这是不可能的,因为区间树是二分的
    // 这里可以添加逻辑来处理这种情况,例如返回左子树的结果或者右子树的结果
    // 这里我们假设左子树的结果更优,返回左子树的结果
    return leftResult
}

func main() {
    // 初始化 TNil 哨兵节点
    TNil = &IntervalNode{}

    // 构建区间树 T
    // ...

    // 给定区间 i
    i := 5

    // 执行 INTERVALSEARCH-EXACTLY 操作
    result := INTERVALSEARCH_EXACTLY(T.root, i)

    if result != TNil {
        fmt.Printf("Found interval: [%d, %d]\n", result.intLow, result.intHigh)
    } else {
        fmt.Println("No matching interval found.")
    }
}

在这个实现中,我们首先定义了一个
IntervalNode
结构体来表示区间树的节点,以及一个全局的
TNil
哨兵节点。
INTERVALSEARCH_EXACTLY
函数递归地搜索区间树,寻找与给定区间 i 完全匹配的节点。如果找到了这样的节点,它将返回该节点;如果没有找到,它将返回
TNil

请注意,这个实现假设区间树是平衡的,并且每个节点的低端点和高端点是唯一的。在实际的 Go 代码中,你可能需要根据具体的区间树实现来调整这个代码。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

混元,代码正常运行:

为了支持新的操作 INTERVALSEARCH-EXACTLY(T, i),我们需要修改区间树的数据结构和相关操作。以下是修改后的 Go 语言实现:

package main

import "fmt"

type Interval struct {
	start int
	end   int
}

type Node struct {
	interval Interval
	left     *Node
	right    *Node
}

func newNode(interval Interval) *Node {
	return &Node{
		interval: interval,
	}
}

func insert(root *Node, interval Interval) *Node {
	if root == nil {
		return newNode(interval)
	}

	if interval.start< root.interval.start {
		root.left = insert(root.left, interval)
	} else {
		root.right = insert(root.right, interval)
	}

	return root
}

func intervalSearchExactly(root *Node, i Interval) *Node {
	current := root

	for current != nil {
		if current.interval.start == i.start && current.interval.end == i.end {
			return current
		}

		if i.start< current.interval.start {
			current = current.left
		} else {
			current = current.right
		}
	}

	return nil
}

func main() {
	root := newNode(Interval{start: 1, end: 3})
	root.left = newNode(Interval{start: 2, end: 4})
	root.right = newNode(Interval{start: 4, end: 6})

	i := Interval{start: 2, end: 4}
	result := intervalSearchExactly(root, i)

	if result != nil {
		fmt.Printf("Found interval exactly: %d-%d\n", result.interval.start, result.interval.end)
	} else {
		fmt.Println("No interval found exactly")
	}
}

这个代码实现了一个简单的区间树,其中每个区间都是开区间。我们添加了一个新的函数 intervalSearchExactly,它在给定的区间树中查找与给定区间 i 完全匹配的区间,并返回一个指向该区间的指针。如果没有找到匹配的区间,它将返回 nil。

请注意,这个实现假设区间树中的区间是唯一的。如果区间树中可能存在重复的区间,你需要根据具体需求进行相应的修改。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

代码

原文地址

预备知识:

1.什么是对比学习?

对比学习是一种机器学习范例,将未标记的数据点相互并列,以教导模型哪些点相似,哪些点不同。 也就是说,顾名思义,样本相互对比,属于同一分布的样本在嵌入空间中被推向彼此。 相比之下,属于不同分布的那些则相互拉扯。

摘要

神经模型在关系抽取(RE)的基准任务上表现出色。但是,我们还不清楚文本中哪些信息对现有的RE模型的决策有影响,以及如何进一步提升这些模型的性能。为了解决这个问题,本文实证地分析了文本中两个主要的信息源:文本上下文和实体提及(名称)对RE的作用。本文发现,虽然上下文是预测的主要依据,但RE模型也高度依赖于实体提及中的信息,其中大多数是类型信息;以及现有的数据集可能通过实体提及暴露了一些浅层的启发式规则,从而使RE基准任务上的性能看起来很高。基于这些分析,本文提出了一种基于实体掩码对比预训练的框架,用于RE,旨在在避免对实体的死记硬背或对提及中的表面线索的利用的同时,对文本上下文和类型信息有更深入的理解。本文进行了大量的实验来支持本文的观点,并证明该框架可以在不同的RE场景下提高神经模型的有效性和鲁棒性。

1 Introduction

通常,在典型的RE设置中,文本中有两个主要的信息来源,可能会对RE模型的关系分类有帮助:那就是文本上下文和实体提及(名称)。
文本上下文中存在一些能够表达关系事实的可解释模式。比如,在图1中,“... be founded ... by ...”就是一个能够表示 founded by 关系的模式。早期的RE系统把这些模式形式化成字符串模板,然后通过匹配模板来识别关系。后来的神经模型则更倾向于把这些模式编码成分布式表示,再通过比较表示来预测关系。相比于刚性的字符串模板,神经模型使用的分布式表示具有更好的泛化能力,也有更高的性能。
实体提及对于关系分类也有很多信息价值。如图1所示,可以从实体提及中获得实体的类型信息,这可以帮忙排除一些不符合逻辑的关系。另外,如果这些实体能够和知识图谱(KGs)建立链接,模型就可以利用知识图谱中的外部知识来辅助RE(
Zhang等人, 2019
;
Peters等人, 2019
)。此外,对于近期广泛使用的RE模型中的预训练语言模型,它们在预训练过程中可能已经隐含地学习了一些实体相关的知识(
Petroni等人, 2019
)。
本文通过大量的实验来研究RE模型依赖于两种信息源的程度。发现:
  1. 文本上下文和实体提及都是RE的关键因素。实验表明,上下文是支持分类的主要来源,而实体提及也提供了重要的信息,尤其是实体的类型信息。
  2. 现有的RE基准数据集可能通过实体提及泄露了一些浅层的线索,导致现有模型的高性能。实验显示,即使只给定实体提及作为输入,模型仍然可以达到高性能,这说明这些数据集中存在一些来自实体提及的有偏的统计线索。
基于以上的观察结果,本文提出了一种进一步改进RE模型的方法:应该通过更好地理解上下文和利用实体类型来增强模型的能力,同时防止模型简单地记忆实体或利用提及中的有偏线索。为此,本文探索了一种针对RE的实体掩码对比预训练框架,使用Wikidata来收集可能表达相同关系的句子,并让模型通过对比目标来学习哪些句子在关系语义上是接近的,哪些是不接近的。在这个过程中,随机地掩盖实体提及,以避免模型被它们所影响。在不同的设置和基准数据集上展示了这种预训练框架的有效性,本文认为更好的预训练技术是朝着更好的RE的可靠方向。

2 Pilot Experiment and Analysis

2.1 Models and Dataset

本文选取以下三个模型进行实验分析:
  1. CNN:本文采用了
    Nguyen和Grishman (2015)
    提出的 卷积神经网络 ,并参考了
    Zhang等人(2017)
    的做法,用词性、命名实体识别和位置嵌入来丰富输入。
  2. BERT:本文遵循
    Baldini Soares等人(2019)
    的方法,使用BERT进行RE,用特殊的标记来突显句子中的实体提及,并用实体表示的拼接来进行分类。
  3. Matching the blanks (MTB): MTB
    (Baldini Soares等人,2019)
    是一个基于BERT的面向RE的预训练模型。它通过对两个句子是否提及相同的实体对进行分类来进行预训练,其中实体提及被随机地 遮盖 。它和BERT一样,用相同的方式进行RE的微调。由于它没有公开发布,本文自行预训练了一个

    版本的MTB,并在附录A中给出了细节。
本文使用的基准数据集是 TACRED, 它是一个有监督的RE数据集,具有106,264个实例和42个关系,它还为每个实体提供类型注释。

2.2 Experimental Settings

本文使用不同的输入格式来进行RE,以便在可控的实验中观察上下文和实体提及的影响。前两种格式是以前的文献采用的,也比较接近真实的RE场景:
  1. 上下文+提及(C+M):这是最常用的RE设置,其中提供了整个句子(包括上下文和突出显示的实体提及)。为了让模型知道实体提及的位置,本文对CNN模型使用位置嵌入
    (Zeng et al., 2014)
    ,对预训练的BERT使用特殊的实体标记。
  2. 上下文+类型(C+T):本文用TACRED提供的类型替换实体提及。使用特殊的标记来表示它们:例如,使用[person]和[date]来分别表示类型为person和date的实体。不同于
    Zhang et al. (2017)
    ,本文不会重复特殊标记来匹配实体的长度,以避免泄露实体长度信息。
  3. 仅上下文(OnlyC):为了分析文本上下文对RE的贡献,本文用特殊的标记[SUBJ]和[OBJ]替换所有的实体提及。在这种情况下,实体提及的信息源完全被阻断。
  4. 仅提及(OnlyM):在这种设置下,本文只提供实体提及,丢弃所有其他的文本上下文作为输入。
  5. 仅类型(OnlyT):这与仅提及类似,只不过本文只提供实体类型。

2.3 Result Analysis

表1展示了不同的输入格式和模型在TACRED上的关系分类性能的详细比较。从结果中可以得出以下结论:
  1. 文本上下文和实体提及都是关系分类的关键信息源,而实体提及中最重要的信息是它们的类型。如表1所示,OnlyC,OnlyM和OnlyT相比于C+M和C+T都有显著的性能损失,说明单一的信息源是不足以支持正确的预测的,上下文和实体提及都是必不可少的。此外,还可以发现C+T在TACRED上与C+M在BERT和MTB上达到了相近的效果。这表明实体提及中的大部分有用信息是它们的类型信息。
  2. 在现有的RE数据集中,实体提及存在一些表面的线索,这可能导致RE模型的过高的性能。在TACRED上发现OnlyM在所有三个模型上都有很高的性能,这种现象在其他数据集中也有体现(见表5)。本文还对OnlyC相比于C+M的性能降低进行了深入的分析,在第2.4节中发现,在一些模型难以理解上下文的情况下,它们会倾向于利用实体提及的浅层启发。这促使本文进一步改进模型在从上下文中提取关系的能力,同时避免它们对实体提及的死记硬背。
可以注意到CNN的结果与BERT和MTB有一些不一致:CNN在OnlyC上的性能几乎与OnlyM相同,而C+M比C+T低5%。本文认为这主要是因为CNN的编码能力较弱,不能充分利用上下文的信息,而且更容易对数据集中实体提及的浅层线索产生过拟合。

2.4 Case Study on TACRED

为了进一步了解不同输入格式对性能的影响,本文对TACRED数据集进行了详细的案例研究,选择了BERT作为示例,因为BERT代表了当前最先进的模型类别,而且在MTB数据集上也观察到了类似的结果。
首先,本文比较了C+M和C+T两种输入格式,发现C+M和C+T有95.7%的正确预测是一致的,而C+M的68.1%的错误预测也和C+T相同。这表明模型从实体提及中获取的大部分信息是它们的类型信息。本文还在表2中列出了一些C+M和C+T的不同错误。C+M可能受到训练集中实体分布的影响。在表2中的两个例子中,“Washington”只出现在per:stateorprovince of residence这个关系中,“Bank of America Corp.”只出现在no relation这个关系中,这种偏见可能导致模型的错误。另一方面,C+T可能在缺少具体实体提及的情况下,难以正确理解文本。如例子所示,用类型替换提及后,模型被“general assembly”误导了,无法检测出“Ruben”和“Ruben van Assouw”之间的关系。这表明实体提及除了类型之外,还提供了其他信息来帮助模型理解文本。
本文还研究了为什么OnlyC相比于C+M有如此明显的性能下降。在表3中,将OnlyC(相比于C+M)的所有独有错误预测分为三类。“Wrong”表示句子中有明显的关系模式,但被模型误解了。“No pattern”意味着在掩盖实体提及后,即使是人类也很难判断是什么关系。“Confusing”表示在掩盖实体后,句子变得模糊不清(例如,混淆城市和国家)。如表3所示,在OnlyC的独有错误预测中,有近一半(42%)的句子有明确的关系模式,但模型无法识别出来,这表明在C+M中,模型可能依赖于实体提及的浅层启发来正确预测句子。在其余的情况下,实体提及确实提供了分类的关键信息。

3 Contrastive Pre-training for RE

3.1 Relational Contrastive Example Generation

为了提高RE的效果,本文模型在预训练阶段就专注于从文本中学习关系和实体的表示
。本文采用了对比学习的方法,通过让“邻居”之间的表示更相似,而让“非邻居”之间的表示更不同。在这里,“邻居”是指在知识图谱(KGs)中具有相同关系实体对的句子,而“非邻居”是指没有相同关系实体对的句子。
这里的知识图谱(KG)是由一些关系事实构成的。从中随机选择两个句子



,它们各自包含了两个实体



。如果有一个关系

,使得



都在KG里,就把



称为“邻居”。本文用Wikidata作为KG,因为它和用来预训练的Wikipedia语料库很容易对应起来。在训练过程中,先按照KG里的关系分布来采样一个关系

,然后再采样一个和

有关的句子对

。为了做对比学习,还要随机采样

个句子


,让它们和

组成

个负样本对。模型的任务是在所有正负样本中,找出和

有相同关系的那个句子。
为了防止在预训练时过度依赖实体提及或仅提取它们的表面特征,本文用特殊符号 [BLANK] 随机替换了一些实体提及。用

表示替换的实体比例,并参考
Baldini Soares 等人 (2019)
的方法设置

。注意,
预训练时完全遮盖所有实体提及也不合适,因为这会导致预训练和微调之间的不一致,也会使预训练模型无法利用实体提及的信息(比如,学习实体类型)
用一个例子来说明数据生成过程:在图 2 中,有两个句子“SpaceX was founded in 2002 by Elon Musk”和“As the co-founder of Microsoft, Bill Gates ...””,其中 (SpaceX, founded by, Elon Musk) 和 (Microsoft, founded by, Bill Gates) 都是KG中的关系。本文希望这两个句子能够有相似的表示,体现出 founded by 这个关系。而对于图中右边的另外两个句子,由于它们的实体对之间没有 founded by 这个关系,它们被认为是负样本,应该与左边的句子有不同的表示。在预训练时,每个实体提及都有

的概率被替换为 [BLANK]。
句子中可能没有明确地表达出实体之间的关系,或者表达出的关系与期望的不一致。比如,一个句子提到了“SpaceX”和“Elon Musk”,它们之间可能有 founded by、CEO 或 CTO 等关系,也可能没有任何关系。例如,“Elon Musk answers reporters’questions on a SpaceX press conference”这个句子就没有体现出两者之间的关系。但是,本文认为这种噪声对本文的预训练框架的影响不大:
本文的目的是相比于原始的预训练模型,如 BERT,得到更适合RE的表示,而不是直接用于下游任务的关系抽取模型,所以可以容忍数据中的一些噪声
本文的模型利用生成的关系对比例子,提高了从提及中获取类型信息和从文本上下文中理解关系语义的能力。
(1)
模型通过对比两个涉及不同实体对但关系相同的句子,来识别实体提及之间的关系信息。
同时,使用实体掩码策略,避免了对实体的简单记忆,而是更多地关注实体类型信息。
(2)通过学习一组表达相同关系的不同文本上下文,提取出关系的多样化表达模式。MTB采用了更严格的规则,只采样了两个包含相同实体对的句子。这种方法虽然可以减少噪声,但也限制了数据的多样性,影响了模型学习类型信息的能力。

3.2 Training Objectives

本文的对比式预训练使用了与 BERT相同的 Transformer 架构。用 ENC 表示 Transformer 编码器,用

表示第 i 个位置的输出。对于输入格式,参考了
Baldini Soares等人(2019)
的方法,用特殊的标记来突出实体提及。例如,对于句子“SpaceX was founded by Elon Musk.”,输入序列是““[CLS][E1] SpaceX [/E1] was founded by [E2] Elon Musk [/E2] . [SEP]”。
在预训练过程中,有两个目标:
对比式预训练目标和遮蔽语言建模目标。

对比式预训练目标

如图 2 所示,给定正样本句子对

和负样本句子对

,首先用 Transformer 编码器得到



中的关系感知表示:
其中



是特殊标记 [E1] 和 [E2] 的位置,

表示拼接。有了句子表示,就有了以下的训练目标:
通过优化模型使

最小化,本文期望



的表示更加接近,最终具有相似关系的句子会有相似的表示。

遮蔽语言建模目标

为了保持从 BERT 继承的语言理解能力,并避免灾难性遗忘,本文也采用了 BERT 的遮蔽语言建模 (MLM) 目标。MLM 随机地遮蔽输入中的一些词,然后让模型预测被遮蔽的词,从而学习包含丰富语义和句法知识的上下文表示。用

表示 MLM 的损失函数。
最终,有以下的训练损失:

4 Experiment

4.1 RE Tasks

Supervised RE

这是RE中最广泛采用的设置,其中有一个预定义的关系集合

,数据集中的每个句子

都表达了

中的某个关系。在一些基准测试中,有一种特殊的关系叫做



,表示句子中没有表达给定实体之间的任何关系,或者它们的关系不包含在

中。对于有监督的 RE 数据集,本文使用 TACRED,SemEval-2010 Task 8 ,Wiki80和 ChemProt 。表 4 显示了数据集之间的比较。本文还添加了 1% 和 10% 的设置,意味着只使用训练集的 1% / 10% 的数据。这是为了模拟低资源的情况,并观察模型性能在不同的数据集和设置下的变化。注意,ChemProt 只有 4169 个训练实例,这导致了表 5 中 1% ChemProt 的异常结果。在附录 B 中给出了这个问题的详细说明。

Few-Shot RE

少样本学习(Few-shot learning)是一个近期出现的研究主题,它探讨如何用少量的样本来训练一个模型,使其能够处理新的任务。少样本关系抽取(Few-shot RE)的一个典型设置是 N-way K-shot RE,其中在每个评估阶段,从 N 个关系类型中采样 K 个样本,以及一些查询样本(都属于 N 个关系中的一个),并要求模型根据给定的 N ×K 个样本来对查询样本进行分类。本文使用 FewRel 作为数据集,并在表 4 中列出了它的统计信息。使用原型网络(Prototypical Networks)作为模型,参考前 的方法,并做了一些改动:(1)使用第 3.2 节描述的表示方法,而不是使用 [CLS]。 (2)使用点积(dot production)而不是欧氏距离(Euclidean distance)来衡量样本之间的相似度。本文发现这种方法比原始原型网络有很大的提升。

4.2 RE Models

本文在 2.1 节介绍了 BERT 和 MTB 两种预训练模型,用于(RE。除此之外,本文还提出了一种新的对比预训练框架 (CP),并对其进行了评估。所有模型的预训练和微调过程的详细超参数设置,都写在了附录 A 和 B 中。值得注意的是,由于 MTB 和 CP 都使用了 Wikidata 作为预训练数据,而 Wiki80 和 FewRel 这两个数据集也是基于 Wikidata 构建的,所以在预训练时排除了这两个数据集测试集中的所有实体对,以防止测试集泄露的问题。

4.3 Strength of Contrastive Pre-training

表 5 和 6 对 BERT、MTB 和本文对比预训练模型进行了详细的比较。无论是在不同的设置还是不同的数据集上,MTB 和 CP 都能显著提升模型的性能,体现了面向关系抽取的预训练的强大作用。相较于 MTB,CP 获得了更优的结果,证明了对比预训练框架的有效性。具体来说,本文发现:
(1) CP 在 C+M、OnlyC 和 OnlyM 这三种设置下都能提高模型的性能,说明本文的预训练框架能够同时增强模型对上下文的理解和对类型信息的抽取能力。
(2) CP 在 C+M 和 OnlyC 这两种设置下的性能提升是普遍的,即使是在来自生物医学领域的 ChemProt 和 FewRel 2.0 数据集上也是如此。其在维基百科上训练的模型在生物医学数据集上也有良好的表现,说明 CP 学习到了跨领域的关系模式。
(3) CP 在 OnlyM 这种设置下也有显著的改进,特别是在与维基百科密切相关的 TACRED、Wiki80 和 FewRel 1.0 数据集上。这表明本文的模型在从提及中抽取类型信息方面有更好的能力。上下文和提及的双重提升最终使得 CP 的关系抽取结果更好(更好的 C+M 结果)。
(4) 本文的对比预训练模型在低资源和少样本的设置下的性能提升更为明显。对于 C+M,观察到在 10-way 1-shot FewRel 1.0 上的提升为 7%,在 TACRED 的 1% 设置上的提升为 18%,在 Wiki80 的 1% 设置上的提升为 24%。OnlyC 和 OnlyM 也有类似的趋势。在低资源和少样本的设置下,由于训练数据的不足,模型更难从上下文中学习到关系模式,更容易过拟合于提及的表面线索。但是,通过对比预训练,本文的模型可以更好地利用文本上下文,同时避免被实体所影响,从而大幅度地超过了其他的基线模型。