Maven加强版 — mvnd的使用测试

InterviewCoder

# Maven 加强版 — mvnd 的使用测试

Maven、gradle 作为主流的构建工具,几乎所有的 Java 项目都使用,但是 Maven 相对 gradle 来说,构建还是太慢了。特别是构建十几个子项目的程序。

如果要把项目从 Maven 转换成 gradle,成本也是巨大的。

但是现在有了 maven-mvnd ,可以使构建变得更快。

# 1、maven-mvnd 介绍

maven-mvndApache Maven团队 借鉴了 GradleTakari 的优点,衍生出来的更快的构建工具,是 maven 的 强化版

github 地址:https://github.com/apache/maven-mvnd

maven-mvnd 特性:

  • 嵌入 Maven (所以不需要单独安装 Maven); maven 过渡到 maven-mvnd 的过程中实现 无缝切换 !所以不需要再安装 maven 或进行复杂的配置更改。
  • 实际的构建发生在一个长期存在的后台进程中,也就是守护进程。如果没有为构建请求服务的空闲守护进程,则可以并行产生多个守护进程。
  • 一个守护进程实例可以处理来自 mvnd 客户机的多个连续请求。
  • 使用 GraalVM 构建的本地可执行文件。与传统的 JVM 相比,它启动更快,使用的内存更少。

这种架构带来的优势有:

  • 运行实际构建的 JVM 不需要为每个构建重新启动,节省时间。
  • JVM 中的实时 (JIT) 编译器生成的本机代码也保留了下来。与 Maven 相比,JIT 编译花费的时间更少。在重复构建过程中,JIT 优化的代码可以立即使用。这不仅适用于来自 Maven 插件和 Maven Core 的代码,也适用于来自 JDK 本身的所有代码。

# 2、使用步骤

# 2.1、下载

下载:https://github.com/mvndaemon/mvnd/releases

img

我这里是 windows,下载 mvnd-0.7.1-windows-amd64.zip 版本即可。

# 2.2、安装

直接解压。

然后配置环境变量:将 bin 目录添加到 PATH

# 2.3、测试

打开 CMD 终端,输入 mvnd -v

可以看到如下信息表示安装成功:

1
2
3
4
5
6
7
8
C:\Users\HaC> mvnd -v
mvnd native client 0.7.1-windows-amd64 (97c587c11383a67b5bd0ff8388bd94c694b91c1e)
Terminal: org.jline.terminal.impl.jansi.win.JansiWinSysTerminal
Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739)
Maven home: E:\apache-mvnd-0.7.1-windows-amd64\mvn
Java version: 1.8.0_131, vendor: Oracle Corporation, runtime: E:\JDK1.8\jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

可以看到 mvnd 集成了 Maven 3.8.3 版本。

# 2.4、使用

在使用上和 Maven 一样,参数也一致。

Maven 使用 mvn clean package ;使用 Maven-mvnd 只需要变成 mvnd clean package 即可,其他同理。

# 2.5、配置修改

为了最小程度的兼容原来的 Maven,可以使用原来的 setting.xml

打开 Maven-mvnd 安装目录下 的 /conf/mvnd.properties 文件,修改:

1
maven.settings=E://apache-maven-3.5.4-bin//apache-maven-3.5.4//conf//settings.xml

