2023年4月

在Spring框架中@PropertySource注解是非常常用的一个注解,其主要作用是将外部化配置解析成key-value键值对"存入"Spring容器的Environment环境中,以便在Spring应用中可以通过@Value或者占位符${key}的形式来使用这些配置。

使用案列

// @PropertySource需要和@Configuration配个使用
// @PropertySource加载的配置文件时需要注意加载的顺序,后面加载的配置会覆盖前面加载的配置
// @PropertySource支持重复注解
// value值不仅支持classpath表达式,还支持任意合法的URI表达式
@Configuration
@PropertySource(value = "classpath:/my.properties",encoding = "UTF8")
@PropertySource(value = "classpath:/my2.properties",encoding = "UTF8",ignoreResourceNotFound = true)
public static class PropertyConfig {
}

@Component
public class App {
    @Value("${key1:default-val}")
    private String value;

    @Value("${key2:default-val2}")
    private String value2;
}

下面是配置文件my.properties和my2.properties的具体内容。

# my.properties
key1=自由之路

# my2.properties
key1=程序员
key2=自由之路

Spring容器启动时,会将my.properties和my2.properties的内容加载到Environment中,并在App类的依赖注入环节,将key1和key2的值注入到对应的属性。

自定义PropertySource工厂

阅读@PropertySource的源代码,我们发现还有一个factory属性。从这个属性的字面意思看,我们不难猜测出这个属性设置的是用于产生PropertySource的工厂。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {

	String name() default "";
    
	String[] value();
	
    boolean ignoreResourceNotFound() default false;

	String encoding() default "";

	Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

}

要深入理解PropertySourceFactory,我们先要知道以下的背景知识。

在Spring中,配置的来源有很多。Spring将配置来源统一抽象成 PropertySource 这个抽象类,Spring中内建的常用的 PropertySource 有以下这些

  • MapPropertySource

  • CommandLinePropertySource

  • PropertiesPropertySource

  • SystemEnvironmentPropertySource

  • ResourcePropertySource

ResourcePropertySource这个类将一系列配置来源统一成ResourcePropertySource,可以说是对 PropertySource 的进一步封装。

PropertySourceFactory 接口,用于产生PropertySource。Spring中,PropertySourceFactory 默认的实现是DefaultPropertySourceFactory,用于生产 ResourcePropertySource。

经过上面的介绍,我们知道如果没有配置@PropertySource的factory属性的话,默认的PropertySourceFactory使用的就是DefaultPropertySourceFactory。当然,我们也可以自定义PropertySourceFactory,用于“生产”我们自定义的PropertySource。下面就演示一个将yaml文件解析成MapPropertySource的使用案列。

/**
 * Spring中内置的解析yaml的处理器
 * YamlProcessor
 *  - YamlMapFactoryBean  --> 解析成Map
 *  - YamlPropertiesFactoryBean  --> 解析成Properties
 */
public class YamlMapSourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        YamlMapFactoryBean yamlMapFactoryBean = new YamlMapFactoryBean();
        yamlMapFactoryBean.setResources(resource.getResource());
        Map<String, Object> map = yamlMapFactoryBean.getObject();
        return new MapPropertySource(name, map);
    }
}

// 加了factory属性,必须加name属性
// 有了factory机制,我们可以做很多自定一的扩展,比如配置可以从远程来
@PropertySource(name = "my.yaml",value = "classpath:/my.yaml",encoding = "UTF8",factory = YamlMapSourceFactory.class)
public static class PropertyConfig {
}

原理简析

到这边我们对@PropertySource已经有了一个感性的认识,知道了其主要作用是将各种类型的外部化配置文件以key-value的形式加载到Spring的Environment中。这个部分我们从源码的角度来分析下Spring是怎么处理@PropertySource这个注解的。分析源码可以加深我们对@PropertySource的认识(看源码不是目的,是为了加深理解,学习Spring的设计思想)。

@PropertySource注解的处理是在ConfigurationClassPostProcessor中进行触发的。最终会调用到ConfigurationClassParser的processPropertySource方法。

// ConfigurationClassParser#processPropertySource
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    String name = propertySource.getString("name");
    if (!StringUtils.hasLength(name)) {
        name = null;
    }
    String encoding = propertySource.getString("encoding");
    if (!StringUtils.hasLength(encoding)) {
        encoding = null;
    }
    String[] locations = propertySource.getStringArray("value");
    Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

    Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    // 如果有自定义工厂就使用自定义工厂,没有自定义工厂就使用DefaultPropertySourceFactory
    PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
            DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
    // 遍历各个location地址
    for (String location : locations) {
        try {
            // location地址支持占位符的形式
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            // 获取Resource
            Resource resource = this.resourceLoader.getResource(resolvedLocation);
            addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
        }
        catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
            // Placeholders not resolvable or resource not found when trying to open it
            if (ignoreResourceNotFound) {
                if (logger.isInfoEnabled()) {
                    logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
                }
            }
            else {
                throw ex;
            }
        }
    }
}

总的来说,Spring处理@PropertySource的源代码非常简单,这边就不再过多赘述了。

题图

企业级应用中,通常一个业务系统并不是孤立存在的,而是需要与企业、部门或者是外部的已有系统进行集成。一般而言,系统集成的数据和接口交互方式通常有以下几种:

  1. 文件传输:通过文件传输的方式将数据传递给其他系统,例如使用 FTP 或 SFTP 等协议传送文件。这种交互方式适合单向或批量数据传输。
  2. Web 服务 API:使用 API 与其他系统进行通信,一般采用 SOAP 或 REST 等通信协议。这种交互方式可用于实现单向或双向数据传输。
  3. 数据库交互:使用数据库共享的方式,或者通过数据库连接进行数据交换。这种交互方式在双向数据同步和实时数据交换方面非常有效。但是一定要注意数据权限控制和数据安全。
  4. 消息队列:通过消息队列进行数据的收发,而无需与其他系统直接通信。消息队列可用于处理大量数据以及异步数据传输的情况。