(注意是 //

# 3、打包对比

由于 mvnd-0.7.1 版本使用了 Maven 3.8.3 版本,我这里同样使用 Maven 3.8.3 进行对比。

命令:

1
2
3
4
# maven 打包命令
mvn clean package -Dmaven.test.skip=true
# mvnd 打包命令
mvnd clean package -Dmaven.test.skip=true

电脑配置:

CPU:Intel® Core™ i7-4790 CPU @ 3.60GHz 3.60 GHz

内存:16GB

结果如下:

  • 13 个子项目

img41 秒 vs 21 秒

可以看到 mvnd 打包的总时间比 mvn 快了不少,因为 mvnd 使用了 CPU 的多核心,可以看到每个子模块打包的时间都差不多,所以在单核的机器,就不要尝试使用 mvnd 了。

  • 19 个子项目

img32 秒 vs 10 秒

呈现子项目越多,相对速度更快的趋势。

总的来说:

如果项目模块很多,可以尝试使用 mvnd 进行辅助打包,比如 测试、生产,可以节省很多时间;

开发则可以继续使用 mvn ,毕竟 IDEA 无法集成 mvnd,可以在 terminal 通过命令打包。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【SqlServer】SqlServer查询今日和昨日数据

InterviewCoder

# 【SqlServer】SqlServer 查询今日和昨日数据

在 where 后加入以下 sql 语句

今天:

1
where DateDiff(dd,时间字段,getdate())=0

昨天:

1
where DateDiff(dd,时间字段,getdate())=1

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Elasticsearch学习

InterviewCoder

# Elasticsearch

img

# 目标

  • 了解 ES 中的基本概念
  • 掌握 RESTFul 操作 ES 的 CRUD
  • 掌握 Spring Data Elasticsearch 操作 ES

# 简介

Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。我们希望搜索解决方案要运行速度快,我们希望能有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用 JSON 通过 HTTP 来索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并扩展到数百台,我们要实时搜索,我们要简单的多用户,我们希望建立一个云的解决方案。因此我们利用 Elasticsearch 来解决所有这些问题及可能出现的更多其它问题。

# 安装和运行

该软件是基于 Java 编写的解压即用的软件,只需要有 Java 的运行环境即可,把压缩包解压后,进入到 bin 目录运行 elasticsearch.bat,出现以下界面,表示成功启动服务器

img

浏览器输入:localhost:9200,看到浏览器输出服务器的信息,表示安装成功,可以使用了

img

注意:程序启动后有两个端口 9200 和 9300,9200 端口用于 HTTP 协议,基于 RESTFul 来使用,9300 端口用于 TCP 协议,基于 jar 包来使用

# 后台启动

使用安装目录 /bin/elasticsearch-service.bat 程序可以把 Elasticsearch 安装后服务列表中,以后我们可以在服务列表来启动该程序,也可以设置成开机启动模式

注意:设置后台启动需要手动配置 Java 虚拟机的路径,使用命令 elasticsearch-service.bat manager 来配置

img

# 安装 head 插件

Elasticsearch 默认的客户端工具是命令行形式的,操作起来不方便,也不直观看到数据的展示,所以我们需要去安装一个可视化插件,但是这些插件都是基于 H5 开发的,在谷歌的应用商店中找到 elasticsearch-head 插件,然后安装,使用该插件能比较直观的展示服务器中的数据

# 安装 kibana

该软件也是解压即用的工具,用于管理和监控 Elasticsearch 的运作,同时内部包含了客户端工具,支持 RESTFul 操作 Elasticsearch。解压后运行 bin/kibana.bat,看到启动成功的端口号即可以使用浏览器来使用了

img

浏览器输入:http://localhost:5601

# 概念名词

# 数据存储图

img

注意:从 Elasticsearch6 开始一个索引里面只能有一个类型,后续计划删除类型这个概念,从 ES6 开始一般让索引名称和类型名称一致

# 主要组件

  • 索引

    ES 将数据存储于一个或多个索引中,索引是具有类似特性的文档的集合。类比传统的关系型数据库领域来说,索引相当于 SQL 中的一个数据库,或者一个数据存储方案 (schema)。索引由其名称 (必须为全小写字符) 进行标识,并通过引用此名称完成文档的创建、搜索、更新及删除操作。一个 ES 集群中可以按需创建任意数目的索引。

  • 类型

    类型是索引内部的逻辑分区 (category/partition),然而其意义完全取决于用户需求。因此,一个索引内部可定义一个或多个类型 (type)。一般来说,类型就是为那些拥有相同的域的文档做的预定义。例如,在索引中,可以定义一个用于存储用户数据的类型,一个存储日志数据的类型,以及一个存储评论数据的类型。类比传统的关系型数据库领域来说,类型相当于表。

  • 映射

    Mapping, 就是对索引库中索引的字段名称及其数据类型进行定义,类似于 mysql 中的表结构信息。不过 es 的 mapping 比数据库灵活很多,它可以动态识别字段。一般不需要指定 mapping 都可以,因为 es 会自动根据数据格式识别它的类型,如果你需要对某些字段添加特殊属性(如:定义使用其它分词器、是否分词、是否存储等),就必须手动添加 mapping。

    需要注意的是映射是不可修改的,一旦确定就不允许改动,在使用自动识别功能时,会以第一个存入的文档为参考来建立映射,后面存入的文档也必须符合该映射才能存入

  • 文档

    文档是 Lucene 索引和搜索的原子单位,它是包含了一个或多个域的容器,基于 JSON 格式进行表示。文档由一个或多个域组成,每个域拥有一个名字及一个或多个值,有多个值的域通常称为多值域。每个文档可以存储不同的域集,但同一类型下的文档至应该有某种程度上的相似之处。

# 分片和副本

ES 的分片 (shard) 机制可将一个索引内部的数据分布地存储于多个节点,它通过将一个索引切分为多个底层物理的 Lucene 索引完成索引数据的分割存储功能,这每一个物理的 Lucene 索引称为一个分片 (shard)。每个分片其内部都是一个全功能且独立的索引,因此可由集群中的任何主机存储。创建索引时,用户可指定其分片的数量,默认数量为 5 个。

Shard 有两种类型:primary 和 replica,即主 shard 及副本 shard。Primary shard 用于文档存储,每个新的索引会自动创建 5 个 Primary shard,当然此数量可在索引创建之前通过配置自行定义,不过,一旦创建完成,其 Primary shard 的数量将不可更改。Replica shard 是 Primary Shard 的副本,用于冗余数据及提高搜索性能。每个 Primary shard 默认配置了一个 Replica shard,但也可以配置多个,且其数量可动态更改。ES 会根据需要自动增加或减少这些 Replica shard 的数量。

# 分词器

把文本内容按照标准进行切分,默认的是 standard,该分词器按照单词切分,内容转变为小写,去掉标点,遇到每个中文字符都当成 1 个单词处理,后面会安装开源的中文分词器插件(ik)

# 感受分词器效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
先创建一个名叫shop_product的索引,然后再感受分词效果
PUT /shop_product

默认分词器:
GET /shop_product/_analyze
{
"text":"I am Groot"
}

GET /shop_product/_analyze
{
"text":"英特尔酷睿i7处理器"
}
结论:默认的分词器只能对英文正常分词,不能对中文正常分词

# 安装 IK 分词器

直接把压缩文件中的内容解压,然后放在 elasticsearch/plugins 下,然后重启即可

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
IK分词器:
ik_smart:粗力度分词
ik_max_word:细力度分词

GET /shop_product/_analyze
{
"text":"I am Groot",
"analyzer":"ik_smart"
}

GET /shop_product/_analyze
{
"text":"英特尔酷睿i7处理器",
"analyzer":"ik_smart"
}

GET /shop_product/_analyze
{
"text":"英特尔酷睿i7处理器",
"analyzer":"ik_max_word"
}

结论:都能正常分词

# 拓展词库

最简单的方式就是找到 IK 插件中的 config/main.dic 文件,往里面添加新的词汇,然后重启服务器即可

# 倒排索引

img

# 基本操作(了解)

# 索引操作

建表索引,相当于在是在建立数据库

# 建立索引

1
2
3
4
5
6
7
8
9
语法:PUT /索引名
在没有特殊设置的情况下,默认有5个分片,1个备份,也可以通过请求参数的方式来指定
参数格式:
{
"settings": {
"number_of_shards": 5, //设置5个片区
"number_of_replicas": 1 //设置1个备份
}
}

# 删除索引

1
语法:DELETE /索引名

# 映射操作

# 建立索引和映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
语法:PUT /索引名
{
"mappings": {
类型名: {
"properties..0": {
字段名: {
"type": 字段类型,
"analyzer": 分词器类型,
"search_analyzer": 分词器类型,
...
},
...
}
}
}
}

字段类型:double / long / integer / text / keyword / date / binary
注意:text和keyword都是字符串类型,但是只有text类型的数据才能分词,字段的配置一旦确定就不能更改
映射的配置项有很多,我们可以根据需要只配置用得上的属性

# 查询映射

1
语法:GET /索引名/_mapping

# CRUD 操作

# 文档操作

# 新增和替换文档

1
2
3
4
5
6
7
8
9
10
11
12
13
语法:PUT /索引名/类型名/文档ID
{
field1: value1,
field2: value2,
...
}

注意:当索引/类型/映射不存在时,会使用默认设置自动添加
ES中的数据一般是从别的数据库导入的,所以文档的ID会沿用原数据库中的ID
索引库中没有该ID对应的文档时则新增,拥有该ID对应的文档时则替换

需求1:新增一个文档
需求2:替换一个文档

每一个文档都内置以下字段

_index:所属索引

_type:所属类型

_id:文档 ID

_version:乐观锁版本号

_source:数据内容

# 查询文档

1
2
3
4
5
6
语法:
根据ID查询 -> GET /索引名/类型名/文档ID
查询所有(基本查询语句) -> GET /索引名/类型名/_search

需求1:根据文档ID查询一个文档
需求2:查询所有的文档

查询所有结果中包含以下字段

took:耗时

_shards.total:分片总数

hits.total:查询到的数量

hits.max_score:最大匹配度

hits.hits:查询到的结果

hits.hits._score:匹配度

# 删除文档

1
2
3
4
5
语法:DELETE /索引名/类型名/文档ID
注意:这里的删除并且不是真正意义上的删除,仅仅是清空文档内容而已,并且标记该文档的状态为删除

需求1:根据文档ID删除一个文档
需求2:替换刚刚删除的文档

# 高级查询

数据准备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
PUT /shop_product
{
"mappings": {
"shop_product": {
"properties": {
"id": {
"type": "integer"
},
"title":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"price":{
"type": "double"
},
"intro":{
"type": "text",
"analyzer": "ik_smart",
"search_analyzer": "ik_smart"
},
"brand":{
"type": "keyword"
}
}
}
}
}

POST /shop_product/shop_product/_bulk
{"create":{"_id": 1}}
{"id":1,"title":"Apple iPhone XR (A2108) 128GB 白色 移动联通电信4G手机 双卡双待","price":5299,"intro":"【iPhoneXR限时特惠!】6.1英寸视网膜显示屏,A12仿生芯片,面容识别,无线充电,支持双卡!选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 2}}
{"id":2,"title":"Apple 2019款 Macbook Pro 13.3【带触控栏】八代i7 18G 256G RP645显卡 深空灰 苹果笔记本电脑 轻薄本 MUHN2CH/A","price":15299,"intro":"【八月精选】Pro2019年新品上市送三重好礼,现在购买领满8000减400元优惠神劵,劵后更优惠!","brand":"Apple"}
{"create":{"_id": 3}}
{"id":3,"title":"Apple iPad Air 3 2019年新款平板电脑 10.5英寸(64G WLAN版/A12芯片/Retina显示屏/MUUL2CH/A)金色","price":3788,"intro":"8月尊享好礼!买iPad即送蓝牙耳机!领券立减!多款产品支持手写笔!【新一代iPad,总有一款适合你】选【换修无忧版】获 AppleCare 原厂服务,享只换不修!更有快速换机、保值换新、轻松月付!","brand":"Apple"}
{"create":{"_id": 4}}
{"id":4,"title":"华为HUAWEI MateBook X Pro 2019款 英特尔酷睿i5 13.9英寸全面屏轻薄笔记本电脑(i5 8G 512G 3K 触控) 灰","price":7999,"intro":"3K全面屏开启无界视野;轻薄设计灵动有型,HuaweiShare一碰传","brand":"华为"}
{"create":{"_id": 5}}
{"id":5,"title":"华为 HUAWEI Mate20 X (5G) 7nm工艺5G旗舰芯片全面屏超大广角徕卡三摄8GB+256GB翡冷翠5G双模全网通手机","price":6199,"intro":"【5G双模,支持SA/NSA网络,7.2英寸全景巨屏,石墨烯液冷散热】5G先驱,极速体验。","brand":"华为"}
{"create":{"_id": 6}}
{"id":6,"title":"华为平板 M6 10.8英寸麒麟980影音娱乐平板电脑4GB+64GB WiFi(香槟金)","price":2299,"intro":"【华为暑期购】8月2日-4日,M5青春版指定爆款型号优惠100元,AI语音控制","brand":"华为"}
{"create":{"_id": 7}}
{"id":7,"title":"荣耀20 PRO DXOMARK全球第二高分 4800万四摄 双光学防抖 麒麟980 全网通4G 8GB+128GB 蓝水翡翠 拍照手机","price":3199,"intro":"白条6期免息!麒麟980,4800万全焦段AI四摄!荣耀20系列2699起,4800万超广角AI四摄!","brand":"荣耀"}
{"create":{"_id": 8}}
{"id":8,"title":"荣耀MagicBook Pro 16.1英寸全面屏轻薄性能笔记本电脑(酷睿i7 8G 512G MX250 IPS FHD 指纹解锁)冰河银","price":6199,"intro":"16.1英寸无界全面屏金属轻薄本,100%sRGB色域,全高清IPS防眩光护眼屏,14小时长续航,指纹一健开机登录,魔法一碰传高速传输。","brand":"荣耀"}
{"create":{"_id": 9}}
{"id":9,"title":"荣耀平板5 麒麟8核芯片 GT游戏加速 4G+128G 10.1英寸全高清屏影音平板电脑 WiFi版 冰川蓝","price":1549,"intro":"【爆款平板推荐】哈曼卡顿专业调音,10.1英寸全高清大屏,双喇叭立体环绕音,配置多重护眼,值得拥有!","brand":"荣耀"}
{"create":{"_id": 10}}
{"id":10,"title":"小米9 4800万超广角三摄 6GB+128GB全息幻彩蓝 骁龙855 全网通4G 双卡双待 水滴全面屏拍照智能游戏手机","price":2799,"intro":"限时优惠200,成交价2799!索尼4800万广角微距三摄,屏下指纹解锁!","brand":"小米"}
{"create":{"_id": 11}}
{"id":11,"title":"小米(MI)Pro 2019款 15.6英寸金属轻薄笔记本(第八代英特尔酷睿i7-8550U 16G 512GSSD MX250 2G独显) 深空灰","price":6899,"intro":"【PCIE固态硬盘、72%NTSC高色域全高清屏】B面康宁玻璃覆盖、16G双通道大内存、第八代酷睿I7处理器、专业级调校MX150","brand":"小米"}
{"create":{"_id": 12}}
{"id":12,"title":"联想(Lenovo)拯救者Y7000P 2019英特尔酷睿i7 15.6英寸游戏笔记本电脑(i7 9750H 16G 1T SSD GTX1660Ti 144Hz)","price":9299,"intro":"超大1T固态,升级双通道16G内存一步到位,GTX1660Ti电竞级独显,英特尔9代i7H高性能处理器,144Hz电竞屏窄边框!","brand":"联想"}

Elasticsearch 基于 JSON 提供完整的查询 DSL(Domain Specific Language:领域特定语言)来定义查询。

1
2
基本语法:
GET /索引名/类型名/_search

一般都是需要配合查询参数来使用的,配合不同的参数有不同的查询效果

参数配置项可以参考博客:https://www.jianshu.com/p/6333940621ec

# 结果排序

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
参数格式:
{
"sort": [
{field: 排序规则},
...
]
}

排序
GET /shop_product/shop_product/_search
{
"sort": [
{
"price": {
"order": "desc"
}
}
]
}



排序规则:
asc表示升序
desc:表示降序
没有配置排序的情况下,默认按照评分降序排列

# 分页查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
参数格式:
{
"from": start,
"size": pageSize
}

分页 从第几个开始 每页几个
GET /shop_product/shop_product/_search
{
"from": 2,
"size": 4,
"sort": [
{
"price": {
"order": "desc"
}
}
]
}

需求1:查询所有文档按照价格降序排列
需求2:分页查询文档按照价格降序排列,显示第2页,每页显示3个

# 检索查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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
参数格式:
{
"query": {
检索方式: {field: value}
}
}

检索查询 全文检索
需求1:查询商品标题中符合"游戏 手机"的字样的商品
GET /shop_product/shop_product/_search
{
"query": {
"match": {
"title": "游戏 手机"
}
}
}


检索查询 精准匹配
需求2:查询商品价格等于15299的商品
GET /shop_product/shop_product/_search
{
"query": {
"term": {
"price": 15299
}
}
}

检索查询 范围检索
需求3:查询商品价格在5000~10000之间商品,按照价格升序排列
GET /shop_product/shop_product/_search
{
"query": {
"range": {
"price": {
"gte": 5000,
"lte": 10000
}
}
},
"sort": [
{
"price": {
"order": "asc"
}
}
]
}



检索方式:
term表示精确匹配,value值不会被分词器拆分,按照倒排索引匹配
match表示全文检索,value值会被分词器拆分,然后去倒排索引中匹配
range表示范围检索,其value值是一个对象,如{ "range": {field: {比较规则: value, ...}} }
比较规则有gt / gte / lt / lte 等

注意:term和match都能用在数值和字符上,range用在数值上

需求1:查询商品标题中符合"游戏 手机"的字样的商品
需求2:查询商品价格等于15299的商品
需求3:查询商品价格在5000~10000之间商品,按照价格升序排列

# 关键字查询

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
参数格式:
{
"query": {
"multi_match": {
"query": value,
"fields": [field1, field2, ...]
}
}
}

关键字查询
需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品
GET /shop_product/shop_product/_search
{
"query": {
"multi_match": {
"query": "蓝牙 指纹 双卡",
"fields": ["title", "intro"]
}
}
}

multi_match:表示在多个字段间做检索,只要其中一个字段满足条件就能查询出来,多用在字段上

需求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
参数格式:
{
"query": { ... },
"highlight": {
"fields": {
field1: {},
field2: {},
...
},
"pre_tags": 开始标签,
"post_tags" 结束标签
}
}

高亮显示
需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示
GET /shop_product/shop_product/_search
{
"query": {
"multi_match": {
"query": "蓝牙 指纹 双卡",
"fields": ["title", "intro"]
}
},
"highlight": {
"fields": {
"title": {},
"intro": {}
},
"pre_tags":"<h1>",
"post_tags":"</h1>"
}
}

highlight:表示高亮显示,需要在fields中配置哪些字段中检索到该内容需要高亮显示
必须配合检索(term / match)一起使用

需求1:查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示

# 逻辑查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
参数格式: 
{
"query": {
"bool": {
逻辑规则: [
{检索方式: {field: value}},
...
],
...
}
}
}

逻辑查询
需求1:查询商品标题中符合"i7"的字样并且价格大于7000的商品
GET /shop_product/shop_product/_search
{
"query": {
"bool": {
"must": [
{"match": {
"title": "i7"
}},
{"range": {
"price": {
"gt": 7000
}
}}
]
}
}
}


逻辑查询
需求2:查询商品标题中符合"pro"的字样或者价格在1000~3000的商品
GET /shop_product/shop_product/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": "pro"
}},
{"range": {
"price": {
"gte": 1000,
"lte": 3000
}
}}
]
}
}
}