不论使用那种方式进行通信和传输,在主系统(即,正在实施的系统)中,还需要考虑的一个问题是,第三方系统过来的数据需不需要保存?如果主系统要基于接入的数据进行进一步处理,则通常需要保存数据。而有时候由于数据安全方面的原因,亦或是考虑到本地存储数据后还存在数据同步与重复存储的问题,第三方系统的数据过来后,主系统并不需要存储数据,只是提供展示和操作界面。

本文中,我们以常见的 REST API 通信为例,看看 Jmix 应用是如何直接使用外部数据的(这里我们不存储外部数据)。

外部数据源

我们假设外部数据源通过 REST API 提供关于项目(project)和任务(task)的 CRUD 接口。

定义 DTO 和 Service

首先,我们在主系统中定义两个 DTO 实体:
Project

Task
,用 Jmix Studio 可以直接创建 DTO 实体:

Studio 创建 DTO

然后,在主系统中我们需要定义两个 Services,专门用来对
Project

Task
实体进行 CRUD 操作,而这些操作里面,其实是调用了外部系统提供的 REST 接口,以
TaskService
为例:

@Component
public class TaskService {

    public static final String TASKS_BASE_URL = "http://localhost:18080/tasks";

    @Autowired
    private RestTemplate restTemplate;

    public List<Task> loadTasks() {
        Task[] tasks = restTemplate.getForObject(TASKS_BASE_URL, Task[].class);
        return Arrays.asList(tasks);
    }

    public Task saveTask(Task task) {
        String url = task.getId() != null ?
                TASKS_BASE_URL + "/"  + task.getId() :
                TASKS_BASE_URL;
        ResponseEntity<Task> response = restTemplate.postForEntity(url, task, Task.class);
        return response.getBody();
    }

    public void deleteTask(Task task) {
        restTemplate.delete(TASKS_BASE_URL + "/" + task.getId());
    }
}

Project
DTO 的创建过程和
ProjectService
的内容与上面步骤类似,这里就不再赘述。

第一种方式:使用代理

第一种方式是使用数据加载代理和提交代理方法,将原本使用
DataManager
进行数据加载和写入的相应方法替换为使用我们自定义的服务:

使用代理的方式

这里,我们选择
Task
DTO 和它的列表页和编辑页作为示例。

首先,在列表页添加数据加载的代理,在界面选中数据加载器后,双击代理方法中的
<empty>
标签,Studio 会自动生成方法并跳转到方法定义,添加自定义逻辑:

加载数据代理

然后,在编辑页添加数据提交代理,这里需要在 XML 中选中
data
节点,然后双击生成
commitDelegate

保存数据代理

这样就完成了我们需要实现的功能。是不是很简单?

第二种方式:自定义数据存储

Jmix 中,数据存储可以进行自定义,通过自定义的数据存储,可以像处理 JPA 实体一样,使用
DataManager
处理 DTO 实体。在检测到 DTO 实体关联到某个自定义存储后,
DataManager
会将 CRUD 操作都通过代理执行,并且能处理对 DTO 实体的引用。具体实现框架如下:

使用自定义数据存储的方式

可以看到,使用这种方式不需要对界面中的实体操作进行拦截,而是将所有对于外部系统的接口调用都交给
DataManager
通过数据存储进行分发。

这里,我们选择
Project
DTO 作为示例,创建相应的数据存储:

  1. 创建
    ProjectDataStore
    实现
    DataStore
    接口:
@Component("sample_ProjectDataStore")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ProjectDataStore implements DataStore {

    // 注入 ProjectService 用于具体的操作
    @Autowired
    private ProjectService projectService;

    // 后面需要实现接口中的方法,主要是通过 projectService 对数据进行 CRUD,这里省略。
    ...
}
  1. 创建一个实现
    StoreDescriptor
    接口的类。必须是一个 Spring 单例 bean,其中
    getBeanName()
    方法必须返回上一步创建的 bean 的名称:
@Component("sample_ProjectDataStoreDescriptor")
public class ProjectDataStoreDescriptor implements StoreDescriptor {

    @Override
    public String getBeanName() {
        return "sample_ProjectDataStore";
    }

    @Override
    public boolean isJpa() {
        return false;
    }
}

  1. application.properties
    中添加对数据存储的配置:
# 如果有多个,则以逗号分隔
jmix.core.additional-stores = projectds
# 配置名称为 jmix.core.storeDescriptor_<store_name>
jmix.core.store-descriptor_projectds = sample_ProjectDataStoreDescriptor


  1. Project
    实体添加
    @Store
    注解:
@Store(name = "projectds")
@JmixEntity
public class Project {
    ...
}

通过这几步,我们完成了数据存储的实现和配置。
Project
的列表页和编辑也不需要做任何改动,并且,任何通过
DataManager

Project
DTO 的操作就像操作 JPA 实体一样方便,可以在服务层和 UI 层调用。

结论

如果外部 API 提供了丰富的操作接口,比如 CRUD、分页、排序甚至支持某种查询语言,那么我们推荐创建一个自定义的数据存储。这种为数据操作 Service 提供自定义数据存储的方式,更贴近 Jmix 原生的开发方式。另外,如果需要的话,自定义的数据存储也可以继承
AbstractDataStore
类,这个类是 Jmix 内置
JpaDataStore
的父类。通过这个类派生可以使用框架提供的一些机制,比如数据访问安全和对外部数据的审计。

但是如果外部 API 只提供了几个简单的接口,这种情况我们建议直接在 UI 层使用数据读写代理的方式。

示例的完整代码请访问
GitHub

论文信息

论文标题:Efficient and Robust Pseudo-Labeling for Unsupervised Domain Adaptation
论文作者:Hochang Rhee、Nam Ik Cho

论文来源:2019——ICML

论文地址:download
论文代码:download
视屏讲解:click

1 摘要

问题:无监督域适应传统方法将超过一定置信度阈值的数据视为目标域的伪标记数据,因此选择合适的阈值会影响目标性能。

在本文中,提出了一种新的基于置信度的加权方案来获得伪标签,并提出了一种自适应阈值调整策略,以在整个训练过程中提供足够和准确的伪标签。 准确地说,基于置信度的加权方案根据置信度生成伪标签具有不同的贡献,这使得性能对阈值不太敏感。 此外,所提出的自适应阈值调整方法根据网络对目标域的适应程度来选择阈值,从而避免了对适当阈值的穷举搜索的需要。

2 介绍

背景

伪标记[21] 是对未标记数据进行人工标记,将无监督学习转化为半监督学习的过程。
本文遵循的前提,即具有高置信度输出的样本可以用作准确的标签 [22]、[23]
。 具体来说,置信度超过某个阈值的未标记样本被视为真实标签,称之为伪标签。 伪标记技术的性能在很大程度上依赖于阈值,其中选择合适的阈值对于实现高性能至关重要。 高阈值会导致更准确的伪标签,从而获得高性能,但也会导致标签不足。 另一方面,低阈值会导致大量伪标签,但也会因伪标签不准确而导致性能不佳。 因此,找到合适的阈值以实现高性能是伪标记中的一个主要问题。

此外,传统方法不是在整个训练过程中固定阈值,而是随着训练的进行不断增加阈值 [18]-[20]。 随着训练的进行,网络会适应目标域,这意味着与训练的早期阶段相比,将生成更多具有高置信度的伪标签。 当训练进行到一定程度时,即使阈值很高,网络也能够提供足够的伪标签。 因此,网络专注于通过在整个训练过程中增加阈值来生成准确的伪标签。然而,上述关于阈值选择的方法具有局限性。

首先,性能对阈值极其敏感,阈值的微小差异会产生巨大的负面影响。 进行详尽搜索以找到正确的阈值,这很耗时并且需要针对不同的数据集单独完成。其次,先前的工作根据源域训练的进度或网络的准确性调整阈值。尽管伪标签是从目标域样本生成的,但阈值对目标域没有依赖性。

为了克服传统方法的上述局限性,我们提出了一种基于置信度的加权方案来构建伪标签和一种自适应阈值调整方法。 我们基于置信度的加权方案限制了高阈值和低阈值的缺点。 我们为具有高置信度的伪标签赋予高权重,为低置信度赋予低权重。 因此,性能对阈值变得不那么敏感,从而避免了对适当阈值进行详尽搜索的需要。 此外,所提出的自适应阈值调整方法根据网络对目标域的适应程度来选择阈值。 阈值变得依赖于目标域,奖励目标域以在整个训练过程中生成足够和准确的伪标签。 通过所提出的方法,网络能够以自适应方式调整阈值,而无需穷举搜索。

3 方法

整体框架:

判别器的目标函数:

$\begin{array}{l}L_{D}= \underset{W^{D}}{\text{min}}\left(\sum_{x^{s} \in X_{s}} H\left(D\left(z^{s}\right), y^{s d}\right)\right.\left.+\sum_{x^{t} \in X_{t}} H\left(D\left(z^{t}\right), y^{t d}\right)\right) \\\end{array}   \quad\quad(1)$

其中,$y^{sd}$ 和  $y^{td}$ 是二分类域标签;

编码器的目标函数:

$L_{G}=\min _{W^{E}}\left(\sum_{x^{t} \in X_{t}} H\left(D\left(z^{t}\right), y^{s d}\right)\right)   \quad\quad(2)$

编码器 $\text{E}$ 和分类器 $\text{C}$ 的目标函数:

$\begin{array}{c}L_{C}=\underset{W^{E}, W^{C}}{\text{min}}  \left(\sum_{x^{s} \in X^{s}} H\left(C\left(z^{s}\right), y^{s}\right)\right.\left.+\sum_{\hat{x}^{t} \in \hat{X}_{t}} H\left(C\left(\hat{z}^{t}\right), \hat{y}^{t}\right)\right)\end{array}\quad\quad(3)$

4.1 基于置信度的加权方案

当通过类别损失训练分类器时,先前的工作 [18]-[20] 对每个伪标记样本的类别损失给出了相同的贡献,即使样本的置信度不同。 在高度置信度伪标签大多正确的前提下[22],[23],具有不同置信度的伪标签对类别损失的贡献应该不同。 具有高置信度的伪标签意味着匹配真实标签的概率很高,这应该对类别损失有很大贡献。 相反,低置信度的伪标签有被错误标记的风险,这意味着应该对类别损失给予低贡献以抑制风险。 因此,我们建议赋予伪标签与其置信度成正比的权重。

本文将完全置信的伪标签(置信度为 1.0 的标签)设置为 1.0 的权重,而根据经验将置信度为阈值的伪标签设置为与完全置信的伪标签相比具有一半的贡献。 也就是说,为伪标签分配权重为 0.5 的阈值置信度。 本文将伪标签的权重设置为与伪标签的置信度成线性比例:

$w\left(x^{t}\right)=\frac{0.5}{1-t h} \operatorname{con} f\left(x^{t}\right)+\frac{0.5-t h}{1-t h}\quad\quad(4)$

其中 $\operatorname{con} f\left(x^{t}\right)$ 表示从分类器导出的目标样本 $x_t$ 的置信度,$th$ 表示阈值。

考虑到伪标签的置信度,我们可以通过应用 $\text{Eq.4}$ 来修改等式 $\text{Eq.3}$ 以重新制定新的类别损失:

$\begin{aligned}L_{C}=  \min _{W^{E}, W^{C}}\left(\sum_{x^{s} \in X^{s}} H\left(C\left(z^{s}\right), y^{s}\right)\right. \left.+\sum_{\hat{x}^{t} \in \hat{X}_{t}} w\left(\hat{x}^{t}\right) H\left(C\left(\hat{z}^{t}\right), \hat{y}^{t}\right)\right)\end{aligned}  \quad\quad(5)$