逻辑规则:must / should / must_not,相当于and / or / not

需求1:查询商品标题中符合"i7"的字样并且价格大于7000的商品
需求2:查询商品标题中符合"pro"的字样或者价格在1000~3000的商品

# 过滤查询

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
参数格式:
{
"query": {
"bool": {
"filter": [
{ 检索方式: { field: value } },
...
]
}
}
}

过滤查询 不评分
GET /shop_product/shop_product/_search
{
"query": {
"bool": {
"filter": [
{"match": {
"title": "pro"
}}
]
}
}
}



从效果上讲过滤查询和检索查询能做一样的效果
区别在于过滤查询不评分,结果能缓存,检索查询要评分,结果不缓存
一般是不会直接使用过滤查询,都是在检索了一定数据的基础上再使用

关于 filter 的更多认知推荐大家读这篇博客:https://blog.csdn.net/laoyang360/article/details/80468757

# 分组查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
参数格式:
{
"size": 0,
"aggs": {
自定义分组字段: {
"terms": {
"field": 分组字段,
"order": {自定义统计字段:排序规则},
"size": 10 //默认显示10组
},
"aggs": { //分组后的统计查询,相当于MySQL分组函数查询
自定义统计字段: {
分组运算: {
"field": 统计字段
}
}
}
}
}
}

分组查询
需求1:按照品牌分组,统计各品牌的数量
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"brand_group": {
"terms": {
"field": "brand",
"order": {"brand_group":"desc"},
"size": 10
},
"aggs": {
"brand_group": {
"value_count": {
"field": "id"
}
}
}
}
}
}

分组查询
需求2:按照品牌分组,统计各品牌的平均价格
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"brand_group": {
"terms": {
"field": "brand",
"order": {"brand_group":"desc"},
"size": 10
},
"aggs": {
"brand_group": {
"avg": {
"field": "price"
}
}
}
}
}
}


分组查询
需求3:按照品牌分组,统计各品牌的价格数据
GET /shop_product/shop_product/_search
{
"size": 0,
"aggs": {
"brand_group_aggs": {
"terms": {
"field": "brand",
"order": {"brand_group_aggs.count":"desc"},
"size": 10
},
"aggs": {
"brand_group_aggs": {
"stats": {
"field": "price"
}
}
}
}
}
}



分组运算:avg / sum / min / max / value_count / stats(执行以上所有功能的)
注意:这里是size=0其目的是为了不要显示hit内容,专注点放在观察分组上

需求1:按照品牌分组,统计各品牌的数量
需求2:按照品牌分组,统计各品牌的平均价格
需求3:按照品牌分组,统计各品牌的价格数据

# 批处理(了解)

当需要集中的批量处理文档时,如果依然使用传统的操作单个 API 的方式,将会浪费大量网络资源,Elasticsearch 为了提高操作的性能,专门提供了批处理的 API

# mget 批量查询

1
2
3
4
5
6
7
8
语法:
GET /索引名/类型/_mget
{
"docs": [
{"_id": 文档ID},
...
]
}

# bulk 批量增删改

1
2
3
4
5
6
7
8
9
10
11
语法:
POST /索引名/类型/_bulk
{动作:{"_id": 文档ID}}
{...}
{动作:{"_id": 文档ID}}
{...}

动作:create / update / delete,其中delete只有1行JSON,其他操作都是有2行JSON,并且JSON不能格式化,如果是update动作,它的数据需要加个key为doc
如:
{"update": {"_id": xx}}
{"doc": {"xx":xx, "xx":xx}}

# Spring Data Elasticsearch

# 准备环境

# 导入依赖

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>

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

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

<!--SpringBoot整合Spring Data Elasticsearch的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

# 编写 DOMAIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
@Document:配置操作哪个索引下的哪个类型
@Id:标记文档ID字段
@Field:配置映射信息,如:分词器
*/
@Getter@Setter@ToString
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName="shop_product", type="shop_product")
public class Product {
@Id
private String id;

@Field(analyzer="ik_max_word",searchAnalyzer="ik_max_word", type=FieldType.Text)
private String title;

private Integer price;

@Field(analyzer="ik_max_word",searchAnalyzer="ik_max_word", type=FieldType.Text)
private String intro;

@Field(type=FieldType.Keyword)
private String brand;
}

# 配置连接信息

1
2
3
4
5
#application.properties
# 配置集群名称,名称写错会连不上服务器,默认elasticsearch
spring.data.elasticsearch.cluster-name=elasticsearch
# 配置集群节点
spring.data.elasticsearch.cluster-nodes=localhost:9300

# ElasticsearchRepository

该接口是框架封装的用于操作 Elastsearch 的高级接口,只要我们自己的写个接口去继承该接口就能直接对 Elasticsearch 进行 CRUD 操作

1
2
3
4
5
6
7
8
9
/**
泛型1:domain的类型
泛型2:文档主键类型
该接口直接该给Spring,底层会使用JDK代理的方式创建对象,交给容器管理
*/
@Repository
public interface ProductESRepository extends ElasticsearchRepository<Product, String> {
// 符合Spring Data规范的高级查询方法
}

# 完成 CRUD + 分页 + 排序

# 组件介绍

1
2
3
4
5
6
7
8
9
10
11
ElasticsearchRepository:框架封装的用于便捷完成常用操作的工具接口
ElasticsearchTemplate:框架封装的用于便捷操作Elasticsearch的模板类

NativeSearchQueryBuilder:用于生成查询条件的构建器,需要去封装各种查询条件
QueryBuilder:该接口表示一个查询条件,其对象可以通过QueryBuilders工具类中的方法快速生成各种条件
boolQuery():生成bool条件,相当于 "bool": { }
matchQuery():生成match条件,相当于 "match": { }
rangeQuery():生成range条件,相当于 "range": { }
AbstractAggregationBuilder:用于生成分组查询的构建器,其对象通过AggregationBuilders工具类生成
Pageable:表示分页参数,对象通过PageRequest.of(页数, 容量)获取
SortBuilder:排序构建器,对象通过SortBuilders.fieldSort(字段).order(规则)获取

# ElasticsearchTemplate

该模板类,封装了便捷操作 Elasticsearch 的模板方法,包括 索引 / 映射 / CRUD 等底层操作和高级操作,该对象用起来会略微复杂些,尤其是对于查询,还需要把查询到的结果自己封装对象

1
2
3
//该对象已经由SpringBoot完成自动配置,直接注入即可
@Autowired
private ElasticsearchTemplate template;

一般情况下,ElasticsearchTemplate 和 ElasticsearchRepository 是分工合作的,ElasticsearchRepository 已经能完成绝大部分的功能,如果遇到复杂的查询则要使用 ElasticsearchTemplate,如多字段分组、高亮显示等

# 实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@Autowired
private ElasticsearchTemplate template;

@Autowired
private ProductESRepository repository;

// 新增或者覆盖一个文档
@Test
public void testSaveOrUpdate() throws Exception {
// 索引库中不存在则新增,存在则覆盖
Product p = new Product("13", "华为手环 B5(Android 运动手环)", 999,
"高清彩屏 腕上蓝牙耳机 心率检测 来电消息提醒", "华为");
repostitory.save(p);
}

// 删除一个文档
@Test
public void testDelete() throws Exception {
repostitory.deleteById("13");
}

// 根据ID查询一个文档
@Test
public void testGet() throws Exception {
Optional<Product> optional = repostitory.findById("1");
optional.ifPresent(System.out::println);
}

// 查询所有文档
@Test
public void testList() throws Exception {
Iterable<Product> iter = repostitory.findAll();
iter.forEach(System.out::println);
}

// 分页查询文档按照价格降序排列,显示第2页,每页显示3个
@Test
public void testQuery1() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 设置分页信息
builder.withPageable(PageRequest.of(1, 3));
// 设置排序信息
builder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));

Page<Product> page = repostitory.search(builder.build());
System.out.println(page.getTotalElements()); //总记录数
System.out.println(page.getTotalPages()); //总页数
page.forEach(System.out::println);
}

// 查询商品标题中符合"游戏 手机"的字样的商品
@Test
public void testQuery2() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.matchQuery("title", "游戏 手机")
);
Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 查询商品价格等于15299的商品
@Test
public void testQuery3() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.termQuery("price", 15299)
);
Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 查询商品价格在5000~9000之间商品,按照价格升序排列
@Test
public void testQuery4() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.rangeQuery("price")
.gte(5000).lte(9000)
);
builder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品
@Test
public void testQuery5() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡",
"title", "intro")
);
Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 查询商品标题中符合"i7"的字样并且价格大于7000的商品
@Test
public void testQuery6() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("title", "i7"))
.must(QueryBuilders.rangeQuery("price").gt(7000))
);

Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 查询商品标题中符合"pro"的字样或者价格在1000~3000的商品
@Test
public void testQuery7() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.withQuery(
QueryBuilders.boolQuery()
.should(QueryBuilders.matchQuery("title", "pro"))
.should(QueryBuilders.rangeQuery("price").gte(1000).lte(3000))
);
Page<Product> page = repostitory.search(builder.build());
page.forEach(System.out::println);
}

// 按照品牌分组,统计各品牌的数量
@Test
public void testQuery8() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand")
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repostitory.search(builder.build());
// 获取自定义的分组字段
StringTerms brand = (StringTerms) page.getAggregation("groupByBrand");
brand.getBuckets().forEach(bucket -> System.out.println(bucket.getDocCount()));
}

// 按照品牌分组,统计各品牌的平均价格
@Test
public void testQuery9() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand")
.subAggregation(AggregationBuilders.avg("avgPrice").field("price"))
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repostitory.search(builder.build());
// 获取自定义的分组字段
StringTerms brand = (StringTerms) page.getAggregation("groupByBrand");
brand.getBuckets().forEach(bucket -> {
InternalAvg avgPrice = bucket.getAggregations().get("avgPrice");
System.out.println(avgPrice.getValue());
});
}

// 按照品牌分组,统计各品牌的价格数据
@Test
public void testQuery10() throws Exception {
NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
builder.addAggregation(
AggregationBuilders.terms("groupByBrand").field("brand")
.subAggregation(AggregationBuilders.stats("statsPrice").field("price"))
);
AggregatedPage<Product> page = (AggregatedPage<Product>) repostitory.search(builder.build());
// 获取自定义的分组字段
StringTerms brand = (StringTerms) page.getAggregation("groupByBrand");
brand.getBuckets().forEach(bucket -> {
InternalStats statsPrice = bucket.getAggregations().get("statsPrice");
System.out.println(statsPrice.getSumAsString());
System.out.println(statsPrice.getAvgAsString());
System.out.println(statsPrice.getMaxAsString());
System.out.println(statsPrice.getMinAsString());
System.out.println(statsPrice.getCount());
System.out.println("-----------------------");
});
}

// 查询商品标题或简介中符合"蓝牙 指纹 双卡"的字样的商品,并且高亮显示
@Test
public void testHighlight() throws Exception {
// Java与JSON互转的工具对象
ObjectMapper mapper = new ObjectMapper();

NativeSearchQueryBuilder builder = new NativeSearchQueryBuilder();
// 设置查询哪个索引中的哪个类型
builder.withIndices("shop_product").withTypes("shop_product");
builder.withQuery(
QueryBuilders.multiMatchQuery("蓝牙 指纹 双卡",
"title", "intro")
);
builder.withHighlightFields(
new HighlightBuilder.Field("title")
.preTags("<span style='color:red'>").postTags("</span>"),
new HighlightBuilder.Field("intro")
.preTags("<span style='color:red'>").postTags("</span>")
);
AggregatedPage<Product> page = template.queryForPage(builder.build(), Product.class, new SearchResultMapper() {
public <T> AggregatedPage<T> mapResults(SearchResponse resp, Class<T> clazz, Pageable pageable) {
List<T> list = new ArrayList<>();
try {
for (SearchHit hit : resp.getHits().getHits()) {
// 把查询到的JSON字符串转换成Java对象
T t = mapper.readValue(hit.getSourceAsString(), clazz);
for (HighlightField field : hit.getHighlightFields().values()) {
// 替换需要高亮显示的字段,用到Apache的BeanUtils工具
BeanUtils.setProperty(t, field.getName(), field.getFragments()[0].string());
}
list.add(t);
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
long total = resp.getHits().totalHits;
return new AggregatedPageImpl<>(list, pageable, total);
}
});

page.forEach(System.out::println);
}

# springboot2.3.x 以上配置

配置关系对应

截屏2021-08-14 14.17.14

  1. pom 相同

  2. 配置

    1
    2
    3
    4
    5
    6
    7
    ## 旧版本以spring.data.elasticsearch.开头;访问地址配置不用声明访问协议,监听es的tcp端口
    #spring.data.elasticsearch.cluster-nodes=localhost:9300
    #spring.data.elasticsearch.cluster-name=elasticsearch

    ## 新版本以spring.elasticsearch.rest.开头;访问地址配置需要声明访问协议,直接监听es访问端口
    spring.elasticsearch.rest.uris=http://localhost:9200
    spring.elasticsearch.rest.username=elasticsearch
  3. 使用

    • 旧版的核心访问对象是 ElasticsearchTemplate;新版的核心访问对象是 ElasticsearchRestTemplate;

    • 实体类没有类型

      1
      2
      // @Document指定当前类是索引对象。indexName:索引名称;shards:创建索引时的分片数;replicas:创建索引时的备份数
      @Document(indexName = "book_", shards = 5, replicas = 1)

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Docker】关于docker服务报错WARNING IPv4 forwarding is disabled. Networking will not work

InterviewCoder

# 【Docker】关于 docker 服务报错 WARNING IPv4 forwarding is disabled. Networking will not work

注意:在这里强调,强制进入或者进入镜像,进入后会引起 yum install 和 wget 等不能使用

# 一,docker 运行直接报错

报错:

[root@localhost /]# docker run -it ubuntu /bin/bash

WARNING: IPv4 forwarding is disabled. Networking will not work.

img

1. 解决方式:

第一步:在宿主机上执行 echo “net.ipv4.ip_forward=1” >>/usr/lib/sysctl.d/00-system.conf

img

2. 第二步:重启 network 和 docker 服务

[root@localhost /]# systemctl restart network && systemctl restart docker

3. 第三步:验证是否成功

img

可见完美解决问题。

# 二,如果你是 docker 容器运行镜像的时候也是报这种错误,相对应得也是重启一下 docker 就可以完美解决了了。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【chatGPT】利用腾讯云函数免费部署国内直接使用ChatGPT代理,解决网络不可用及1020等问题

# 【chatGPT】利用腾讯云函数免费部署国内直接使用 ChatGPT 代理,解决网络不可用及 1020 等问题

InterviewCoder

# 前言

在 Github 上发现一个项目,可以利用腾讯云提供的云函数,一分钟就能搭建好一台可以直接访问 ChatGPT 的代理,关键还是免费的。今天就来一起折腾它。

# 项目

项目地址:https://github.com/geekr-dev/openai-proxy

# 教程

可以直接看官方文字教程,如果是新手,可以按我图文教程

# 第一步:

​ 登录腾讯控制台:https://console.cloud.tencent.com/scf/list

​ 如果之前没使用腾讯云函数,会提示授权

image-20230420085936706

# 第二步:前往访问管理

image-20230420085957003

# 第三步:新建函数,记得选择非大陆地区哟

image-20230420090015818

image-20230420090031428

# 上传 Github 项目中的 ZIP 文件
# 第四步:高级配置

上传完 ZIP 文件,接着下拉,找到高级配置,点进去编辑image-20230420090114681

点高级配置的最右边的编辑按钮

image-20230420090132944

image-20230420090140465

# 第五步:获取代理地址

image-20230420090255336

不要 "/release" 路径,只要取子域名部分就可以了

image-20230420090309398

这时候访问路径内容和 api.open.com 的结果是一至的了。

至此:已经有了海外域名的代理地址了。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【chatGPT】基于Docker如何快速部署自己的ChatGPT

InterviewCoder

# 基于 Docker 如何快速部署自己的 ChatGPT

# 背景

随着 OpenAI 在 2022 年底发布的 LLM 模型 - ChatGPT 展现出的强大效果,ChatGPT 无疑成为了当下炙手可热的明星模型。

现有的基于 GPT 的开源项目已经非常多,本文以现有的高热度 github 开源项目 chatgpt-web 为例,教大家简单快速地搭建属于自己的 ChatGPT。

# ChatGPT-Web

chatgpt-web 项目中的部署教程已经非常完整,本文不再过多解释。

仅以 Docker 部署为例

前置条件

  • 本地或者服务器应该具有 Docker 环境
  • 具有 ChatGPT 帐号

以 token 模式为例,请求 chatgpt web 版本,免费但稍微具有延迟

Step1. 找到你帐号的 token

点击 https://chat.openai.com/api/auth/session,获取你帐号的 token,并记录他

Step2. 运行 docker

按需配置访问 Web 页面的密码,Token、超时等信息

1
2
docker run --name chatgpt-web -d -p 127.0.0.1:3888:3002 --env OPENAI_ACCESS_TOKEN=your_access_token --env AUTH_SECRET_KEY=you_secret_key chenzhaoyu94/chatgpt-web
1

Step3. 访问 localhost:3002 查看效果
在这里插入图片描述
在上述步骤中我们无需进行任何代理,就可以直接与 GPT 交流,使用 API 方式同理。当然了,根据项目作者的介绍,使用 API 时需要进行代理自建

如果你只是在本地部署给自己使用,那么以上 3 步就满足了需求,如果想要在公网访问,或者像 App 一样访问你的 ChatGPT,那么请接着往下看。

# Nginx 反向代理

以宝塔面板为例,我们在服务器上拉起 docker 镜像后,可以通过 ip:port 进行访问

但通常来说我们的网站带有域名,以笔者所使用的腾讯云服务器为例

前置条件

  • 拥有一个域名
  • 拥有一台云服务器

Step1. SSL 证书

首先在云产品中找到 SSL 证书,点击我的证书 - 免费证书 - 申请免费证书

在这里插入图片描述

填写申请的域名,申请成功之后,点击下载,下载 nginx 格式的即可

Step2. 配置域名 SSL

在宝塔面板中选择 - 网站 - 添加站点

填写刚刚申请 SSL 证书的域名,选择纯静态,其余默认,点击确定即可

在这里插入图片描述

Step3. 配置证书

点击添加好的网站,然后点击 SSL,填入刚刚下载的文件中的 keypem
在这里插入图片描述

配置完成后点击保存

Step4. 配置 DNS 解析

在云产品中搜索 - 云解析 - 选择 DNS 解析 DNSPod

点击我的域名 - 添加记录

在这里插入图片描述

填入刚刚申请的域名,如果带有前缀,则第一个红框填入你的域名前缀,比如 www.baidu.com,则这里填 www

第二个红框填写你的服务器 ip,或者你的 CDN 域名

在这里插入图片描述

Step5. 配置反向代理

在宝塔面板中,点击刚刚添加的网站,点击反向代理,填入刚刚 docker 启动时的宿主机端口
在这里插入图片描述

如上文中的 3888

以上配置完成之后,访问 https:// 你的域名就可以了~

# PWA 支持

PWA 技术可以让我们访问网站能够拥有访问 App 一般的体验,在 chatgpt-web 中已经内嵌,但默认是关闭的

我们可以通过设置启动时的参数 -env VITE_GLOB_APP_PWA=true 将他打开

1
2
docker run --name chatgpt-web -d -p 127.0.0.1:3888:3002 --env OPENAI_ACCESS_TOKEN=your_access_token --env AUTH_SECRET_KEY=you_secret_key --env VITE_GLOB_APP_PWA=true chenzhaoyu94/chatgpt-web
1

部署成功之后,我们再到手机上访问该网站时便可以保存他在桌面了。

默认的 PWA 图标和全局用户信息配置在项目中,即使在网页可以修改当前登陆者的用户信息,在清除 Cookie 之后便会还原,如果你想定制这两种信息,请拉下 chatgpt-web 项目进行镜像自定义

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Docker】Linux部署Docker

InterviewCoder

# 【Docker】Linux 部署 Docker

接触一段时间 docker,这个工具大大提高了开发者打包应用的效率。

一直都是直接把镜像扔到到 docker 里构建容器启动,并没有深入了解。

在这里插入图片描述
本文由 alpha0808 大佬指导,如果要了解 docker 的概念以及命令,请去看大佬这篇 DOCKER 之入门篇

本篇文章集中于 linux 系统下对 docker 及相关组件的部署。

# 目录

# 一、安装 docker

按照官网 https://docs.docker.com/engine/install/centos/ 执行命令即可

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
#1.yum检查更新
sudo yum check-update
#2.删除旧版本
sudo yum remove docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
#3.安装gcc环境
yum -y install gcc
yum -y install gcc-c++
#4.安装依赖项
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
#5.将 Docker 存储库添加到 CentOS
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
#使用阿里服务器下载
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
#如果没有执行命令1更新,那么此时执行命令即可
yum makecache fast
#6.下载docker
sudo yum install -y docker #注意这样下载需要接受GPG秘钥,相当于一个数字指纹,指纹格式:060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35
#或者社区版本
yum -y install docker-ce
#7.检查版本
docker version
#或者
docker -v
#8.查看docker进程
docker ps

执行第 8 步,如果报错
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemo
是因为 docker 还没启动

解决方案:

1
systemctl start docker.service

# 二、镜像加速

使用阿里云镜像地址来加速镜像下载的速度

# 获取阿里云镜像地址

点击容器镜像服务
镜像工具镜像加速器,生成加速器地址

在这里插入图片描述

# 添加加速器地址

切换目录至 /etc/docker

1
cd /etc/docker

编辑 daemon.js 文件

1
2
3
{
"registry-mirrors": ["加速器地址"]
}

重启 docker 的伴随线程

1
systemctl daemon-reload

重启 docker 服务

1
systemctl restart docker

# 三、可视化管理工具 Portainer

# 简介

Portainer 是 Docker 的图形化管理工具,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm 集群和服务等集中管理和操作、登录用户管理和控制等功能

# 展示

  • 首页
    包含 docker-compose、容器、镜像、卷、网络总体概况
    在这里插入图片描述
  • 容器
    包含容器的启动、暂停、杀死进程、重启、新增,监控,日志查看,容器控制台等功能。
    在这里插入图片描述
  • 镜像
    包含镜像详细查看、删除、导入、导出等功能
    在这里插入图片描述

# 安装

采用 docker 安装

# 镜像下载

查询 portainer 镜像

1
docker search portainer

在这里插入图片描述

下载 portainer 镜像

1
docker pull portainer/portainer

在这里插入图片描述

# 容器运行

1
docker run -p 9000:9000 --name portainer -v /var/run/docker.sock:/var/run/docker.sock -d portainer/portainer

开放 9000 端口

1
firewall-cmd --zone=public --add-port=9000/tcp --permanent && firewall-cmd --reload

# 创建用户

访问 9000 端口,第一次登录设置管理员账号和密码
在这里插入图片描述

# docker 连接管理

在这里插入图片描述
可以选择管理本地 Local 和远程 Remote 的 Docker 两个选项,我们安装在本机,直接选择 Local,然后 Connect 进入管理界面
在这里插入图片描述
点击 connect,报错

Failure dial unix /var/run/docker.sock: connect: permission denied

可以猜测是 SElinux 的问题,看 SELinux 状态:sestatus 命令进行查看

1
2
/usr/sbin/sestatus -v      ##如果SELinux status参数为enabled即为开启状态
SELinux status: enabled

修改 /etc/selinux/config 文件,保存后重启机器

SELINUX=enforcing 改为 SELINUX=disabled

再次访问 9000,连接 local,成功
在这里插入图片描述

# 四、补充

# docker 开机自启

1
sudo systemctl enable docker

# 容器开机自启

以上面的 docker 可视化管理工具 portainer 为例,希望开机的时候,自动启动镜像

启动命令加–restart=always

1
docker run -p 9000:9000 --name portainer --restart=always  -v /var/run/docker.sock:/var/run/docker.sock -d portainer/portainer

如果已经在运行的镜像

1
docker update --restart=always portainer

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

CAS机制

InterviewCoder

# CAS 机制

# 我们先看一段代码:

启动两个线程,每个线程中让静态变量 count 循环累加 100 次。

img

最终输出的 count 结果一定是 200 吗?因为这段代码是非线程安全的,所以最终的自增结果很可能会小于 200。我们再加上 synchronized 同步锁,再来看一下。

img

加了同步锁之后,count 自增的操作变成了原子性操作,所以最终输出一定是 count=200,代码实现了线程安全。虽然 synchronized 确保了线程安全,但是在某些情况下,这并不是一个最有的选择。

关键在于性能问题。

synchronized 关键字会让没有得到锁资源的线程进入 BLOCKED 状态,而后在争夺到锁资源后恢复为 RUNNABLE 状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。

尽管 JAVA 1.6 为 synchronized 做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用 java 中的 “原子操作类”。

所谓原子操作类,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。如 AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于 Boolean,Integer,Long 类型的原子性操作。

现在我们尝试使用 AtomicInteger 类:

img

使用 AtomicInteger 之后,最终的输出结果同样可以保证是 200。并且在某些情况下,代码的性能会比 synchronized 更好。

而 Atomic 操作类的底层正是用到了 “CAS 机制”。

CAS 是英文单词 Compare and Swap 的缩写,翻译过来就是比较并替换。

CAS 机制中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。

更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B。

我们看一个例子:

\1. 在内存地址 V 当中,存储着值为 10 的变量。

img

\2. 此时线程 1 想把变量的值增加 1. 对线程 1 来说,旧的预期值 A=10,要修改的新值 B=11.

img

\3. 在线程 1 要提交更新之前,另一个线程 2 抢先一步,把内存地址 V 中的变量值率先更新成了 11。

img

\4. 线程 1 开始提交更新,首先进行 A 和地址 V 的实际值比较,发现 A 不等于 V 的实际值,提交失败。

img

\5. 线程 1 重新获取内存地址 V 的当前值,并重新计算想要修改的值。此时对线程 1 来说,A=11,B=12。这个重新尝试的过程被称为自旋。

img

\6. 这一次比较幸运,没有其他线程改变地址 V 的值。线程 1 进行比较,发现 A 和地址 V 的实际值是相等的。

img

\7. 线程 1 进行交换,把地址 V 的值替换为 B,也就是 12.

img

从思想上来说,synchronized 属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS 属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

在 java 中除了上面提到的 Atomic 系列类,以及 Lock 系列类夺得底层实现,甚至在 JAVA1.6 以上版本,synchronized 转变为重量级锁之前,也会采用 CAS 机制。

CAS 的缺点:

1) CPU 开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给 CPU 带来很到的压力。

2) 不能保证代码块的原子性

CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 synchronized 了。

3) ABA 问题

这是 CAS 机制最大的问题所在。(后面有介绍)

我们下面来介绍一下两个问题:

*1. JAVA 中 CAS 的底层实现 *

*2. CAS 的 ABA 问题和解决办法。*

我们看一下 AtomicInteger 当中常用的自增方法 incrementAndGet:

public final int incrementAndGet() {

for (;😉 {

​ int current = get();

​ int next = current + 1;

​ if (compareAndSet(current, next))

​ return next;

}

}

private volatile int value;

public final int get() {

return value;

}

这段代码是一个无限循环,也就是 CAS 的自旋,循环体中做了三件事:

\1. 获取当前值

\2. 当前值 + 1,计算出目标值

\3. 进行 CAS 操作,如果成功则跳出循环,如果失败则重复上述步骤

这里需要注意的重点是 get 方法,这个方法的作用是获取变量的当前值。

如何保证获取的当前值是内存中的最新值?很简单,用 volatile 关键字来保证(保证线程间的可见性)。我们接下来看一下 compareAndSet 方法的实现:

img

compareAndSet 方法的实现很简单,只有一行代码。这里涉及到两个重要的对象,一个是 unsafe,一个是 valueOffset。

什么是 unsafe 呢?Java 语言不像 C,C++ 那样可以直接访问底层操作系统,但是 JVM 为我们提供了一个后门,这个后门就是 unsafe。unsafe 为我们提供了硬件级别的原子操作。

至于 valueOffset 对象,是通过 unsafe.objectFiledOffset 方法得到,所代表的是 AtomicInteger 对象 value 成员变量在内存中的偏移量。我们可以简单的把 valueOffset 理解为 value 变量的内存地址。

我们上面说过,CAS 机制中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。

而 unsafe 的 compareAndSwapInt 方法的参数包括了这三个基本元素:valueOffset 参数代表了 V,expect 参数代表了 A,update 参数代表了 B。

正是 unsafe 的 compareAndSwapInt 方法保证了 Compare 和 Swap 操作之间的原子性操作。

我们现在来说什么是 ABA 问题。假设内存中有一个值为 A 的变量,存储在地址 V 中。

img

此时有三个线程想使用 CAS 的方式更新这个变量的值,每个线程的执行时间有略微偏差。线程 1 和线程 2 已经获取当前值,线程 3 还未获取当前值。

img

接下来,线程 1 先一步执行成功,把当前值成功从 A 更新为 B;同时线程 2 因为某种原因被阻塞住,没有做更新操作;线程 3 在线程 1 更新之后,获取了当前值 B。

img

在之后,线程 2 仍然处于阻塞状态,线程 3 继续执行,成功把当前值从 B 更新成了 A。

img

最后,线程 2 终于恢复了运行状态,由于阻塞之前已经获得了 “当前值 A”,并且经过 compare 检测,内存地址 V 中的实际值也是 A,所以成功把变量值 A 更新成了 B。

img

看起来这个例子没啥问题,但如果结合实际,就可以发现它的问题所在。

我们假设一个提款机的例子。假设有一个遵循 CAS 原理的提款机,小灰有 100 元存款,要用这个提款机来提款 50 元。

img

由于提款机硬件出了点问题,小灰的提款操作被同时提交了两次,开启了两个线程,两个线程都是获取当前值 100 元,要更新成 50 元。

理想情况下,应该一个线程更新成功,一个线程更新失败,小灰的存款值被扣一次。

img

线程 1 首先执行成功,把余额从 100 改成 50. 线程 2 因为某种原因阻塞。这时,小灰的妈妈刚好给小灰汇款 50 元。

img

线程 2 仍然是阻塞状态,线程 3 执行成功,把余额从 50 改成了 100。

img

线程 2 恢复运行,由于阻塞之前获得了 “当前值” 100,并且经过 compare 检测,此时存款实际值也是 100,所以会成功把变量值 100 更新成 50。

img

原本线程 2 应当提交失败,小灰的正确余额应该保持 100 元,结果由于 ABA 问题提交成功了。

怎么解决呢?加个版本号就可以了。

真正要做到严谨的 CAS 机制,我们在 compare 阶段不仅要比较期望值 A 和地址 V 中的实际值,还要比较变量的版本号是否一致。

我们仍然以刚才的例子来说明,假设地址 V 中存储着变量值 A,当前版本号是 01。线程 1 获取了当前值 A 和版本号 01,想要更新为 B,但是被阻塞了。

img

这时候,内存地址 V 中变量发生了多次改变,版本号提升为 03,但是变量值仍然是 A。

img

随后线程 1 恢复运行,进行 compare 操作。经过比较,线程 1 所获得的值和地址的实际值都是 A,但是版本号不相等,所以这一次更新失败。

img

在 Java 中,AtomicStampedReference 类就实现了用版本号作比较额 CAS 机制。

*1. java 语言 CAS 底层如何实现?*

* 利用 unsafe 提供的原子性操作方法。*

*2. 什么事 ABA 问题?怎么解决?*

* 当一个值从 A 变成 B,又更新回 A,普通 CAS 机制会误判通过检测。*

* 利用版本号比较可以有效解决 ABA 问题。*

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java】Java字符与ASCII码互转

InterviewCoder

# 【Java】Java 字符与 ASCII 码互转

字符转对应 ASCII 码

1
2
3
4
5
6
7
8
// 方法一:将char强制转换为byte
char ch = 'A';
byte byteAscii = (byte) ch;
System.out.println(byteAscii);

// 方法二:将char直接转化为int,其值就是字符的ascii
int byteAscii1 = (int) ch;
System.out.println(byteAscii1);

ASCII 码转字符

1
2
3
4
// 直接int强制转换为char
int ascii = 65;
char ch1 = (char)ascii;
System.out.println(ch1);

应用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Pattern;

/**
* HJ21 简单密码
*
* 描述
* 现在有一种密码变换算法。
* 九键手机键盘上的数字与字母的对应: 1--1, abc--2, def--3, ghi--4, jkl--5, mno--6, pqrs--7, tuv--8 wxyz--9, 0--0,把密码中出现的小写字母都变成九键键盘对应的数字,如:a 变成 2,x 变成 9.
* 而密码中出现的大写字母则变成小写之后往后移一位,如:X ,先变成小写,再往后移一位,变成了 y ,例外:Z 往后移是 a 。
* 数字和其它的符号都不做变换。
* 数据范围: 输入的字符串长度满足 1≤n≤100
* 输入描述:
* 输入一组密码,长度不超过100个字符。
*
* 输出描述:
* 输出密码变换后的字符串
*
* 示例1
* 输入:
* YUANzhi1987
* 输出:
* zvbo9441987
*/
public class HJ21Test {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String input = scanner.next();

Map<Integer, String> map = new HashMap<>();
map.put(1, "1");
map.put(2, "abc");
map.put(3, "def");
map.put(4, "ghi");
map.put(5, "jkl");
map.put(6, "mno");
map.put(7, "pqrs");
map.put(8, "tuv");
map.put(9, "wxyz");
map.put(0, "0");

char[] chars = input.toCharArray();
int length = chars.length;

StringBuilder builder = new StringBuilder();

for (int i = 0; i < length; i++) {
int finalI = i;
map.forEach((k, v) -> {
if (v.contains(String.valueOf(chars[finalI]))) {
builder.append(k);
}
});

int u = chars[finalI];
if (u >= 65 && u <= 90) {
int i1 = u + 33;
if (i1 == 123 ) {
builder.append("a");
} else {
char ch = (char) (u + 33);
builder.append(ch);
}
}
Pattern pattern = Pattern.compile("[2-9]");
if (pattern.matcher(String.valueOf(chars[finalI])).matches()) {
builder.append(chars[i]);
}
}

System.out.println(builder);

}
}