通过伪标签的基于置信度的加权方案,我们能够减少低阈值和高阈值的缺点。 由低阈值生成的不准确的伪标签被赋予较小的权重,因此分类器受到的不准确性影响较小。 高阈值经历了伪标签的不足,由低阈值提供的伪标签补偿。 结果,网络的性能不会因伪标签的不准确和不足而显着下降,这意味着性能对阈值变得不那么敏感。

4.2 自适应阈值调整

为了关注训练早期伪标签的缺乏和后期伪标签的准确性,许多研究根据训练的进展增加阈值[19],[20]。 训练的进度由当前纪元或网络在源域样本上的准确性决定。 然而,这些术语不依赖于目标域,因此不能完全反映训练的进度。 因此,我们提出自适应阈值调整策略,根据模型对目标域的适应程度来设置阈值。 对目标域适应性低的模型不能对目标样本进行分类,这会导致输出置信度低,而适应性好的模型则输出置信度高。 因此,我们将模型对目标域的适应程度视为目标样本的平均置信度输出。 基于模型对目标域的置信度的修改阈值表示为:

$t h=\max \left(\frac{\sum_{x^{t} \in X_{t}} \operatorname{con} f\left(x^{t}\right)}{n_{t}}, \alpha\right)  \quad\quad(6)$

其中 $n_t$ 表示目标样本的数量,$\alpha = 0.7$ 表示最小阈值。 确定一个最小阈值,以防止在训练初期模型对目标域没有适应性时阈值过低。 太低的阈值会产生许多不准确的伪标签,这会阻止网络学习目标区分表示。 通过所提出的方法,网络可以自适应地选择合适的阈值,以在整个训练过程中保持伪标签的充分性和准确性。 结果,模型可以对各种数据集和模型容量具有鲁棒性。

4.3 总体目标函数

总体目标函数可以表示为

$L=  \underset{W^{E}, W^{D}, W^{C}}{\text{min}}  \left(L_{E}+L_{D}+\beta L_{C}\right)  \quad\quad(7)$

其中 β 是平衡参数。 编码器和鉴别器以对抗方式进行优化,将源样本和目标样本映射到公共域表示中。 编码器和分类器使用源和伪标记样本进行优化,以实现高分类性能。 详细的优化过程如算法 1 所示。

算法总结:

5 实验

可视化:

6 总结

image

我相信有不少小伙伴已经用过eureka,那么问题来了,Nacos是个啥?

看到这个标题,MySQL数据库与Nacos搭建监控服务,它们有什么关系么?

其实是Nacos支持连接MySQL,内部已配置好数据源、连接池供我们使用。如果使用其它数据源(比如信创要求,使用达梦数据库比较多),可以通过插件形式适配,模仿MySQL实现方式。具体如何实现,可参考 Nacos 的 github issues。

MySQL是什么?
一句话概括:一款社区活跃的开源数据库(database)软件,已被Oracle公司收购。

nacos是什么?

一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

nacos 官方文档:目前推荐2.x版本

https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

nacos 样例
http://console.nacos.io/nacos/index.html#/login

默认用户名与密码均为:nacos

image

Nacos部署

项目环境

Nacos 依赖
Java
环境来运行。如果你是一名开发人员,或许需要准备如下环境:

  • Maven 3.2.x+
  • 64 bit JDK1.8+
  • 64 bit OS,支持 Linux/Unix/Mac/Windows,
    推荐选用 Linux/Unix/Mac

主要以Linux平台作为演示环境
。关于nacos 的获取(发行包、源码包),可以前往github开源仓库。

nacos开源仓库:
https://github.com/alibaba/nacos

目前nacos最新稳定版是2.2.1:
https://github.com/alibaba/nacos/releases/tag/2.2.1

下载很缓慢,可以使用在线工具箱加速(公共免费资源请适度使用):
https://tool.mintimate.cn/gh/

官方推荐版本为2.1.1:

您可以在Nacos的release notes及博客中找到每个版本支持的功能的介绍,当前推荐的稳定版本为2.1.1。

快速开始

解压nacos

unzip nacos-server-2.2.0.zip & tar -zxvf nacos-server-2.2.0.tar.gz

启动服务

Linux平台以单机模式启动nacos服务

sh startup.sh -m standalone

Windows平台以单机模式启动nacos服务

startup.cmd -m standalone

访问:
http://127.0.0.1:8848/nacos
后,进入登录界面:

image

服务注册、发现以及配置管理

服务注册

curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=192.168.245.132&port=8080'

参数说明:

  • serviceName:服务名
  • ip:配置自己远程地址
  • port:端口(外部访问需要放通)

服务发现

curl -X GET 'http://127.0.0.1:8848/nacos/v1/ns/instance/list?serviceName=nacos.naming.serviceName'

发布配置

curl -X POST "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld"

参数说明:

  • dataId:数据Id。
  • group:组名。
  • content:发布配置输入的具体内容。

获取配置

curl -X GET "http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test"

控制台显示输出内容:HelloWorld,代表配置成功。

关于curl命令教程,可以去curl官网(curl.se)找一找,还是很常用的。

关闭服务

Linux平台:

sh shutdown.sh

Windows平台:

.\shutdown.cmd

nacos2.2.0版本配置说明

以nacos2.2.0版本为示例说明
nacos配置文件,支持配置文件存储到数据库(DB)中保存。如果没有开放mysql数据源相应设置,默认使用本地存储数据源。

配置目录:
\nacos-server-2.2.0\conf
,文件清单,每个版本可能略微有所不同:

  • application.properties
  • application.properties.example
  • derby-schema.sql
  • mysql-schema.sql
  • 1.4.0-ipv6_support-update.sql
  • cluster.conf.example
  • nacos-logback.xml

配置基本说明
以 .example 结尾,是示例配置,可供参考。以 update.sql 结尾的,则是sql更新脚本。

application.properties是nacos基本配置,例如端口、ip、数据源等等可以在此配置中修改,cluster.conf.example 是集群配置示例,nacos-logback.xml是日志相关配置。

mysql-schema.sql提供支持MySQL数据库SQL表结构,derby-schema.sql提供支持derby数据库SQL表结构。