运行结果
在这里插入图片描述

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

SVN&Git使用手册

InterviewCoder

切记 代码是先更新再提交.
(一)SVN 和 GIt 区别
1.基本
SVN 是集中式版本控制工具
git 是分布式版本控制工具

2.SVN 和 Git 优缺点
svn 优点:
1. 方便管理者查看每个开发者开发进度
2. 方便对开发者进行权限控制
svn 缺点:
1. 严重依赖网络环境进行版本控制
2. 中央服务器宕机,无法进行版本控制
3. 中央服务器磁盘损坏,丢失历史版本内容
Git 优点:
1. 很完美的解决了 SVN 存在的缺点

TortoiseGit 小乌龟
给当前项目提交到码云上
1. 用浏览器登录码云,在码云上创建一个仓库
2. 打开 TortoiseGit 软件
3. 新建一个文件夹改好名字 (注意是空文件夹)
4. 右键文件夹–>Git 克隆…–> 弹出如下图–> 粘贴好 url
5. 点击确定就可以了

6. 然后就给你自己的代码放入刚才的文件夹下,然后就点击推送等等.
2.设置用户名和邮箱

直接设置即可。签名密钥不需要管

Git 使用笔记
(一)概念
1.什么是版本控制

版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 除了项目源代码,你可以对任何类型的文件进行版本控制。

2.为什么要版本控制
有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。
许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。 这么做唯一的好处就是简单,但是特别容易犯错。 有时候会混淆所在的工作目录,一不小心会写错文件或者覆盖意想外的文件。
为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。
3.集中化的版本控制系统
接下来人们又遇到一个问题,如何让在不同系统上的开发者协同工作? 于是,集中化的版本控制系统(Centralized Version Control Systems,简称 CVCS)应运而生。
集中化的版本控制系统都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。
这么做虽然解决了本地版本控制系统无法让在不同系统上的开发者协同工作的诟病,但也还是存在下面的问题:

单点故障: 中央服务器宕机,则其他人无法使用;如果中心数据库磁盘损坏有没有进行备份,你将丢失所有数据。本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。

必须联网才能工作: 受网络状况、带宽影响。

4.分布式版本控制系统
于是分布式版本控制系统(Distributed Version Control System,简称 DVCS)面世了。 Git 就是一个典型的分布式版本控制系统。
这类系统,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
分布式版本控制系统可以不用联网就可以工作,因为每个人的电脑上都是完整的版本库,当你修改了某个文件后,你只需要将自己的修改推送给别人就可以了。但是,在实际使用分布式版本控制系统的时候,很少会直接进行推送修改,而是使用一台充当 “中央服务器” 的东西。这个服务器的作用仅仅是用来方便 “交换” 大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
分布式版本控制系统的优势不单是不必联网这么简单,后面我们还会看到 Git 极其强大的分支管理等功能。
5.Git 与其他版本管理系统的主要区别
Git 在保存和对待各种信息的时候与其它版本控制系统有很大差异,尽管操作起来的命令形式非常相近,理解这些差异将有助于防止你使用中的困惑。
下面我们主要说一个关于 Git 其他版本管理系统的主要差别:对待数据的方式。
Git 采用的是直接记录快照的方式,而非差异比较。我后面会详细介绍这两种方式的差别。
大部分版本控制系统(CVS、Subversion、Perforce、Bazaar 等等)都是以文件变更列表的方式存储信息,这类系统将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。
具体原理如下图所示,理解起来其实很简单,每个我们对提交更新一个文件之后,系统记录都会记录这个文件做了哪些更新,以增量符号 Δ(Delta) 表示。

我们怎样才能得到一个文件的最终版本呢?
很简单,高中数学的基本知识,我们只需要将这些原文件和这些增加进行相加就行了。
这种方式有什么问题呢?
比如我们的增量特别特别多的话,如果我们要得到最终的文件是不是会耗费时间和性能。
Git 不按照以上方式对待或保存数据。 反之,Git 更像是把数据看作是对小型文件系统的一组快照。 每次你提交更新,或在 Git 中保存项目状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。 为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。 Git 对待数据更像是一个 快照流。

6.使用 git 原因
分布式版本控制工具
为什么使用 git 呢?
互联网项目,业务越来越复杂,项目越来越大,使用 SVN 版本控制不太好使了。
从 git 目标,看 git 的优势 速度快、分布式、有能力管理大型项目

7.工作流程以及流程图

在工作目录中修改文件。
暂存文件,将文件的快照放入暂存区域。
提交更新,找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。
8.程序员工作流程
项目经理:1 创建项目 2 项目添加本地暂存态 3 项目提交到本地 4 建立远程连接 5 推送代码到远程服务器
程序员 A:1 克隆代码到本地 2 编写代码,将代码添加本地暂存态 3 代码提交本地 4 推送代码到服务器
程序员 B:1 克隆代码到本地 2 编写代码,将代码添加本地暂存态 3 代码提交本地 4 推送代码到服务器 5 更新其他程序员提交的代码
9.git 三种状态

已提交(committed):数据已经安全的保存在本地数据库中。

已修改(modified):已修改表示修改了文件,但还没保存到数据库中。

已暂存(staged):表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

由此引入 Git 项目的三个工作区域的概念:Git 仓库 (.git directoty) 、工作目录 (Working Directory) 以及 暂存区域 (Staging Area) 。

(二)Git 分支
1.分支概念

几乎每一种版本控制系统都以某种形式支持分支。使用分支意味着你可以从开发主线上分离开来,然后在不影响主线的同时继续工作。在很多版本控制系统中,这是个昂贵的过程,常常需要创建一个源代码目录的完整副本,对大型项目来说会花费很长时间。

有人把 Git 的分支模型称为 “必杀技特性”,而正是因为它,将 Git 从版本控制系统家族里区分出来。Git 有何特别之处呢?Git 的分支可谓是难以置信的轻量级,它的新建操作几乎可以在瞬间完成,并且在不同分支间切换起来也差不多一样快。和许多其他版本控制系统不同,Git 鼓励在工作流程中频繁使用分支与合并,哪怕一天之内进行许多次都没有关系。理解分支的概念并熟练运用后,你才会意识到为什么 Git 是一个如此强大而独特的工具,并从此真正改变你的开发方式。
2.分支需求
现在让我们来看一个简单的分支与合并的例子,实际工作中大体也会用到这样的工作流
程:

开发某个网站。
为实现某个新的需求,创建一个分支。
在这个分支上开展工作。
假设此时,你突然接到一个电话说有个很严重的问题需要紧急修补,那么可以按照下面的方
式处理:
返回到原先已经发布到生产服务器上的分支。
为这次紧急修补建立一个新分支。
测试通过后,将此修补分支合并,再推送到生产服务器上。
切换到之前实现新需求的分支,继续工作。
(三)git 指令
1.创建 git 目录
git init

创建 git 目录:
该命令将创建一个名为 .git 的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。但是,在这个时候,我们仅仅是做了一个初始化的操作,你的项目里的文件还没有被跟踪。(参见 Git 内部原理 来了解更多关于到底 .git 文件夹中包含了哪些文件的信息。)
2.克隆仓库
git clone [url]
克隆仓库的命令格式是 git clone [url] 。比如,要克隆 Git 的可链接库 libgit2,可以用下面的命令
$ git clone https://github.com/libgit2/libgit2

这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹,从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。如果你进入到这个新建的 libgit2 文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。如果你想在克隆远程仓库的时
候,自定义本地仓库的名字,你可以使用如下命令
$ git clone https://github.com/libgit2/libgit2 mylibgit
这将执行与上一个命令相同的操作,不过在本地创建的仓库名字变为 mylibgit
3.查看文件状态
git status
要查看哪些文件处于什么状态,可以用 git status 命令。如果在克隆仓库后立即使用此命令,会看到类似这
样的输出.

跟踪新文件

(四)使用
1.安装 git

http://note.youdao.com/noteshare?id=448878ab8bdba693717f8967e6aa6a4f

2.将项目上传到码云上

如果账户已经配置好,git 客户端也安装好,在将要上传的文件中,右键打开 Git Bash Here, 按如下步骤即可:
1.git init
2.git remote add origin “你的码云项目地址(ssh 或 https)”
开始提交项目
3.git pull origin master 如果密码报错看下面
4.git touch init .txt // 如果已经存在更改的文件,则操作这一步,否则跳过即可
5.git add .
6.git commit -m “第一次提交(提交信息)”
7.git push origin master
如出现错误 hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: ‘git pull …’) before pushing again.
hint: See the ‘Note about fast-forwards’ in ‘git push --help’ for details.
可执行此操作继续,git push -u origin master -f 强制命令会覆盖别人分支,慎用

密码报错在这里进行修改

3.用 git 从 github 上下载代码

新建一个文件夹,放你需要下载的东西。右键点击 “Git Bash Here”

此时会弹出 git 的命令窗口

3、输入 git clone + 下载地址,回车即可。如 git clone https://github.comxxx.git
等待下载

下载好之后,文件夹里就会出现相应的项目啦~

(五)GitGUI 操作
1.使用 gui 上传代码到码云上
https://blog.csdn.net/qq_33867131/article/details/80831491

SVN 的基本使用
(一)基本简介
开源的版本控制系统,可以用来保存代码,同步代码,也可以保存图片文档电影什么的.
主要作用就是可以随时进行代码同步。但是会存在一些问题,会有一些解决方案
(二)服务端创建仓库
1.搭建仓库,添加用户

http://note.youdao.com/noteshare?id=549663e56580a3643bbcc9104c357226