如果和我一样使用的是MySQL数据库,
需要注意的配置文件是application.properties和mysql-schema.sql
,记住后续用得上。

导入表结构
新建数据库:库名命名为:nacos。编码最好指定为utf8mb4和utf8mb4_bin,避免导入后看到中文注释乱码。

create database nacos;

导入sql脚本到新建的库nacos中:mysql-schema.sql。库名并不是固定的,可以根据实际需求更改,比如nacos_config。如果你是从旧版本升级上来的,可能需要执行SQL脚本:
1.4.0-ipv6_support-update.sql

上面聊到 nacos 可以在配置文件中配置数据源,当然必不可少,需要部署MySQL数据库。

找到
application.properties
配置文件,配置MySQL数据源:

image

### If use MySQL as datasource: 
spring.datasource.platform=mysql                                                                           ### Count of DB:                                                                                             
db.num=1                                                                   
### Connect URL of DB:                                                                                   
db.url.0=jdbc:mysql://192.168.245.132:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC        
db.user.0=root                                                                                              db.password.0=mypwd@123        

很奇怪的一个问题,第一次使用,只导入初始化sql脚本MySQL数据源:

[root@Centos9-Stream ~]# cat /usr/local/nacos-2.2.0/conf/mysql-schema.sql | mysql -uroot -p nacos

nacos以单机模式启动:sh startup.sh -m standalone,启动无异常,正常访问。

MySQL数据库已设置自启,排查数据库正常启动。第二天启动centos-stream-9,再次启动 nacos 服务,出现无法设置数据源,尝试屏蔽数据源配置文件,启动正常。猜测可能是初始化脚本出问题了,执行升级sql脚本:1.4.0-ipv6_support-update.sql,再次启动nacos服务正常。

如果你和我一样,使用的是
MySQL8.0.30
版本数据库,可以参考。

如果遇到
Public Key Retrieval is not allowed
,可能还需要加上
&allowPublicKeyRetrieval=true

在github issues查找解决方案,提到可能是MySQL8.0.x设置时区的问题,将默认serverTimezone=UTC修改为
serverTimezone=Asia/Shanghai

如果配置好MySQL,但还是遇到数据源无法找到,也许是数据库导入脚本没有升级,也许是数据库版本和时区问题

尝试重连,反复提醒 Public Key Retrieval 不被允许,反复提醒数据源没有设置。

java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.

Caused by: com.mysql.cj.exceptions.CJException:
Public Key Retrieval is not allowed

Caused by: com.alibaba.nacos.api.exception.NacosException: Nacos Server did not start because dumpservice bean construction failure :
No DataSource set

Caused by: java.lang.IllegalStateException:
No DataSource set

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'externalDumpService': Invocation of init method failed; nested exception is ErrCode:500, ErrMsg:Nacos Server did not start because dumpservice bean construction failure :
No DataSource set

也有可能是MySQL版本问题和设置时区问题,连接配置做如下调整:

### Connect URL of DB:
db.url.0=jdbc:mysql://192.168.245.132:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode    =true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
db.user.0=root
db.password.0=mypwd@123

Linux终端执行命令导入升级脚本:

cat /usr/local/nacos-2.2.0/conf/1.4.0-ipv6_support-update.sql | mysql -uroot -p nacos

问题排查

关于nacos大部分问题
,可以在官方仓库的 issues 找到解决方案:

https://github.com/alibaba/nacos/issues

总结配置数据源相关问题

  1. 导入初始化sql脚本缺失字段问题。
  2. 使用数据源配置jdbc,数据库时区设置问题。
  3. MySQL8.0.x数据库默认密码使用缓存算法(caching_sha2_password)问题。

MySQL部署

tips:下载注意使用带有GA(General Availability)标识,稳定版。

实际工作中,使用比较到的是下载离线安装包。安装方式主要有如下几种分类:

安装方式

Windows平台

  1. msi文件:直接双击进行安装,有可视化界面,安装较为容易,但不够灵活。
  2. 归档包(archive):以zip格式进行压缩,类似于Linux中的二进制包。比较灵活,只需几个命令即可安装服务和实例化。
  3. 源码包(source package):最灵活,可根据需求编译安装功能,难易度最高。
  4. docker形式安装:其实是在容器中安装。

Linux平台

  1. rpm & deb 包安装:最为简单,但不灵活,适合初学者使用。
  2. 二进制包(binary package):也称归档包(archive),编译好的源码包,比rpm包更灵活。个人认为是安装多个服务最佳选择。
  3. 源码包(source package):最灵活,可根据需求编译安装功能,难易度最高。
  4. docker形式安装:其实是在容器中安装。

当然,MySQL同样支持macOS平台。

你也可以使用MySQL的妹妹MariaDB替代MySQL,是很好的选择。

由于测试使用,本人数据库使用比较新,MySQL8.0.30。说8.0新,其实也并不是很新,距离8.0第一个稳定版
Changes in MySQL 8.0.11 (2018-04-19, General Availability)
快5年了。

目前市面上主要以MySQL5.7为主,离停止维护不远了,未来应该会逐渐升级为MySQL8.0.x。看到官网将MySQL5.6文档页面转移了,已停止版本更新。

Windows 平台

此处省略安装过程。

请参考个人公众号里面的文章,有简易安装教程:SQL基础知识扫盲。

Linux平台(CentOS-Stream-9)部署MySQL

此处我只演示
Linux平台(CentOS-Stream-9)
其中最方便的一种部署方式。

Linux平台(CentOS-Stream-9)

通过RHEL系列自带的命令服务安装管理工具,yum与dnf二选一即可。

yum -y install mysql-server-8.0.30
dnf -y install mysql-server-8.0.30

安装后,查看mysql服务状态,默认服务名为mysqld.service。

systemctl status mysqld.service

默认没有启动,手动启动 mysqld 服务

systemctl start mysqld.service

登录mysql字符命令行界面

mysql -uroot -p

调试防火墙管理工具

如需远程登录,需要关闭防火墙相关服务,或者开放相应端口,个人建议采用开放相应端口

firewall-cmd --zone=public --add-port=3306/tcp --permanent

由于MySQL默认端口3306可能被黑客利用通用扫描软件攻击,建议实际工作中进行修改。或者利用隧道转发功能。
如果只是在本地使用,改与不改并不影响。

重载firewalld服务

firewall-cmd --reload

MySQL用户权限

设置开机自启

systemctl enable mysqld.service

查看mysqld服务状态:看到enabled代表设置为自启

systemctl status mysqld.service

如果使用第三方管理工具,需要开放相应用户权限才能登录MySQL:

修改用户密码

ALTER USER 'root'@'localhost' IDENTIFIED BY 'mypwd@123';

创建用户root,主机地址%,匹配所有;如果是localhost,则只让本地使用,也可以是指定ip地址。

CREATE USER 'root'@'%' IDENTIFIED BY 'mypwd@123';
CREATE USER 'root'@'localhost' IDENTIFIED BY 'mypwd@123';
CREATE USER 'root'@'192.168.245.132' IDENTIFIED BY 'mypwd@123';

授权root用户所有权限(ALL),即可使远程登录。同样可以指定特定权限,只给查询(select)。

GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;
GRANT ALL ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
GRANT ALL ON *.* TO 'root'@'192.168.245.132' WITH GRANT OPTION;

如果遇到第三方工具连接提示密码插件规则不被支持问题:

ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'mypwd@123';

刷新权限

flush privileges;

更多权限细化设置请参考MySQL8.0.x官方文档第6章节Security。关于权限控制和账号(用户)管理介绍很详细。

6.2 Access Control and Account Management

更多安装方式,你还可以参考个人持续更新的笔记:

https://blog.cnwangk.top/2023/03/15/MySQL8-0-x-简易安装教程

MySQL导入mysql-schema脚本

下面,将演示数据库创建以及
sql脚本导入

登录

mysql -uroot -p

创建数据库

create database nacos;

导入数据,执行sql脚本

关于数据导入,可以使用DB管理工具(MySQL workbench、DBeaver、SQLyog)连接导入,也可以使用cat或者zcat命令导入。

演示Linux平台使用cat命令导入sql脚本

[root@Centos9-Stream ~]# cat /usr/local/nacos-2.2.0/conf/mysql-schema.sql | mysql -uroot -p nacos

原本我也不知道这种用法,联想到以前使用过zabbix监控工具。灵机一动,我也可以将nacos的sql脚本这样导入数据库中。

查看nacos数据库表
:验证是否导入成功

mysql> use nacos;
mysql> show tables;

Springboot项目构建

如果你只是想体验nacos简单用法,可以跳过springboot项目集成nacos微服务

如果你是软件实施、运维人员,可以关注nacos官方文档运维监控文档(运维指南)。

介绍很详细,我也觉得我很啰嗦了,这已经属于开发范畴了。

目标
:项目中集成nacos服务,使用nacos监控到打包好并正常运行服务健康情况。集成spring-cloud-starter-alibaba-nacos-discovery和spring-cloud-starter-alibaba-nacos-config。nacos-discovery是服务发现组件,nacos-config则是服务配置组件。

项目环境

需要准备的环境

  • Maven 3.6.3
  • JDK17
  • Linux环境(CentOS9-Stream)

Linux平台安装JDK

解压JDK

tar -zxvf jdk-17.0.4.1_linux-x64_bin.tar.gz

配置全局环境变量:vim /etc/profile

JAVA_HOME=/usr/java/jdk17
CLASS_PATH=.:$JAVA_HOME/jre/lib/rt.jar:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
PATH=$PATH:$JAVA_HOME/bin
export JAVA_HOME CLASS_PATH PATH

配置当前用户环境变量:vim .bash_profile 或者 .bashrc,加入配置全局变量我所列出的内容即可。

执行 source 命令立即生效

source /etc/profile

关于环境变量更多配置可参考个人公众号关于JDK17的介绍。

验证版本:

java -version

Linux平台安装maven

yum -y install maven

当然,你也可以到Maven官网下载打包好的二进制包,解压即可用。

unzip apache-maven-3.6.3-bin.zip
mv apache-maven-3.6.3 /usr/local/

加入环境变量:

 # Maven Home
 MAVEN_HOME=/usr/local/apache-maven-3.6.3
 PATH=$PATH:$MAVEN_HOME/bin
 export MAVEN_HOME PATH

执行source命令立即生效
:source /etc/profile

查看版本

[root@Centos9-Stream bin]# mvn --version
Apache Maven 3.6.3 (Red Hat 3.6.3-15)
Maven home: /usr/share/maven
Java version: 17.0.4.1, vendor: Oracle Corporation, runtime: /usr/java/jdk17
Default locale: zh_CN, platform encoding: UTF-8
OS name: "linux", version: "5.14.0-148.el9.x86_64", arch: "amd64", family: "unix"

看到以上环境,证明你的Maven环境已经部署好了。

注意
:默认Maven镜仓库地址很慢,需要更换,推荐阿里云镜像源。

修改Maven配置文件:

[root@Centos9-Stream bin]# vim /usr/share/maven/conf/settings.xml

修改默认本地仓库存放目录
:找到localRepository,将如下内容开放:
image

<localRepository>/local/repo</localRepository>

配置Maven阿里镜像地址
,加入如下配置,
注意是在

加入

image

  <mirrors>
    <mirror>
          <id>aliyunmaven</id>
          <name>aliyun maven</name>
          <!-- 老版本url -->
          <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
          <!-- 新版本url -->
          <!--<url>https://maven.aliyun.com/repository/public/</url>-->
          <mirrorOf>central</mirrorOf>        
    </mirror>
  </mirrors>

项目构建

注意
:如果使用IDE工具构建,注意springboot、spring cloud以及spring cloud alibaba版本对应关系。尽量使用阿里云云原生应用脚手架官网推荐的版本,有助于后期遇到问题排查。当然还有一个好处,选择组件时有中文说明,一目了然。