2.分配权限

创建好用户后,用户还是不能访问我们的仓库,接下来我们要给用户分个组(如果想让用户不进组也能访问仓库,可以直接给单个用户权限

3.给代码放到 svn 上面

然后复制仓库地址

在你要上传的项目右键

然后点击确定

(三)错误解决
1.代码错乱问题
去查看日志,下载正确的时间段的代码,然后再创建新的仓库,放那个代码,原来的仓库就删除掉就可以了.

2.No appropriate protocol
公司的 SVN 协议从 svn 协议变更到 https 协议,结果 IDEA 的 SVN 报 No appropriate protocol,查询资料,最终解决方案是:
Go to Preferences > Version Control > SubVersion > Enable Interactive Mode
3.versionControl 里面找不到 subversion 选项
去安装 svnToolBox 插件 即可解决

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Java】如何优雅的将数组转换为List集合

InterviewCoder

# 【Java】如何优雅的将数组转换为 List 集合

# 第一种方式 (未必最佳): 使用 ArrayList.asList (strArray)

使用 Arrays 工具类 Arrays.asList (strArray) 方式,转换完成后,只能对 List 数组进行查改,不能增删,增删就会抛出 UnsupportedOperationException 异常

1
2
3
4
5
6
7
8
9
import java.util.Arrays;
import java.util.List;
public static void Demo1() {
String[] str = {"fgx", "lzy"};
//注意这个List不是Collections包内的List,而是util包里面的List接口
List<String> ints = Arrays.asList(str);
//这里会报错
ints.add("laopo");
}

添加数据报错:

1
2
3
4
5
6
7
8
9
10
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at JAVA基础.JDK8新特性.Java数组转List.Demo1(Java数组转List.java:20)
at JAVA基础.JDK8新特性.Java数组转List.main(Java数组转List.java:13)

报错原因:Arrays.asList(str)返回值是java.util.Arrays类中一个私有静态内部类
java.utiil.Arrays.Arraylist,并不是我们平时用的java.util.ArrayList();

使用场景:Arrays.asList(strArray)方式仅能用在将数组转换为List后,不需要增删其中的值,仅作为数据源读取使用。

# 第二种方法 (支持增删查改):

通过 ArrayList 的构造器,将 Arrays.asList (strArray) 的返回值由 java.utilArrays.ArrayList 转为 java.util.ArrayList.
关键代码:ArrayList list = new ArrayList (Arrays.asList (strArray)) ;

1
2
3
4
5
String[] str = {"fgx", "lzy"};
//注意这个List不是Collections包内的List,而是util包里面的List接口
java.util.ArrayList<String> strings = new ArrayList<>(Arrays.asList(str));
strings.add("aop");
strings.stream().forEach(System.out::println);

在这里插入图片描述
使用场景:需要在将数组转换为 List 后,对 List 进行增删改查操作,在 List 的数据量不大的情况下,可以使用。

# 第三种方式 (通过集合工具类 Collections.addAll () 方法 (最高效))

通过 Collections.addAll (arrayList, strArray) 方式转换,根据数组的长度创建一个长度相同的 List,然后通过 Collections.addAll () 方法,将数组中的元素转为二进制,然后添加到 List 中,这是最高效的方法。

1
2
3
4
5
6
public static void Demo3() {
//注意这个List不是Collections包内的List,而是util包里面的List接口
String[] str = {"fgx", "lzy"};
java.util.ArrayList<String> stringList = new ArrayList<>(str.length);
Collections.addAll(stringList,str);
}

# 第四种方式通过 JDK8 的 Stream 流将 3 总基本类型数组转为 List

如果 JDK 版本在 1.8 以上,使用流 stream 来将下列 3 种数组快速转为 List, 分别是 int [],long [],double [], 不支持 short [ ],byte [ ],char [] 在 JDK1.8 中暂不支持.

1
2
3
4
5
6
int[] ints = {2, 34, 55, 22, 11};
long[] longs = {1, 2, 3};
double[] doubles = {1, 2, 3};
Arrays.stream(ints).boxed().collect(Collectors.toList());
Arrays.stream(longs).boxed().collect(Collectors.toList());
Arrays.stream(doubles).boxed().collect(Collectors.toList());

TIPs: 为什么 int [] 不能直接转为 List, 而 Integer [] 可以转为 List, 而 Integer [] 就可以转为 List 了,因为 List 中的泛型必须是引用类型。

java 数组转 list 误区
一、不能把基本数据类型转化为列表
仔细观察可以发现 asList 接受的参数是一个泛型的变长参数,而基本数据类型是无法泛型化的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
public  class  App {
public static void main(String[] args) {
int [] intarray = { 1 , 2 , 3 , 4 , 5 };
//List<Integer> list = Arrays.asList(intarray); 编译通不过
List< int []> list = Arrays.asList(intarray);
System.out.println(list);
}
}

output:
[[I @66d3c617 ]

这是因为把 int 类型的数组当参数了,所以转换后的列表就只包含一个 int [] 元素。
解决方案:
要想把基本数据类型的数组转化为其包装类型的 list,可以使用 guava 类库的工具方法,示例如下:

1
2
int [] intArray = { 1 ,  2 ,  3 ,  4 };
List<Integer> list = Ints.asList(intArray);

二、asList 方法返回的是数组的一个视图
视图意味着,对这个 list 的操作都会反映在原数组上,而且这个 list 是定长的,不支持 add、remove 等改变长度的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public  class  App {
public static void main(String[] args) {
int [] intArray = { 1 , 2 , 3 , 4 };
List<Integer> list = Ints.asList(intArray);
list.set( 0 , 100 );
System.out.println(Arrays.toString(intArray));
list.add( 5 );
list.remove( 0 );
}
}

output:
[ 100 , 2 , 3 , 4 ]
UnsupportedOperationException
UnsupportedOperationException

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Linux环境下安装jenkins

InterviewCoder

# Linux 环境下安装 jenkins

# 1、添加存储库

yum 的 repo 中默认没有 Jenkins,需要先将 Jenkins 存储库添加到 yum repos,执行下面的命令:

1
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo

完成界面:

在这里插入图片描述

然后执行下面的命令:

1
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

# 2、安装 jenkins

执行安装命令: yum install jenkins
如下图所示,出现询问是否下载时,输入 y,然后点击回车,等待安装完成:
在这里插入图片描述

# 3、修改配置

jenkins 安装成功后,默认的用户是 jenkins,端口是 8080,为了防止冲突,并且给用户赋权限,我们修改用户名和端口。
输入命令,进入 jenkins 配置文件:

1
vi /etc/sysconfig/jenkins

找到如下配置:

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
JENKINS_USER="jenkins"

## Type: string
## Default: "false"
## ServiceRestart: jenkins
#
# Whether to skip potentially long-running chown at the
# $JENKINS_HOME location. Do not enable this, "true", unless
# you know what you're doing. See JENKINS-23273.
#
#JENKINS_INSTALL_SKIP_CHOWN="false"

## Type: string
## Default: "-Djava.awt.headless=true"
## ServiceRestart: jenkins
#
# Options to pass to java when running Jenkins.
#
JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true"

## Type: integer(0:65535)
## Default: 8080
## ServiceRestart: jenkins
#
# Port Jenkins is listening on.
# Set to -1 to disable
#
JENKINS_PORT="8080"

## Type: string
## Default: ""
## ServiceRestart: jenkins
#
# IP address Jenkins listens on for HTTP requests.
# Default is all interfaces (0.0.0.0).
#

# 修改用户名,端口:

在这里插入图片描述

# 若为云服务器,需配置安全组并开放端口才可以正常访问

# 启动 jenkins

# 1. 如果是 2022 年 7 月以后安装的 jekins,需要下载 jdk11 或者 jdk17 版本的 jdk 环境

1
yum install fontconfig java-11-openjdk

# 设置自启后启动 jenkins 服务:systemctl enable --now jenkins

jenkins在linux下安装(rpm包)_javascript_07

# 查看是否自启动:systemctl is-enabled jenkins

jenkins在linux下安装(rpm包)_java_08

# 查看服务状态:systemctl status jenkins.service

jenkins在linux下安装(rpm包)_java_09

# jenkins: failed to find a valid Java installation

jenkins在linux下安装(rpm包)_javascript_10

# 使用以下方法启动 jenkins

1
2
3
4
5
6
7
8
cd /etc/init.d

# 启动
./jenkins start
# 停止
./jenkins stop
# 状态
./jenkins status

# 提示 jdk 版本不满足,可能是 jdk11 没有配置到 jeknins 上

# Jenkins requires Java versions [17, 11] but you are running with Java 1.8 from /usr/local/jdk1.8.0_211/jre

jenkins在linux下安装(rpm包)_json_13

# 将 jdk11 的环境配置到 /etc/rc.d/init.d/jenkins 的 candidates 中

image-20221122105322847

# 重新启动服务,没有报错

image-20221122105452465

# 访问 Jenkins,第一次需要输入生成的密码,在 /var/lib/jenkins/secrets 目录下的 initialAdminPassword 文件中

image-20221122104907426

image-20221122104919096

# 成功

image-20221122105607288

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Flutter集成中国移动一键登录业务

InterviewCoder

# Flutter 集成中国移动一键登录业务

#本文适用于 Flutter 平台开发的小伙伴需要所谓一键登录的业务

https://img.mukewang.com/szimg/5e3fda1b08fe88e203600780.jpg

先贴上链接

中国移动互联网能力开放平台:https://dev.10086.cn

号码认证 Android_5.9.5 接入文档:http://dev.10086.cn/dev10086/pub/loadAttach?attachId=9324E5A5EB8E4DF5BC5118221A93D3ED

移动认证服务端接入文档:https://dev.10086.cn/dev10086/pub/loadAttach?attachId=6EF75FD09D4F40D1973CB7C36C3DB2E2

话不多说,直接上正题,本文比某 SDN 的质量高不知道多少倍呢。

# 1. 准备工作

# 1.1 注册账号并创建一个应用

image-20230112193829020

# 1.2 下载好统一认证 SDK,这里使用的版本是 quick_login_android_5.9.5.jar

请求我的服务器下载:http://43.143.40.221:8080/quick_login_android_5.9.5.rar
下载好后解压

image-20230112194426269

# 1.3 在移动开发平台申请好应用拿到 appid、appkey

image-20230112193937792

# 2. 开始接入

# 2.1 使用 AS 打开你的项目,新建 lib 文件夹,导入 quick_login_android_5.9.5.jar ,右键 asLibirary,导入库

image-20230112194530336

打开项目视图

image-20230112194611295

点击 APP 栏位,出现 quick_login_android_5.9.5.jar 即可

image-20230112194628573

# 2.2 在 app 级别下的 build.gradle 中的 dependencies 栏位导入依赖,使用相对路径引入即可

1
2
3
4
dependencies {
···
implementation files('..\\lib\\quick_login_android_5.9.5.jar')
}

image-20230112194743574

# 2.3 打开 AndroidManifest.xml 在 application 引入 android:networkSecurityConfig="@xml/network_security_config"

image-20230112194847830

同时,在 res 目录下新建一个 xml 目录

新建 network_security_config.xml 文件 引入以下代码

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>

image-20230112195214620

下一步、将 SDK 集成的 GenLoginAuthActivity 引入主文件

1
2
3
4
5
6
7
<activity
android:name="com.cmic.gen.sdk.view.GenLoginAuthActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:screenOrientation="unspecified"
android:theme="@style/AuthPage"
android:launchMode="singleTop">
</activity>

image-20230112195050430

同时,在 res 目录下的两个 values 文件中的 styles.xml 中引入代码:(两个都需要引入)

1
2
3
4
5
<style name="AuthPage" parent="@android:style/Theme.Holo.Light.NoActionBar">
<item name="android:background">@null</item>
<item name="android:colorBackground">@null</item>
<item name="android:windowIsTranslucent">true</item>
</style>

image-20230112194947904

下一步、引入权限代码:

1
2
3
4
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />

image-20230112195305682

至此、主文件配置完成!

# 2.4 将上面压缩包文件中 SDK 提供的 Demo 程序中的 res-umc 文件目录引入到我们的 main 目录中,与 res 同级

image-20230112195514010

将 res-umc 目录下的文件复制进入 res

image-20230112195543408

最终效果:

image-20230112195605424

image-20230112195619339

# 2.5 打开 kotlin 下的 MainActivity.kt ,将代码全部复制进入,如果导不到包可以看看你的 Jar 包有没有成功引入

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
package com.example.mobiledemo

import android.os.Bundle
import android.widget.Toast
import com.cmic.gen.sdk.auth.GenAuthnHelper
import com.cmic.gen.sdk.auth.GenTokenListener
import com.cmic.gen.sdk.view.GenAuthThemeConfig
import io.flutter.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.GeneratedPluginRegistrant
import org.json.JSONObject


class MainActivity : FlutterActivity() {
private var mHelper: GenAuthnHelper? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MethodChannel(
getFlutterEngine()?.getDartExecutor()?.getBinaryMessenger(),
"TYRZ"
).setMethodCallHandler { methodCall, result ->
if (methodCall.method == "loginAuth") {
loginAuth(result)
} else {
Log.e("TYRZ", "notImplemented")
result.notImplemented()
}
}

//创建AuthnHelper实例
mHelper = GenAuthnHelper.getInstance(this)
//打开SDK日志打印开关
GenAuthnHelper.setDebugMode(true)
//初始化授权页主题
mHelper?.setAuthThemeConfig(GenAuthThemeConfig.Builder().build())
getFlutterEngine()?.let { GeneratedPluginRegistrant.registerWith(it) }
}

/**
* 统一认证SDK授权方法调用
*/
private fun loginAuth(result: MethodChannel.Result) {
//调用授权方法,这里要填写的appid、appkey为开发者在移动开发平台申请的appid、appkey
mHelper?.loginAuth(
"300012327504",
"2FDF3FA644E7476FE6733E123D968A82",
object : GenTokenListener {
override fun onGetTokenComplete(i: Int, jsonObject: JSONObject) {
try {
val resultCode = jsonObject.optString("resultCode", "没有返回码!")
Toast.makeText(this@MainActivity, resultCode, Toast.LENGTH_SHORT).show()
//将结果回传给flutter
result.success(resultCode)
} catch (e: Exception) {
e.printStackTrace()
}
}
})
}
}

# 3. 运行调试

3.1 demo 代码如下:

核心逻辑:

1
2
//调用方法通道 TYRZ,即我们在MainActivity中注册的方法,调用loginAuth登录方法
String result = await const MethodChannel("TYRZ").invokeMethod("loginAuth");
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
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key? key, this.title}) : super(key: key);