如果你对于IDE工具使用,实在有困难,可以在线构建。

两种在线脚手架构建方式:

个人还是比较推荐使用阿里云云原生应用脚手架构建,便于在线选择nacos 等微服务相关组件。

image

springboot版本可以自己调试,目前已经支持springboot2.7.6和3.0.0配置。之前建议版本为springboot2.6.11,如果使用高版本,选择nacos相关组件时灰色的。

image

构建完成,如何打包成jar(目前使用比较多的时候jar包形式)?少不了JDK和Maven环境,参考环境准备步骤。老项目是有不少使用war包形式。

解压下载好的springboot项目demo.zip

unzip demo.zip
cd demo

执行Maven安装命令,这个过程可能有点缓慢,下载jar包越多,越耗时间:

[root@Centos9-Stream demo]# mvn install

如果新建的是单体应用,在根目录找到application.properties配置文件,如果构建mvc架构应用,则在demo-start子项目找到application.properties,然后修改spring.cloud.nacos.config.server-addr与spring.cloud.nacos.discovery.server-addr的IP地址。

示例如下

image

有点纳闷,也许是我本地环境问题,之前很少使用阿里脚手架构建项目。感觉使用阿里云云原生应用脚手架构建,会出现某些jar包(openfeign)找不到,使用Maven打包出现主清单文件找不到的问题。自己一步一步构建,添加微服务组件,配置nacos则是正常的。

Maven打包遇到找不到依赖:
image

示例如下,引入openfeign

image

如果不出意外,正常打包,可以使用 java -jar 执行jar服务

java -jar demo-web/target/demo-web-0.0.1-SNAPSHOT.jar

在nacos控制台服务管理可以监控到服务,比如我搭建测试环境,给应用区名称为:springboot-test。

Nacos联动测试

新建配置
:springboot-test.properties

image

配置详情
:配置的比较随意,参考看看就行

image

当然,你也需要在controller层 Java 代码里加入如下匹配:

/** 使用动态配置获取测试 --BEGIN-- **/
	
	@Value("${alibaba.config.discovery}")
	private String discovery;
	
	@Value("${alibaba.config.name}")
	private String name;

	@GetMapping("/getConfig")
	public String getConfig() {
		log.info("getConfig>>>>>>>>>>>");
		return "getConfig>>>>>>>>>>>>" + "发现:" + discovery + ">>>服务名称:" + name;
	}
	
	@Value("${custom.config.find}")
	private String find;
	
	@Value("${custom.config.say}")
	private String say;
	
	@GetMapping("/meet")
	public String meet() {
		log.info("meet>>>>>>>>>>>");
		return "meet>>>>>>>>>>>>" + "偶遇老湿:" + find + ">>>你好:" + say;
	}
	
	/** 使用动态配置获取测试 --END-- **/

提供一种思路。可以根据业务情况而定,将多个配置封装到一个类里面,如下所示,新建CustomConf类:

/**
 * desc:对象配置类
 * @Value 
 * @Component @ConfigurationProperties
 */
@Component
@ConfigurationProperties(prefix = "custom")
public class CustomConf {

	private Integer one;
	private Integer two;
	private Integer three;
	private String description;

    // 此处省略掉了 get set 方法,实际需要补上
}

使用到注解:

  • @Value :通常情况,使用注解取值。
  • @Component :加入注解,便于被扫描到。
  • @ConfigurationProperties:引入配置,通过prefix指定配置前缀。

在项目中注入:

    @Autowired
    private CustomConf custom;

	@RequestMapping("/custom")
    public String custom() {
        return "[custom] " + custom;
    }

准备好集成nacos服务的jar包:

[root@Centos9-Stream test]# ll springboot-nacos-cloud-0.0.1-SNAPSHOT.jar
-rw-rw-r--. 1 kart kart 77534357  2月 12 18:44 springboot-nacos-cloud-0.0.1-SNAPSHOT.jar

运行服务

nohup 代表脱离终端运行,&代表放入后台


[root@Centos9-Stream test]# nohup java -jar -Dspring.profiles.active=prod /opt/workspace/test/springboot-nacos-cloud-0.0.1-SNAPSHOT.jar  > /opt/
workspace/test/springboot-nacos.log 2>&1 &
[1] 4628

指定为生产环境:-Dspring.profiles.active=prod

测试接口:


http://192.168.245.132:8082/t/getConfig

你也可以使用curl命令请求:

curl -X GET http://192.168.245.132:8082/t/getConfig

得到输出结果:

getConfig>>>>>>>>>>>>发现:配置>>>服务名称:nacos服务

查看日志验证:

image

验证成功,getConfig日志打印出来了。

Nacos监控(控制台)

官方文档比我演示介绍要更详细,如果进一步使用,请参考官方文档(运维指南)。

控制台手册

https://nacos.io/zh-cn/docs/console-guide.html

基本功能演示

nacos 功能比较丰富

  • 配置管理:动态更新配置、查看历史版本、监听查询;
  • 服务管理:查看、创建服务、查看订阅者列表;
  • 权限列表:管理用户、角色、权限配置;
  • 命名空间:默认保留空间(public)
  • 集群管理:管理各个节点。

登录mysql,切换到nacos数据库
,查看原始users表只有一条数据,存储nacos用户。

此处只演示一下连接上MySQL数据库后,在控制台左侧菜单栏找到权限控制,新建用户。

示例如下

image

验证数据库表

验证users表
,发现多了一条数据,nacos_test成功存入到数据库,至此MySQL与nacos联动完成。

image

mysql> select * from users\G
*************************** 1. row ***************************
username: nacos
password: $2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu
 enabled: 1
*************************** 2. row ***************************
username: nacos_test
password: $2a$10$5F3NOcu2TFgdrBH0Av2iUuxbV9qBDihjmRt06163y5FTvPrErkWKS
 enabled: 1
2 rows in set (0.00 sec)

新增配置,对照数据库表

use nacos;
show tables;