String? title;

@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
//调用java方法
void loginAuth() async {
print("按钮点击!");
try {
String result = await const MethodChannel("TYRZ").invokeMethod("loginAuth");
//打印统一认证回调的响应码
print("resultCode = " + result);
} catch (e) {
print(e);
}
}

//创建一个按钮,在点击按钮时调用统一认证的loginAuth方法拉起授权页。
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title!),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: const Text("一键登录"),
onPressed: () {
loginAuth();
},
),
],
),
),
);
}
}

# 结果:成功打印

image-20230112200118488

客户端部分就此结束。

# 服务端接入认证并请求移动接口获取用户手机号:

贴上部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String mobile;
HashMap<String, String> res = null;
Map<Object, Object> result = new HashMap<>();

try {
//获取到结果
res = JSONObject.parseObject(post(CHINA_MOBILE_OBTAINS_MOBILE_PHONE_NUMBER, JSONObject.toJSONString(getMD5NoEncryPtionRequestParamMap(token))), HashMap.class);
//结果集判断
if (AssertUtil.isEmpty(res) || !res.containsKey("msisdn")) {
logger.error("一键登录异常:{},{}", res, ResponseCode.MOBILE_LOGIN_EXCEPTION.desc());// 一键登录异常
result.put(ResponseCode.MOBILE_LOGIN_EXCEPTION.code(), ResponseCode.MOBILE_LOGIN_EXCEPTION.desc());
return result;
} else {
//获取到手机号
mobile = res.get("msisdn");
}
} catch (Exception e) {
logger.error("一键登录异常:{},{}", res, ResponseCode.MOBILE_LOGIN_EXCEPTION.desc());// 一键登录异常
result.put(ResponseCode.MOBILE_LOGIN_EXCEPTION.code(), ResponseCode.MOBILE_LOGIN_EXCEPTION.desc());
return result;
}

MD5 加密签名

1
2
3
4
5
6
7
8
9
10
11

/**
* Calculates the MD5 digest and returns the value as a 32 character hex string.
*
* @param data Data to digest
* @return MD5 digest as a hex string
*/
public static String md5Hex(String data) {
return org.apache.commons.codec.digest.DigestUtils.md5Hex(data).toUpperCase();
}

获取请求集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 获取MD5加密方式的无对称加密的请求集合
*
* @param token
* @return
*/
private HashMap<String, String> getMD5NoEncryPtionRequestParamMap(String token) {
HashMap<String, String> param = new HashMap<>();
String msgId = UserUtil.createUUID();
String time = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
param.put("version", "2.0");
param.put("msgid", msgId);
param.put("systemtime", time);
param.put("strictcheck", "0");
param.put("appid", appId);
param.put("token", token);
param.put("encryptionalgorithm", "");
param.put("sign", md5Hex(appId + "2.0" + msgId + time + "0" + token + APPSecret));
return param;
}

post 请求方法:

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
public static String post(String URL, String json) {
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(URL);
post.setHeader("Content-Type", "application/json");
String result = "";
try {

StringEntity s = new StringEntity(json, "UTF-8");
s.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
"application/json"));
post.setEntity(s);
// 发送请求
HttpResponse httpResponse = client.execute(post);
// 获取响应输入流
InputStream inStream = httpResponse.getEntity().getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(
inStream, "utf-8"));
StringBuilder strber = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null)
strber.append(line + "\n");
inStream.close();
result = strber.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
return result;
}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

使用docker搭建开发环境记录

InterviewCoder

# 使用 docker 搭建开发环境记录

1
2
3
4
5
docker start 容器名
docker stop 容器名
docker restart 容器名

docker run = docker create + docker start

# mysql

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
# 拉取镜像
docker pull mysql:5.7

# 创建实例并启动
# -p 映射端口
# --name 名称
# -v 映射文件
# -e MYSQL_ROOT_PASSWORD mysql密码
# -d 后台运行并运行
docker run -p 3306:3306 --name mysql -v /mydata/mysql/log:/var/log/mysql -v /mydata/mysql/data:/var/lib/mysql -v /mydata/mysql/conf:/etc/mysql -e MYSQL_ROOT_PASSWORD=root -d mysql:5.7

# 进入容器
docker exec -it mysql /bin/bash
# 退出容器
exit

# 配置my.conf
## 1.在容器外编辑配置文件
vi /mydata/mysql/conf/my.cnf

[client]
default-character-set=utf8
[mysql]
default-character-set=utf8
[mysqld]
init_connect='SET conllation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve
## 2. 重启mysql容器
docker restart mysql
## 3. 进入mysql容器验证
docker exec -it mysql /bin/bash
cat /etc/mysql/my.cnf

# Redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 拉取镜像
docker pull redis

# 配置文件
本地:/mydata/redis/data/redis.conf 提前准备好
主要配置
bind 127.0.0.1 #注释掉这部分,使redis可以外部访问
daemonize no#用守护线程的方式启动
requirepass 你的密码#给redis设置密码
appendonly yes#redis持久化  默认是no
tcp-keepalive 300 #防止出现远程主机强迫关闭了一个现有的连接的错误 默认是300

# 创建实例并启动
docker run -p 6379:6379 --name redis -v /mydata/redis/data/redis.conf:/etc/redis/redis.conf -v /mydata/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes

# Rabbitmq

1
2
3
4
5
# 拉取镜像management带控制台
docker pull rabbitmq:management

# 创建实例并启动
docker run -d --hostname my-rabbit --name rabbit -v /mydata/rabbitmq:/var/lib/rabbitmq -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=Mm_123456 -p 15672:15672 -p 5672:5672 rabbitmq:management

# mongo

1
2
3
4
5
# 拉取镜像
docker pull mongo

# 创建实例并启动
docker run -p 27017:27017 -v /mydata/mongo:/data/db --name mongodb -d mongo

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

MySQL8下载与安装教程

InterviewCoder

# MySQL 下载与安装(8.0.20 版)教程

MySQL 官网:链接

直接点击链接也可以下载:mysql 8.0.20

登录官网后如下图下载 MySQL 软件:

MySQL下载与安装

点击 1 和 2 后进入下图页面:

MySQL下载与安装

再点击 MySQL Community Server 进入下图下载页面:

MySQL下载与安装

根据上图选择好 1 处后点击 2 处进入 Windows 安装包的下载页面 (如下图):

mysql80.20安装教程

上图中有两个下载包,一个是安装引导包,一个是压缩安装包,我们选择第二个。点击 Download 进入下图页面。

mysql80.20安装教程

我们不登陆下载,所以直接点击红框按钮进行下载。这样我们就把软件下载下来了。

安装:

双击上面下载的软件,打开下图页面。

mysql80.20安装教程

点击 Next, 转到下图页面。

mysql80.20安装教程

如果大红框上面有些没有安装的软件会出现这里,可以点击 Next 左边的 Excute 按钮安装,安装完成之后点击 Next 按钮,会出现个弹框,如下图。

mysql80.20安装教程

点击 Yes。弹出下图页面。

mysql80.20安装教程

点击 Execute 进行安装。等待所有安装完成。

mysql80.20安装教程

至少保证红杠处的 3 项安装成功,假若有安装失败的可以卸载重新安装。安装成功后我们点击 Next。

mysql80.20安装教程

点击 Next。

mysql80.20安装教程

点击 Next。

mysql80.20安装教程

点击 Next。

img

在红框里输入账号和密码,账号密码必须大于或等于 4 个字符,点击 Next。

img

点击 Next。

img

点击 Excute。

img

点击 Finish。

img

点击 Next。

img

点击 Finish。

img

点击 Next。

img

输入密码后点击 Check, 再点击 Next。

img

点击 Execute。

img

点击 Finish。

mysql80.20安装教程

点击 Next。

mysql80.20安装教程

点击 Finish。

到此位置 MySQL 就安装完成了。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!