image

  • 新增配置会保存在:nacos_config
  • 历史配置会保存在:his_config_info
  • 用户相关配置保存在:users
  • 角色相关配置保存在:roles
  • 权限相关配置保存在:permissions

监控方法可以配合prometheus以及grafana使用。关于nacos更多用法,目前还在摸索中。

下面这张流程图思路,可供参考:

image

参考资料

  1. nacos2.x官方文档:
    https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html
  2. nacos2.x github issues:
    https://github.com/nacos-group/nacos-docker/issues/251
  3. MySQL8.0.x官方文档第6章节Security:
    https://dev.mysql.com/doc/refman/8.0/en/access-control.html

最后,希望对你的工作有所帮助。如果觉得写得还不错,可以点个小小的赞。

以上总结仅供参考。

——END——

摘自
JavaGuide
(「Java学习+面试指南」一份涵盖大部分 Java 程序员所需要掌握的核心知识。准备 Java 面试,首选 JavaGuide!)

可能在看这个问题之前很多人和我一样并没有接触和使用过 OpenJDK 。那么 Oracle JDK 和 OpenJDK 之间是否存在重大差异?下面我通过收集到的一些资料,为你解答这个被很多人忽视的问题。

首先,2006 年 SUN 公司将 Java 开源,也就有了 OpenJDK。2009 年 Oracle 收购了 Sun 公司,于是自己在 OpenJDK 的基础上搞了一个 Oracle JDK。Oracle JDK 是不开源的,并且刚开始的几个版本还会相比于 OpenJDK 添加一些特有的功能和工具。

其次,对于 Java 7 而言,OpenJDK 和 Oracle JDK 是十分接近的。OpenJDK 项目主要基于 Sun 捐赠的 HotSpot 源代码。此外,OpenJDK 被选为 Java 7 的参考实现,由 Oracle 工程师参与维护。关于 OpenJDK 和 Oracle JDK 之间的区别,Oracle 博客帖子在 2012 年有一个更详细的答案:

问:OpenJDK 存储库中的源代码与用于构建 Oracle JDK 的代码之间有什么区别?

答:非常接近 - 我们的 Oracle JDK 版本构建过程基于 OpenJDK 7 构建,只添加了几个部分,例如部署代码,其中包括 Oracle 的 Java 插件和 Java WebStart 的实现,以及一些闭源的第三方组件,如图形光栅化器,一些开源的第三方组件,如 Rhino,以及一些零碎的东西,如附加文档或第三方字体。展望未来,我们的目的是开源 Oracle JDK 的所有部分,除了我们考虑商业功能的部分。

最后,简单总结一下 Oracle JDK 和 OpenJDK 的区别:

  1. 是否开源
    :OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是基于 OpenJDK 实现的,并不是完全开源的(个人观点:众所周知,JDK 原来是 SUN 公司开发的,后来 SUN 公司又卖给了 Oracle 公司,Oracle 公司以 Oracle 数据库而著名,而 Oracle 数据库又是闭源的,这个时候 Oracle 公司就不想完全开源了,但是原来的 SUN 公司又把 JDK 给开源了,如果这个时候 Oracle 收购回来之后就把他给闭源,必然会引起很多 Java 开发者的不满,导致大家对 Java 失去信心,那 Oracle 公司收购回来不就把 Java 烂在手里了吗!然后,Oracle 公司就想了个骚操作,这样吧,我把一部分核心代码开源出来给你们玩,并且我要和你们自己搞的 JDK 区分下,你们叫 OpenJDK,我叫 Oracle JDK,我发布我的,你们继续玩你们的,要是你们搞出来什么好玩的东西,我后续发布 Oracle JDK 也会拿来用一下,一举两得!)OpenJDK 开源项目:
    https://github.com/openjdk/jdk
  2. 是否免费
    :Oracle JDK 会提供免费版本,但一般有时间限制。JDK17 之后的版本可以免费分发和商用,但是仅有 3 年时间,3 年后无法免费商用。不过,JDK8u221 之前只要不升级可以无限期免费。OpenJDK 是完全免费的。
  3. 功能性
    :Oracle JDK 在 OpenJDK 的基础上添加了一些特有的功能和工具,比如 Java Flight Recorder(JFR,一种监控工具)、Java Mission Control(JMC,一种监控工具)等工具。不过,在 Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致,之前 OracleJDK 中的私有组件大多数也已经被捐赠给开源组织。
  4. 稳定性
    :OpenJDK 不提供 LTS 服务,而 OracleJDK 大概每三年都会推出一个 LTS 版进行长期支持。不过,很多公司都基于 OpenJDK 提供了对应的和 OracleJDK 周期相同的 LTS 版。因此,两者稳定性其实也是差不多的。
  5. 协议
    : Oracle JDK 使用 BCL/OTN 协议获得许可,而 OpenJDK 根据 GPL v2 许可获得许可。

既然 Oracle JDK 这么好,那为什么还要有 OpenJDK?

答:

  1. OpenJDK 是开源的,开源意味着你可以对它根据你自己的需要进行修改、优化,比如 Alibaba 基于 OpenJDK 开发了 Dragonwell8:
    https://github.com/alibaba/dragonwell8

  2. OpenJDK 是商业免费的(这也是为什么通过 yum 包管理器上默认安装的 JDK 是 OpenJDK 而不是 Oracle JDK)。虽然 Oracle JDK 也是商业免费(比如 JDK 8),但并不是所有版本都是免费的。

  3. OpenJDK 更新频率更快。Oracle JDK 一般是每 6 个月发布一个新版本,而 OpenJDK 一般是每 3 个月发布一个新版本。(现在你知道为啥 Oracle JDK 更稳定了吧,先在 OpenJDK 试试水,把大部分问题都解决掉了才在 Oracle JDK 上发布)

基于以上这些原因,OpenJDK 还是有存在的必要的!

Oracle JDK 和 OpenJDK 如何选择?

建议选择 OpenJDK 或者基于 OpenJDK 的发行版,比如 AWS 的 Amazon Corretto,阿里巴巴的 Alibaba Dragonwell。