Flutter GETX框架

InterviewCoder

​ GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。

​ 与其说是一个状态管理库,倒不如是是一个简化 Flutter 开发的百宝箱。它提供了很多工具来简化我们的开发,本篇我们先对 GetX 有一个大概的认识,然后接下来的篇章再将 GetX 的具体应用。

# GetX 工具介绍

官方文档给出关于 GetX 的介绍如下:

GetX is an extra-light and powerful solution for Flutter. It combines high-performance state management, intelligent dependency injection, and route management quickly and practically. GetX 是一个超轻量且强大的 Flutter 应用解决方案。它组合了高性能的状态管理、智能的依赖注入以及快速可用的路由管理。

而实际上,GetX 还有更多的小工具,示例如下:

# 状态管理

  • Obx 是配合 Rx 响应式变量使用、GetBuilder 是配合 update 使用:请注意,这完全是俩套定点刷新控件的方案。
    区别:前者响应式变量变化,Obx 自动刷新;后者需要使用 update 手动调用刷新
  • 每一个响应式变量,都需要生成对应的 GetStream,占用资源大于基本数据类型,会对内存造成一定压力
  • GetBuilder 内部实际上是对 StatefulWidget 的封装,所以占用资源极小(推荐使用)

# 控制器的注入

  • 静态路由绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class AsWorkStatisticsBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<AsWorkStatisticsController>(() => AsWorkStatisticsController());
}
}

static final List<GetPage> routes = [
GetPage(
name: workStatisticsPage,
page: () => const AsWorkStatisticsPage(),
binding: AsWorkStatisticsBinding(),
),
];
Get.toNamed(ASRouteConfig.workPlanDetailPage);
  • 动态路由绑定
1
Get.to(AsWorkStatisticsPage(),binding: AsWorkStatisticsBinding());
  • 页面注入
1
Get.lazyPut<AsWorkStatisticsController>(() => AsWorkStatisticsController());

# 动态 / 简单路由和静态 / 命名路由

请注意命名路由,只需要在 api 结尾加上 Named 即可,举例:

  • 默认:Get.to(SomePage());
  • 命名路由:Get.toNamed (“/somePage”);
  • 导航到新的页面
1
2
Get.to(NextScreen());
Get.toNamed("/NextScreen");
  • 关闭 SnackBars、Dialogs、BottomSheets 或任何你通常会用 Navigator.pop (context) 关闭的东西
1
Get.back();
  • 进入下一个页面,但没有返回上一个页面的选项(用于 SplashScreens,登录页面等)
1
2
Get.off(NextScreen());
Get.offNamed("/NextScreen");
  • 进入下一个界面并取消之前的所有路由(在购物车、投票和测试中很有用)
1
2
Get.offAll(NextScreen());
Get.offAllNamed("/NextScreen");
  • 发送数据到其它页面

只要发送你想要的参数即可。Get 在这里接受任何东西,无论是一个字符串,一个 Map,一个 List,甚至一个类的实例。

1
2
Get.to(NextScreen(), arguments: 'Get is the best');
Get.toNamed("/NextScreen", arguments: 'Get is the best');

在你的类或控制器上。

1
2
print(Get.arguments);
//print out: Get is the best
  • 要导航到下一条路由,并在返回后立即接收或更新数据
1
2
var data = await Get.to(Payment());
var data = await Get.toNamed("/payment");
  • 在另一个页面上,发送前一个路由的数据
1
2
3
Get.back(result: 'success');
// 并使用它,例:
if(data == 'success') madeAnything();
  • 跳转重复页面,可以这样写
1
2
3
Get.to(XxxxPage(), preventDuplicates: false);
// 或者
Get.toNamed('xxx', preventDuplicates: false);
  • 如果你不想使用 GetX 语法,只要把 Navigator(大写)改成 navigator(小写),你就可以拥有标准导航的所有功能,而不需要使用 context,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 默认的Flutter导航
Navigator.of(context).push(
context,
MaterialPageRoute(
builder: (BuildContext context) {
return HomePage();
},
),
);

// 使用Flutter语法获得,而不需要context。
navigator.push(
MaterialPageRoute(
builder: (_) {
return HomePage();
},
),
);

// get语法
Get.to(HomePage());

# GetView 的使用

GetView 只是对已注册的 Controller 有一个名为 controller 的 getter 的 const Stateless 的 Widget,如果我们只有单个控制器作为依赖项,那我们就可以使用 GetView,而不是使用 StatelessWidget,并且避免了写 Get.Find ()。

GetView 的使用方法非常简单,只是要将你的视图层继承自 GetView 并传入需要注册的控制器并 Get.put () 即可:

1
2
3
4
5
6
7
8
9
class GetViewAndGetWidgetExample extends GetView<GetViewCountController> {
@override
Widget build(BuildContext context) {

Get.put(GetViewCountController());

return Container();
}
}

# 路由

路由支持命名路由和匿名路由:

1
2
3
4
5
6
7
8
9
10
11
Get.to(() => Home());
Get.toNamed('/home');
// 返回上一个页面
Get.back();
// 使用下一个页面替换
Get.off(NextScreen());
// 清空导航堆栈全部页面
Get.offAll(NextScreen());
// 获取命名路由参数
print(Get.parameters['id']);
print(Get.parameters['name']);

GetX 的路由好处是不依赖于 context ,十分简洁,更多路由介绍可以参考:GetX 路由介绍官方文档

# SnackBar

Flutter 自身携带的 SnackBar 有很多限制,而 GetX 的非常简单,当然也有更多的样式配置和位置配置参数。

1
Get.snackbar('SnackBar', '这是GetX的SnackBar');

# 对话框

对话框也一样,默认的对话框开箱即用。

1
2
3
4
5
6
7
8
9
10
11
Get.defaultDialog(
title: '对话框',
content: Text('对话框内容'),
onConfirm: () {
print('Confirm');
Get.back();
},
onCancel: () {
print('Cancel');
},
);

# 内存缓存

GetX 可以缓存内容对象,以便在不同页面共享数据。使用的时候需要注意,需要先 put 操作再 find 操作,否则会抛异常。

1
2
Get.put(CacheData(name: '这是缓存数据'));
CacheData cache = Get.find();

# 离线存储

GetX 提供了一个 get_storage 插件用于离线存储,与 shared_preferences 相比,其优点是纯 Dart 编写,不依赖于原生,因此可以在安卓、iOS、Web、Linux、Mac 等多个平台使用。 GetStorage 是基于内存和文件存储的,当内存容器中有数据时优先从内存读取。同时在构建 GetStorage 对象到时候指定存储的文件名以及存储数据的容器。

1
2
3
GetStorage storage = GetStorage();
storage.write('name', '岛上码农');
storage.read('name');

# 更改主题

可以说是一行代码搞定深色和浅色模式,也可以更改为自定义主题 —— 老板让你根据手机壳改主体颜色的需求已经搞定了一大半了!

1
2
3
Get.changeTheme(
Get.isDarkMode ? ThemeData.light() : ThemeData.dark());
},

img

# 多语言支持

多语言支持使用数据字典完成,在 GetMaterialApp 指定字典对象(继承自 Translations ),使用字符串的时候假设 .tr 后缀,就可以在切换语言的时候自动切换字符串对应语言的翻译了。

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
class GetXDemo extends StatelessWidget {
// 省略其他代码
TextButton(
onPressed: () {
var locale = Locale('en', 'US');
Get.updateLocale(locale);
},
child: Text('name'.tr),
),
}

class Messages extends Translations {
@override
Map<String, Map<String, String>> get keys => {
'en_US': {
'name': 'Island Coder',
},
'zh_CN': {
'name': '岛上码农',
}
};
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return GetMaterialApp(
translations: Messages(),
locale: Locale('zh', 'CN'),
color: Colors.white,
navigatorKey: Get.key,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
brightness: Brightness.light,
),
home: GetXDemo(),
);
}
}

# GetX 的理念

GetX 有三个基本的理念,分别是性能、生产力和组织性(Organization)。

  • 性能(Performance):GetX 关注性能并最小化资源消耗。GetX 不使用 StreamChangeNotifier
  • 生产力(Productivity):GetX 使用简洁愉悦的语法。不管你要做什么,使用 GetX 都会觉得简便。这使得开发的时间大大节省,并且保证应用性能的最大化。通常来说,开发者需要关注从内存中移除控制器。而使用 GetX 的时候,则无需这么做。当控制器不被使用的时候,资源会自动从内存中释放。如果确实需要常住内存,那就需要在依赖中声明 permanent:true 。通过这种方式,可以降低内存中有过多不必要依赖对象的风险。同时,依赖默认也是懒加载。
  • 组织性(Organization):GetX 可以将视图、展示逻辑、业务逻辑、依赖注入和导航完全解耦。路由之间跳转无需 context ,因此我们的导航不会依赖组件树。也不需要使用通过 InheritedWidgetcontext 访问控制器或 BLOC 对象,因此可以将展示逻辑和业务逻辑从虚拟的组件层分离。我们也不需要像 MultiProvider 那样往组件树中注入 Controller/Model/Bloc 等类对象。因此可以将依赖注入和视图分离。

# GetX 生态

GetX 有很多特性,使得编码变得容易。每个特性之间是相互独立的,并且只会在使用的时候才启动。例如,如果仅仅是使用状态管理,那么只有状态管理会被编译。而如果只使用路由,那么状态管理的部分就不会编译。

GetX 有一个很大的生态,包括了大型的社区维护,大量的协作者(GitHub 上看有 132 位),并且承诺只要 Flutter 存在就会继续维护下去。而且 GetX 兼容 Android, iOS, Web, Mac, Linux, Windows 多个平台。GetX 甚至还有服务端版本 Get_Server(感觉 Flutter 要一统程序员界啊,啥时候支持鸿蒙?)。

为了简化开发,GetX 还提供了脚手架工具 **GET_CLI** 和 VSCode 插件 GetX Snippets (也有 Android Studio 和 Intellij 插件)。提供了如下快速代码模板:

  • getmain:GetX 的 main.dart 代码;

  • getmodel:Model 类代码,包括了 fromJson 和 toJson 方法

  • 其他,输入 getxxxx 根据提示生成即可,具体参考:GetX Snippets 介绍

# 总结

本篇对 GetX 插件做了简单的介绍,可以看到 GetX 的生态确实很丰富,感觉是一个集大成者,GetX 基本上涵盖了 Flutter 应用开发的很大一部分,如路由、主题、多语言、弹层、状态管理、依赖注入、网络请求封装等等。GetX 看着像一个框架, 但实际上它的各个模块是独立的,其实是一个工具箱。对于开发的时候,可以用它的全家桶,也可以从中任取所需的模块到我们的应用中使用。

# 关于我

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

InterviewCoder

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

【DDD】DDD分层架构与传统三层架构的区别

InterviewCoder

# 【DDD】DDD 分层架构与传统三层架构的区别

# 文章目录

# DDD 分层与传统三层区别

根据 DDD 领域驱动设计原则,对应的软件架构也需要做出相应的调整。
我们常用的三层架构模型划分为表现层,业务逻辑层,数据访问层等,在 DDD 分层结构中既有联系又有区别,
个人认为主要有如下异同:

  • 在架构设计上,在 DDD 分层结构中将传统三层架构的业务逻辑层拆解为应用层和领域层
    其中 Application 划分为很薄的一层服务,非核心的逻辑放到此层去实现,核心的业务逻辑表现下沉到领域层去实现,凝练为更为精确的业务规则集合,通过领域对象去阐述说明。
    与传统三层区别
  • 在建模方式上, DDD 分层的建模思维方式有别于传统三层
    传统三层通常是以数据库为起点进行数据库分析设计,而 DDD 则需要以业务领域模型为核心建模(即面向对象建模方式),更能体现对现实世界的抽象。
    在 DDD 分层凸显领域层的重要作用,领域层为系统的核心,包括所有的业务领域模型的抽象表达
  • 在职责划分上,基础设施层涵盖了 2 方面内容
    • 持久化功能,其中原三层架构的数据访问层下沉到基础设施层的持久化机制实现
    • 通用技术支持,一些公共通用技术支持也放到基础设施层去实现。

# DDD 分层详解

# 四层架构图

在这里插入图片描述

在该架构中,上层模块可以调用下层模块,反之不行。即

  • Interface ——> application | domain | infrastructure
  • application ——> domain | infrastructure
  • domain ——> infrastructure

# 分层作用

分层 英文 描述
表现层 User Interface 用户界面层,或者表现层,负责向用户显示解释用户命令
应用层 Application Layer 定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识。
领域层 Domain Layer 或称为模型层,系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手
基础设施层 Infrastructure Layer 主要有 2 方面内容,一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;一是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;

# 领域对象

根据战术设计,关注的领域对象主要包括有

类型 英文 描述
值对象 value object 无唯一标识的简单对象
实体 entity 充血的领域模型,有唯一标识
聚合(聚合根) aggregate 实体的聚合,拥有聚合根,可为某一个实体
领域服务 service 无法归类到某个具体领域模型的行为
领域事件 event 不常用
仓储 repository 持久化相关,与基础设施层关联
工厂 factory 负责复杂对象创建
模块 module 子模块引入,可以理解为子域划分

# DDD 编码实践(改进分层)

本文在对上述的传统四层的实践中,(1)根据 依赖倒置原则 对分层结构进行了改进,通过改变不同层的依赖关系(即将基础设施层倒置)来改进具体实现与抽象之间关系;(2)在基础设施层中增加 引用适配层 (防腐层)来增强防御策略,用来统一封装外部系统接口的引用。改进的分层结构如下:
在这里插入图片描述

依赖倒置原则(DIP):

  • 高层模块不依赖于低层模块,两者都依赖于抽象;
  • 抽象不应该依赖于细节,细节应依赖抽象

# 代码结构描述

eg. 后端 Java 代码工程为例,
表现层 在此代码结构中表现为 api层 ,对外暴露接口的最上层

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
├─com.company.microservice
├─com.company.microservice
│ │
│ ├─apis API接口层
│ │ └─controller 控制器,对外提供(Restful)接口
│ │
│ ├─application 应用层
│ │ ├─model 数据传输对象模型及其装配器(含校验)
│ │ │ ├─assembler 装配器,,实现模型转换eg. apiModel<=> domainModel
│ │ │ └─dto 模型定义(含校验规则)
│ │ ├─service 应用服务,非核心服务,跨领域的协作、复杂分页查询等
│ │ ├─task 任务定义,协调领域模型
│ │ ├─listener 事件监听定义
│ │ └─*** others
│ │
│ ├─domain 领域层
│ │ ├─common 模块0-公共代码抽取,限于领域层有效
│ │ ├─module-xxx 模块1-xxx,领域划分的模块,可理解为子域划分
│ │ ├─module-user 模块2-用户子域(领域划分的模块,可理解为子域划分)
│ │ │ ├─action 行为定义
│ │ │ │ ├─UserDomainService.java 领域服务,用户领域服务
│ │ │ │ ├─UserPermissionChecker.java 其他行为,用户权限检查器
│ │ │ │ ├─WhenUserCreatedEventPublisher.java 领域事件,当用户创建完成时的事件
│ │ │ ├─model 领域聚合内模型
│ │ │ │ ├─UserEntity.java 领域实体,有唯一标识的充血模型,如本身的CRUD操作在此处
│ │ │ │ ├─UserDictVObj.java 领域值对象,用户字典kv定义
│ │ │ | ├─UserDPO.java 领域负载对象
│ │ │ ├─repostiory 领域仓储接口
│ │ │ │ ├─UserRepository.java
│ │ │ ├─reference 领域适配接口
│ │ │ │ ├─UserEmailSenderFacade.java
│ │ │ └─factory 领域工厂
│ │
│ ├─infrastructure 基础设施层
│ │ ├─persistence 持久化机制
│ │ │ ├─converter 持久化模型转换器
│ │ │ ├─po 持久化对象定义
│ │ │ └─repository.impl 仓储类,持久化接口&实现,可与ORM映射框架结合
│ │ ├─general 通用技术支持,向其他层输出通用服务
│ │ │ ├─config 配置类
│ │ │ ├─toolkit 工具类
│ │ │ ├─extension 扩展定义
│ │ │ └─common 基础公共模块等
│ │ ├─reference 引用层,包装外部接口用,防止穿插到Domain层腐化领域模型等
│ │ │ ├─dto 传输模型定义
│ │ │ ├─converter 传输模型转换器
│ │ │ └─facade.impl 适配器具体实现,此处的RPC、Http等调用
│ │
│ └─resources
│ ├─statics 静态资源
│ ├─template 系统页面
│ └─application.yml 全局配置文件

其中在上述目录结构中,Domain 层中为对 module 进行划分,实际上默认该层只有一个模块,根据微服务划分可以进行增加模块来规范代码结构。
示例代码工程:

GITHUB 地址:https://github.com/smingjie/bbq-ddd.git

# 扩展定义注解和接口声明

(1) 自定义注解 :在使用 DDD 中自定义了标记的注解 (@DDDAnnotation) 和其衍生子注解,分别是

  • @DomainAggregate
  • @DomainAggregateRoot
  • @DomainEntity
  • @DomainValueObject
  • @DomainService
  • @DomainRepository
  • @DomainEvent
  • @ApplicationService
  • @DomainAssembler
  • @DomainConverter

等注解,详见代码的 infrastructure.general.extension.ddd.annotation.**;其中有些注解继承了 spring 的 @Component , 将会自动注册为 spring bean,有些注解为了标记用于后续扩展;

引入了 Assembler 装配器 / Converter 转换器,通过组合模式解耦继承关系,在 api 层和持久化层都有相应的实现。

(2) 自定义接口 :在 domain.common 定义了部分通用的 契约 接口,如领域对象元数据获取接口 IDomainMetaData ,通过接口解耦继承关系。其他还有: IDomainSaveOrUpdate IDomainDelete ... 等 Command

# 领域模型注入仓储类的问题

区别于传统的分层后,在 domain 中更多关注业务逻辑,考虑到要与 spring 框架集成,需要注意一个领域模型中注入仓储类的问题

在传统分层中,controller,service,repo 均注册为 spring 管理的 bean,
但是在 domain 层中,service 一部分的业务逻辑划分到了具体的领域对象中去实现了,显然这些对象却不能注册为单例 bean,
因此在此处不能沿用与原来分层结构中 service 层中通过 @Autowired or @Resource 等注入仓储接口,

关于这个问题,此处建议使用 ApplicationContext 实现

即通过一个工具类 ApplicationContextUtils 实现 ApplicationContextAware 获取 bean 的方法,即 getBean() 方法,
然后我们就可以在我们的领域模型中直接应用该工具类来获取 Spring 托管的 singleton 对象,即
xxxRepo=ApplicationContextUtils.getBean (“xxxRepository”)

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
@Component
public class ApplicationContextUtils implements ApplicationContextAware {

public static ApplicationContext appctx;

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

/**
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return appctx;
}

/**
* 获取对象
*
* @param name spring配置文件中配置的bean名或注解的名称
* @return 一个以所给名字注册的bean的实例
* @throws BeansException 抛出spring异常
*/
public static <T> T getBean(String name) throws BeansException {
return (T) appctx.getBean(name);
}

/**
* 获取类型为requiredType的对象
*
* @param clazz 需要获取的bean的类型
* @return 该类型的一个在ioc容器中的bean
* @throws BeansException 抛出spring异常
*/
public static <T> T getBean(Class<T> clazz) throws BeansException {
return appctx.getBean(clazz);
}

/**
* 如果ioc容器中包含一个与所给名称匹配的bean定义,则返回true否则返回false
*
* @param name ioc容器中注册的bean名称
* @return 存在返回true否则返回false
*/
public static boolean containsBean(String name) {
return appctx.containsBean(name);
}
}

考虑到代码结构简洁性,还可以封装一层仓储工厂,只用来获取相应的仓储 Bean

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
/**
* 简化版的仓储工厂--用来统一获取仓储的实现Bean
*
* @author jockeys
* @date 2020/9/12
*/
public class RepoFactory {

/**
* 根据仓储接口类型获取对应实现且默认取值第一个
*
* @param tClass 具体目标类型
* @param <T> 仓储接口类型
* @return 如果不是指定实现,默认获得第一个实现Bean
*/
public static <T> T get(Class<? extends T> tClass) {

Map<String, ? extends T> map = ApplicationUtils.getApplicationContext().getBeansOfType(tClass);
Collection<? extends T> collection = map.values();
if (collection.isEmpty()) {
throw new PersistException("未找到仓储接口或其指定的实现:" + tClass.getSimpleName() );
}
return collection.stream().findFirst().get();
}
}

然后在领域模型中就可以直接调用该工厂方法来获取仓储接口的实现,
比如 DictRepo 为定义的仓储接口, DictDao 为该接口的准实现类

1
2
3
4
//直接指定实现
DictRepo repo= RepoFactory.get(DictDao.class);
//不指定实现取Spring容器中默认注册第1位的Bean
DictRepo repo= RepoFactory.get(DictRepo.class);

# 一些个人思考…

上述经典四层架构,笔者更愿意理解为 DDD 在编码实现阶段的一个体现或应用。

补充一点:DDD 除了在编码实践阶段,还体现在需求分析、设计阶段等过程,DDD 推荐不割裂系统的需求和设计,我们这里可以合并称作系统建模过程,可参考 DDD - 建模过程分析一文,不再赘述。

当然除了这个经典四层架构模型,DDD 还有五层架构、六边形架构等,所以这里抛出一个问题,

# 项目按上述经典四层架构进行搭建,可以说是 DDD 架构实践么?

关于这个问题,笔者想引入一对哲学概念,哲学有言形式与内容,现象与本质等辩证关系(当然与本文可能也没啥太大关系啦);从这两个角度来阐述本人的观点:

  • 形式与内容:经典四层架构是一个 DDD 实现的形式,相当于给我们提供了一个框框来让我们自己去实现;在这个框框里面我们怎么实现是自由发挥的,但也是有约束的,这个约束体现在 DDD 对每一层的作用的约定,如每个层约定做了什么功能,充当什么角色等。尤其是对 Domain 层的约定,才是最重要的。那么我们按照哲学辩证的套话来说,形式上满足了 DDD 架构,但这应该是片面的,具体还要看内容,即具体实现是怎样的。

  • 现象与本质:接着上述观点,如果要看实现,就要具体分析一下现象与本质嘞。上面笔者也有提到,DDD 除了四层经典架构,还有五层架构(包括其演化的多层架构)、六边形架构等也都是 DDD 提供的架构模型(形式),那这些都可以理解 DDD 架构模式的外显形式,那么又有哪些共性呢?可自行查询,本文直接给结论,即

    它们都有 Domain 层,Domain 层,Domain 层

    (重要的事情说三遍~~,该结论 DDD 作者译著有写到…),所以不管架构模式怎么演化,Domain 是核心不能变。

    那么如上分析,我们在回到这个问题,我们是不是可以给出一个这样的答案:

    形式上符合 DDD 架构,具体是不是 DDD 的架构实践,本质上还要看

    • (1)项目是否包括有 Domain 层;
    • (2)Domain 层是否满足 DDD 战术篇的要求(或者可暂时简单理解为充血模型吧)

# 题外话:Spring 与 DDD

  • Spring 框架中,Spring 为我们提供了 @Service @Repository 等注解,为我们分离行为和行为(注册为 Bean)和属性(数据模型),同时通过 @Autowired 在合适地方进行注入行为,因为行为被注册为 Spring 容器中的 Bean 后,减少了频繁创建行为的开销,只有属性的数据模型作为数据的载体来传递数据。提供很大的便捷性。但也阻碍了我们应用 DDD 编码实践, Spring 框架主张分离,DDD 思想主张合并,我们在 Spring 框架中使用 DDD 则需要在其基础上进行一些权衡取舍,即 如何将注册为 Bean 的行为穿插到原有的贫血模型中来构建充血模型是我们要解决的问题
  • 关于这个问题,笔者使用了 Spring 框架提供的获取容器内已经注册的 Bean 接口,直接调用接口,在有属性的领域模型中来获取行为;主要还是体现融入领域模型中的部分 Service 获取仓储接口来实现持久化过程。

当然,上述的说明都是从一个软件开发人员的角度来阐述说明 DDD 在编码实践阶段的应用 。
除此之外在业务领域的建模分析过程中也可引入该概念。
比如我们现在所倡导的微服务化,如何划分或拆分微服务;如何有效地区分限界上下文,划分子域;如何构建一个有效的聚合,识别聚合根等。。。

【SpringBoot】SpringBoot3.0 云原生时代即将来临!

InterviewCoder

# 【SpringBoot】SpringBoot3.0 云原生时代即将来临!

云原生时代的Spring Boot 3.0: GraalVM原生镜像,启动速度提升近30倍

# 云原生时代的 Spring Boot 3.0: GraalVM 原生镜像,启动速度提升近 30 倍

InterviewCoder

# Spring Boot 3.0 于(2022 年 11 月 24 日)发布,变化很大,基于 spring6.0,spring6.0 是 Spring 下一个未来十年的新开端。

# JAVA 17

Spring Boot 3.0 版本最低支持 Java17,Springboot 2.7.3 最常用的 jdk 版本是 Java 8,现在直接跳了 9 个版本直接从 8 跳到了 17,且是强制要求,必须 17 或 17 以上的 java 版本。所以以后开发可以用上 17 或 17 以上的 Java 语言的新特性。

# Spring Native

Spring Native 也是升级的一个重大特性,支持使用 GraalVM 将 Spring 的应用程序编译成本地可执行的镜像文件,可以显著提升启动速度、峰值性能以及减少内存使用。

我们传统的应用都是编译成字节码,然后通过 JVM 解释并最终编译成机器码来运行,而 Spring Native 则是通过 AOT 提前编译为机器码,在运行时直接静态编译成可执行文件,不依赖 JVM。

img

# Jakarta EE

JavaEE 改名之后就叫 JakartaEE,比如我们之前的 javax.servlet 包现在就叫 jakarta.servlet。也因此,代码中所有使用到比如 HttpServletRequest 对象的 import 都需要修改。

1
2
3
import javax.servlet.http.HttpServletRequest;
// 改为
import jakarta.servlet.http.HttpServletRequest;

# Spring Boot 3.0 初步使用(Windows)

创建 Spring Boot 3.0 项目有两种方式,一种是 Idea 直接创建。

img

img

若 IDE 不是最新版本,不支持创建 Spring Boot 3.0,还有第二种方式创建 Spring Boot 3.0 项目,登录官网 https://start.spring.io/ 生成 Spring Boot 3.0 初始项目。

img

下面是 Spring Boot 3.0 的最小 pom 文件内容:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>

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

</project>

运行 spring boot 项目,需要安装开发环境,spring boot 3.0 开始不用 jdk 了,取而代之的是 graalvm,且最低版本要求是 java17 graalvm 版本。

https://github.com/graalvm/graalvm-ce-builds/releases 下载对应操作系统的 java17 graalvm 版本。

1
2
3
4
PS C:\Users\hanwei> java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.0 (build 17.0.5+8-jvmci-22.3-b08, mixed mode, sharing)

在最小 Spring Boot 项目源码的基础上了个简单的 controller。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class QuickStartController {
@RequestMapping("/test")
@ResponseBody
public String test(){
return "springboot 3.0 访问测试";
}

@RequestMapping("/hello")
@ResponseBody
public String home(){
return "Hello World from springboot 3.0!";
}
}

用 java17 graalvm 编译运行 demo 项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
C:\sdk\graalvm-ce-java17-22.3.0\bin\java.exe -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2\lib\idea_rt.jar=9872:C:\Program Files\JetBrains\IntelliJ IDEA 2022.2\bin" -Dfile.encoding=UTF-8 -classpath C:\Users\hanwei\Documents\JavaProject\demo-maven\target\classes;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-starter\3.0.0\spring-boot-starter-3.0.0.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot\3.0.0\spring-boot-3.0.0.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-context\6.0.2\spring-context-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\3.0.0\spring-boot-autoconfigure-3.0.0.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-starter-logging\3.0.0\spring-boot-starter-logging-3.0.0.jar;C:\Users\hanwei\.m2\repository\ch\qos\logback\logback-classic\1.4.5\logback-classic-1.4.5.jar;C:\Users\hanwei\.m2\repository\ch\qos\logback\logback-core\1.4.5\logback-core-1.4.5.jar;C:\Users\hanwei\.m2\repository\org\apache\logging\log4j\log4j-to-slf4j\2.19.0\log4j-to-slf4j-2.19.0.jar;C:\Users\hanwei\.m2\repository\org\apache\logging\log4j\log4j-api\2.19.0\log4j-api-2.19.0.jar;C:\Users\hanwei\.m2\repository\org\slf4j\jul-to-slf4j\2.0.4\jul-to-slf4j-2.0.4.jar;C:\Users\hanwei\.m2\repository\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-core\6.0.2\spring-core-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-jcl\6.0.2\spring-jcl-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\yaml\snakeyaml\1.33\snakeyaml-1.33.jar;C:\Users\hanwei\.m2\repository\org\slf4j\slf4j-api\2.0.4\slf4j-api-2.0.4.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-starter-web\3.0.0\spring-boot-starter-web-3.0.0.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-starter-json\3.0.0\spring-boot-starter-json-3.0.0.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.14.1\jackson-databind-2.14.1.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.14.1\jackson-annotations-2.14.1.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.14.1\jackson-core-2.14.1.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.14.1\jackson-datatype-jdk8-2.14.1.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.14.1\jackson-datatype-jsr310-2.14.1.jar;C:\Users\hanwei\.m2\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.14.1\jackson-module-parameter-names-2.14.1.jar;C:\Users\hanwei\.m2\repository\org\springframework\boot\spring-boot-starter-tomcat\3.0.0\spring-boot-starter-tomcat-3.0.0.jar;C:\Users\hanwei\.m2\repository\org\apache\tomcat\embed\tomcat-embed-core\10.1.1\tomcat-embed-core-10.1.1.jar;C:\Users\hanwei\.m2\repository\org\apache\tomcat\embed\tomcat-embed-el\10.1.1\tomcat-embed-el-10.1.1.jar;C:\Users\hanwei\.m2\repository\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.1\tomcat-embed-websocket-10.1.1.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-web\6.0.2\spring-web-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-beans\6.0.2\spring-beans-6.0.2.jar;C:\Users\hanwei\.m2\repository\io\micrometer\micrometer-observation\1.10.2\micrometer-observation-1.10.2.jar;C:\Users\hanwei\.m2\repository\io\micrometer\micrometer-commons\1.10.2\micrometer-commons-1.10.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-webmvc\6.0.2\spring-webmvc-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-aop\6.0.2\spring-aop-6.0.2.jar;C:\Users\hanwei\.m2\repository\org\springframework\spring-expression\6.0.2\spring-expression-6.0.2.jar com.example.demo.DemoApplication

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0)

2022-11-29T10:19:58.816+08:00 INFO 20116 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 20116 (C:\Users\hanwei\Documents\JavaProject\demo-maven\target\classes started by hanwei in C:\Users\hanwei\Documents\JavaProject\demo-maven)
2022-11-29T10:19:58.818+08:00 INFO 20116 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-11-29T10:19:59.501+08:00 INFO 20116 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-11-29T10:19:59.510+08:00 INFO 20116 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-11-29T10:19:59.510+08:00 INFO 20116 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-11-29T10:19:59.594+08:00 INFO 20116 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-11-29T10:19:59.594+08:00 INFO 20116 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 737 ms
2022-11-29T10:19:59.866+08:00 INFO 20116 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-29T10:19:59.872+08:00 INFO 20116 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.386 seconds (process running for 2.036)

rest api 测试 ok。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
http://localhost:8080/hello

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 32
Date: Mon, 28 Nov 2022 08:31:59 GMT
Keep-Alive: timeout=60
Connection: keep-alive

Hello World from springboot 3.0!

Response code: 200; Time: 175ms (175 ms); Content length: 32 bytes (32 B)
http://localhost:8080/test

HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 27
Date: Mon, 28 Nov 2022 08:32:05 GMT
Keep-Alive: timeout=60
Connection: keep-alive

springboot 3.0 访问测试

Response code: 200; Time: 17ms (17 ms); Content length: 19 bytes (19 B)

打包二进制可执行文件需要安装 native-image , 执行 gu install native-image 命令。

打包二进制可执行文件,执行出错:

img

上面报错,因为需要安装 windows docker。

官网下载安装 windows dockerdesktop 完成后。启动 windows dockerdesktop 报错(可能是因为 Windows 11 的默认网络配置被改了,正常不会报错,毕竟是很常用的工具):

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
stderr: 
在 Docker.ApiServices.WSL2.WslShortLivedCommandResult.LogAndThrowIfUnexpectedExitCode(String prefix, ILogger log, Int32 expectedExitCode) 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.ApiServices\WSL2\WslCommand.cs:行号 160
在 Docker.Engines.WSL2.WSL2Provisioning.<ProvisionAsync>d__8.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.Engines\WSL2\WSL2Provisioning.cs:行号 81
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 Docker.Engines.WSL2.LinuxWSL2Engine.<DoStartAsync>d__26.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.Engines\WSL2\LinuxWSL2Engine.cs:行号 170
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 Docker.ApiServices.StateMachines.TaskExtensions.<WrapAsyncInCancellationException>d__0.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.ApiServices\StateMachines\TaskExtensions.cs:行号 29
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 Docker.ApiServices.StateMachines.StartTransition.<DoRunAsync>d__5.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.ApiServices\StateMachines\StartTransition.cs:行号 67
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 Docker.ApiServices.StateMachines.StartTransition.<DoRunAsync>d__5.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.ApiServices\StateMachines\StartTransition.cs:行号 92
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 Docker.ApiServices.StateMachines.EngineStateMachine.<StartAsync>d__14.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.ApiServices\StateMachines\EngineStateMachine.cs:行号 69
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 Docker.Engines.Engines.<StartAsync>d__22.MoveNext() 位置 C:\workspaces\PR-19568\src\github.com\docker\pinata\win\src\Docker.Engines\Engines.cs:行号 106

在 PowerShell(管理员模式)或者 cmd(管理员模式)中执行

1
netsh winsock reset

执行该命令后记得重启!

重启后,Windows 11 下的 dockerdesktop 可以正常启动。

对于没有 vmware 需求,没有虚拟机 vpn 网络需求,仅仅开发 springboot3.0,上面的方式已经可以实现目标。

不过若有 VMware 需求,以及有 VMware 虚拟机共享宿主机 vpn 网络的需求,上面的重置命令慎用,会导致 VMware 里运行的虚拟机使用宿主机 vpn 网络出现问题,网络不通。

并且由于 windows 下的 dockerdesktop 需要打开 hyper-v 或者 WSL 2,一旦打开会影响 VMware 嵌套虚拟化功能,导致 VMware 下虚拟机的嵌套虚拟化功能不可用。

不过也不用担心,只需要 3 步可以恢复:

  1. 卸载 windows dockerdesktop
  2. 设置 - 应用 - 可选功能 - 更多 windows 功能,取消 WSL 2 和 hyper-v 的勾,重启电脑
  3. 卸载 wmware,重新安装 wmware。

这种情况还是在沙盒环境里编译打包原生可执行文件,比如下面的用 Linux 环境。

# Spring Boot 3.0 初步使用(Linux)

上面 windows 跑通的项目源码直接放到 Linux 下,执行,报错:

1
2
[ERROR] Internal error: java.lang.RuntimeException: GraalVM native-image is missing from your system.
[ERROR] Make sure that GRAALVM_HOME environment variable is present.

按报错信息,配置 GRAALVM_HOME 和 安装 GraalVM native-image。

1
2
3
4
5
6
export GRAALVM_HOME 到/etc/bashrc
[hanwei@backendcloud-centos9 ~]$ gu install native-image
Downloading: Component catalog from www.graalvm.org
Processing Component: Native Image
Downloading: Component native-image: Native Image from github.com
Installing new component: Native Image (org.graalvm.native-image, version 22.3.0)

再执行报错:

1
Test configuration file wasn't found.

toggle ‘Skip Tests’ mode,就是在 Linux Idea 的 Maven 窗口,点击跳过测试按钮。

再执行 ok。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/home/hanwei/sdk/graalvm-ce-java17-22.3.0/bin/java -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true -javaagent:/home/hanwei/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/222.4459.24/lib/idea_rt.jar=41343:/home/hanwei/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/222.4459.24/bin -Dfile.encoding=UTF-8 -classpath /home/hanwei/demo/target/classes:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter/3.0.0/spring-boot-starter-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot/3.0.0/spring-boot-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/spring-context/6.0.2/spring-context-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.0.0/spring-boot-autoconfigure-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-logging/3.0.0/spring-boot-starter-logging-3.0.0.jar:/home/hanwei/.m2/repository/ch/qos/logback/logback-classic/1.4.5/logback-classic-1.4.5.jar:/home/hanwei/.m2/repository/ch/qos/logback/logback-core/1.4.5/logback-core-1.4.5.jar:/home/hanwei/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.19.0/log4j-to-slf4j-2.19.0.jar:/home/hanwei/.m2/repository/org/apache/logging/log4j/log4j-api/2.19.0/log4j-api-2.19.0.jar:/home/hanwei/.m2/repository/org/slf4j/jul-to-slf4j/2.0.4/jul-to-slf4j-2.0.4.jar:/home/hanwei/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/home/hanwei/.m2/repository/org/springframework/spring-core/6.0.2/spring-core-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-jcl/6.0.2/spring-jcl-6.0.2.jar:/home/hanwei/.m2/repository/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar:/home/hanwei/.m2/repository/org/slf4j/slf4j-api/2.0.4/slf4j-api-2.0.4.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.0.0/spring-boot-starter-web-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-json/3.0.0/spring-boot-starter-json-3.0.0.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.14.1/jackson-databind-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.14.1/jackson-annotations-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.14.1/jackson-core-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.14.1/jackson-datatype-jdk8-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.14.1/jackson-datatype-jsr310-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.14.1/jackson-module-parameter-names-2.14.1.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/3.0.0/spring-boot-starter-tomcat-3.0.0.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.1/tomcat-embed-el-10.1.1.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.1/tomcat-embed-websocket-10.1.1.jar:/home/hanwei/.m2/repository/org/springframework/spring-web/6.0.2/spring-web-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-beans/6.0.2/spring-beans-6.0.2.jar:/home/hanwei/.m2/repository/io/micrometer/micrometer-observation/1.10.2/micrometer-observation-1.10.2.jar:/home/hanwei/.m2/repository/io/micrometer/micrometer-commons/1.10.2/micrometer-commons-1.10.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-webmvc/6.0.2/spring-webmvc-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-aop/6.0.2/spring-aop-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-expression/6.0.2/spring-expression-6.0.2.jar com.example.demo.DemoApplication

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0)

2022-11-29T00:02:01.348+08:00 INFO 11794 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 11794 (/home/hanwei/demo/target/classes started by hanwei in /home/hanwei/demo)
2022-11-29T00:02:01.352+08:00 INFO 11794 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-11-29T00:02:02.011+08:00 INFO 11794 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-11-29T00:02:02.018+08:00 INFO 11794 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-11-29T00:02:02.018+08:00 INFO 11794 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-11-29T00:02:02.089+08:00 INFO 11794 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-11-29T00:02:02.090+08:00 INFO 11794 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 687 ms
2022-11-29T00:02:02.347+08:00 INFO 11794 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-29T00:02:02.350+08:00 INFO 11794 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.335 seconds (process running for 16.893)

可见执行传统 jar 包的启动速度是 1.335 seconds ,下面编译原生二进制文件。点击 Linux Idea 的 Maven 窗口的 build image 按钮。会在项目的 target 目录下(和生成的 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
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
/home/hanwei/sdk/graalvm-ce-java17-22.3.0/bin/java -Dmaven.multiModuleProjectDirectory=/home/hanwei/demo -Dmaven.home=/home/hanwei/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6 -Dclassworlds.conf=/home/hanwei/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6/bin/m2.conf -Dmaven.ext.class.path=/home/hanwei/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/222.4459.24/plugins/maven/lib/maven-event-listener.jar -javaagent:/home/hanwei/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/222.4459.24/lib/idea_rt.jar=44943:/home/hanwei/.local/share/JetBrains/Toolbox/apps/IDEA-U/ch-0/222.4459.24/bin -Dfile.encoding=UTF-8 -classpath /home/hanwei/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6/boot/plexus-classworlds.license:/home/hanwei/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6/boot/plexus-classworlds-2.6.0.jar org.codehaus.classworlds.Launcher -Didea.version=2022.2.4 -DskipTests=true org.graalvm.buildtools:native-maven-plugin:0.9.16:build -P native
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- native-maven-plugin:0.9.16:build (default-cli) @ demo ---
[WARNING] 'native:build' goal is deprecated. Use 'native:compile-no-fork' instead.
[INFO] Found GraalVM installation from GRAALVM_HOME variable.
[INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.4.5]: Configuration directory not found. Trying latest version.
[INFO] [graalvm reachability metadata repository for ch.qos.logback:logback-classic:1.4.5]: Configuration directory is ch.qos.logback/logback-classic/1.4.1
[INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.1]: Configuration directory not found. Trying latest version.
[INFO] [graalvm reachability metadata repository for org.apache.tomcat.embed:tomcat-embed-core:10.1.1]: Configuration directory is org.apache.tomcat.embed/tomcat-embed-core/10.0.20
[INFO] Executing: /home/hanwei/sdk/graalvm-ce-java17-22.3.0/bin/native-image -cp /home/hanwei/demo/target/classes:/home/hanwei/.m2/repository/org/slf4j/jul-to-slf4j/2.0.4/jul-to-slf4j-2.0.4.jar:/home/hanwei/.m2/repository/org/springframework/spring-web/6.0.2/spring-web-6.0.2.jar:/home/hanwei/.m2/repository/ch/qos/logback/logback-core/1.4.5/logback-core-1.4.5.jar:/home/hanwei/.m2/repository/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.14.1/jackson-annotations-2.14.1.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-json/3.0.0/spring-boot-starter-json-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/spring-core/6.0.2/spring-core-6.0.2.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.14.1/jackson-module-parameter-names-2.14.1.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.1/tomcat-embed-websocket-10.1.1.jar:/home/hanwei/.m2/repository/ch/qos/logback/logback-classic/1.4.5/logback-classic-1.4.5.jar:/home/hanwei/.m2/repository/org/springframework/spring-jcl/6.0.2/spring-jcl-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.0.0/spring-boot-starter-web-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/3.0.0/spring-boot-autoconfigure-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/spring-beans/6.0.2/spring-beans-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/3.0.0/spring-boot-starter-tomcat-3.0.0.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.14.1/jackson-datatype-jsr310-2.14.1.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/10.1.1/tomcat-embed-el-10.1.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.14.1/jackson-core-2.14.1.jar:/home/hanwei/.m2/repository/org/apache/logging/log4j/log4j-api/2.19.0/log4j-api-2.19.0.jar:/home/hanwei/.m2/repository/org/springframework/spring-expression/6.0.2/spring-expression-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/spring-webmvc/6.0.2/spring-webmvc-6.0.2.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot/3.0.0/spring-boot-3.0.0.jar:/home/hanwei/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/10.1.1/tomcat-embed-core-10.1.1.jar:/home/hanwei/.m2/repository/org/springframework/spring-aop/6.0.2/spring-aop-6.0.2.jar:/home/hanwei/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.19.0/log4j-to-slf4j-2.19.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter/3.0.0/spring-boot-starter-3.0.0.jar:/home/hanwei/.m2/repository/org/springframework/boot/spring-boot-starter-logging/3.0.0/spring-boot-starter-logging-3.0.0.jar:/home/hanwei/.m2/repository/org/slf4j/slf4j-api/2.0.4/slf4j-api-2.0.4.jar:/home/hanwei/.m2/repository/org/springframework/spring-context/6.0.2/spring-context-6.0.2.jar:/home/hanwei/.m2/repository/io/micrometer/micrometer-observation/1.10.2/micrometer-observation-1.10.2.jar:/home/hanwei/.m2/repository/org/yaml/snakeyaml/1.33/snakeyaml-1.33.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.14.1/jackson-databind-2.14.1.jar:/home/hanwei/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.14.1/jackson-datatype-jdk8-2.14.1.jar:/home/hanwei/.m2/repository/io/micrometer/micrometer-commons/1.10.2/micrometer-commons-1.10.2.jar --no-fallback -H:Path=/home/hanwei/demo/target -H:Name=demo -H:ConfigurationFileDirectories=/home/hanwei/demo/target/graalvm-reachability-metadata/39f9c4cd5765941e97b499c39e24353f8a36ebd3/org.apache.tomcat.embed/tomcat-embed-core/10.0.20,/home/hanwei/demo/target/graalvm-reachability-metadata/39f9c4cd5765941e97b499c39e24353f8a36ebd3/ch.qos.logback/logback-classic/1.4.1
========================================================================================================================
GraalVM Native Image: Generating 'demo' (executable)...
========================================================================================================================
[1/7] Initializing... (6.4s @ 0.18GB)
Version info: 'GraalVM 22.3.0 Java 17 CE'
Java version info: '17.0.5+8-jvmci-22.3-b08'
C compiler: gcc (redhat, x86_64, 11.3.1)
Garbage collector: Serial GC
1 user-specific feature(s)
- org.springframework.aot.nativex.feature.PreComputeFieldFeature
The bundle named: org.apache.el.Messages, has not been found. If the bundle is part of a module, verify the bundle name is a fully qualified class name. Otherwise verify the bundle path is accessible in the classpath.
Field org.springframework.core.NativeDetector#imageCode set to true at build time
Field org.apache.commons.logging.LogAdapter#log4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#log4jSlf4jProviderPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jSpiPresent set to true at build time
Field org.apache.commons.logging.LogAdapter#slf4jApiPresent set to true at build time
Field org.springframework.format.support.DefaultFormattingConversionService#jsr354Present set to false at build time
Field org.springframework.core.KotlinDetector#kotlinPresent set to false at build time
Field org.springframework.core.KotlinDetector#kotlinReflectPresent set to false at build time
Field org.springframework.cglib.core.AbstractClassGenerator#imageCode set to true at build time
Field org.springframework.boot.logging.java.JavaLoggingSystem$Factory#PRESENT set to true at build time
Field org.springframework.boot.logging.log4j2.Log4J2LoggingSystem$Factory#PRESENT set to false at build time
Field org.springframework.boot.logging.logback.LogbackLoggingSystem$Factory#PRESENT set to true at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#romePresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jaxb2Present set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2Present set to true at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2XmlPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2SmilePresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jackson2CborPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#gsonPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#jsonbPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationCborPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationJsonPresent set to false at build time
Field org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#kotlinSerializationProtobufPresent set to false at build time
Field org.springframework.web.servlet.view.InternalResourceViewResolver#jstlPresent set to false at build time
Field org.springframework.web.context.support.StandardServletEnvironment#jndiPresent set to true at build time
Field org.springframework.web.context.support.WebApplicationContextUtils#jsfPresent set to false at build time
Field org.springframework.web.context.request.RequestContextHolder#jsfPresent set to false at build time
Field org.springframework.context.event.ApplicationListenerMethodAdapter#reactiveStreamsPresent set to false at build time
Field org.springframework.boot.logging.logback.LogbackLoggingSystemProperties#JBOSS_LOGGING_PRESENT set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jaxb2Present set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2Present set to true at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2XmlPresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jackson2SmilePresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#gsonPresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#jsonbPresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationCborPresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationJsonPresent set to false at build time
Field org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter#kotlinSerializationProtobufPresent set to false at build time
Field org.springframework.boot.autoconfigure.web.format.WebConversionService#JSR_354_PRESENT set to false at build time
Field org.springframework.web.client.RestTemplate#romePresent set to false at build time
Field org.springframework.web.client.RestTemplate#jaxb2Present set to false at build time
Field org.springframework.web.client.RestTemplate#jackson2Present set to true at build time
Field org.springframework.web.client.RestTemplate#jackson2XmlPresent set to false at build time
Field org.springframework.web.client.RestTemplate#jackson2SmilePresent set to false at build time
Field org.springframework.web.client.RestTemplate#jackson2CborPresent set to false at build time
Field org.springframework.web.client.RestTemplate#gsonPresent set to false at build time
Field org.springframework.web.client.RestTemplate#jsonbPresent set to false at build time
Field org.springframework.web.client.RestTemplate#kotlinSerializationCborPresent set to false at build time
Field org.springframework.web.client.RestTemplate#kotlinSerializationJsonPresent set to false at build time
Field org.springframework.web.client.RestTemplate#kotlinSerializationProtobufPresent set to false at build time
Field org.springframework.core.ReactiveAdapterRegistry#reactorPresent set to false at build time
Field org.springframework.core.ReactiveAdapterRegistry#rxjava3Present set to false at build time
Field org.springframework.core.ReactiveAdapterRegistry#kotlinCoroutinesPresent set to false at build time
Field org.springframework.core.ReactiveAdapterRegistry#mutinyPresent set to false at build time
SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See https://www.slf4j.org/codes.html#noProviders for further details.
Field org.springframework.web.servlet.mvc.method.annotation.ReactiveTypeHandler#isContextPropagationPresent set to false at build time
Field org.springframework.web.servlet.support.RequestContext#jstlPresent set to false at build time
[2/7] Performing analysis... [*********] (54.5s @ 1.28GB)
15,278 (92.35%) of 16,544 classes reachable
24,932 (67.63%) of 36,867 fields reachable
73,534 (62.25%) of 118,132 methods reachable
780 classes, 163 fields, and 3,506 methods registered for reflection
64 classes, 70 fields, and 55 methods registered for JNI access
4 native libraries: dl, pthread, rt, z
[3/7] Building universe... (5.2s @ 4.12GB)
[4/7] Parsing methods... [**] (4.2s @ 4.35GB)
[5/7] Inlining methods... [***] (1.9s @ 2.82GB)
[6/7] Compiling methods... [*****] (27.8s @ 3.46GB)
[7/7] Creating image... (6.1s @ 1.15GB)
32.83MB (49.82%) for code area: 48,151 compilation units
32.75MB (49.70%) for image heap: 354,531 objects and 320 resources
324.99KB ( 0.48%) for other data
65.90MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area: Top 10 object types in image heap:
1.63MB sun.security.ssl 7.21MB byte[] for code metadata
1.04MB java.util 3.88MB byte[] for embedded resources
832.55KB java.lang.invoke 3.63MB java.lang.Class
718.00KB com.sun.crypto.provider 3.39MB java.lang.String
541.00KB org.apache.catalina.core 2.80MB byte[] for java.lang.String
499.59KB org.apache.tomcat.util.net 2.79MB byte[] for general heap data
490.49KB org.apache.coyote.http2 1.28MB com.oracle.svm.core.hub.DynamicHubCompanion
472.53KB java.lang 815.22KB byte[] for reflection metadata
461.63KB sun.security.x509 659.44KB java.lang.String[]
459.52KB java.util.concurrent 648.80KB java.util.HashMap$Node
25.43MB for 637 more packages 5.47MB for 3070 more object types
------------------------------------------------------------------------------------------------------------------------
9.1s (7.9% of total time) in 37 GCs | Peak RSS: 6.43GB | CPU load: 4.46
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
/home/hanwei/demo/target/demo (executable)
/home/hanwei/demo/target/demo.build_artifacts.txt (txt)
========================================================================================================================
Finished generating 'demo' in 1m 53s.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:55 min
[INFO] Finished at: 2022-11-28T23:55:32+08:00
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0

执行二进制可执行文件,启动速度只有 0.052 seconds 。

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
[hanwei@backendcloud-centos9 ~]$ cd demo/target/
[hanwei@backendcloud-centos9 target]$ ls
classes demo demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar.original demo.build_artifacts.txt generated-sources generated-test-sources graalvm-reachability-metadata maven-archiver maven-status spring-aot surefire-reports test-classes
[hanwei@backendcloud-centos9 target]$ ./demo

. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.0.0)

2022-11-29T00:03:59.026+08:00 INFO 12061 --- [ main] com.example.demo.DemoApplication : Starting AOT-processed DemoApplication using Java 17.0.5 with PID 12061 (/home/hanwei/demo/target/demo started by hanwei in /home/hanwei/demo/target)
2022-11-29T00:03:59.026+08:00 INFO 12061 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default"
2022-11-29T00:03:59.040+08:00 INFO 12061 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2022-11-29T00:03:59.040+08:00 INFO 12061 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2022-11-29T00:03:59.040+08:00 INFO 12061 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-11-29T00:03:59.044+08:00 INFO 12061 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2022-11-29T00:03:59.044+08:00 INFO 12061 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 18 ms
2022-11-29T00:03:59.068+08:00 INFO 12061 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-29T00:03:59.068+08:00 INFO 12061 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 0.052 seconds (process running for 0.056)
[hanwei@backendcloud-centos9 target]$ ll -h
total 84M
drwxr-xr-x. 5 hanwei hanwei 74 Nov 28 23:15 classes
-rwxr-xr-x. 1 hanwei hanwei 66M Nov 28 23:55 demo
-rw-r--r--. 1 hanwei hanwei 18M Nov 28 23:49 demo-0.0.1-SNAPSHOT.jar
-rw-r--r--. 1 hanwei hanwei 119K Nov 28 23:49 demo-0.0.1-SNAPSHOT.jar.original
-rw-r--r--. 1 hanwei hanwei 19 Nov 28 23:55 demo.build_artifacts.txt
drwxr-xr-x. 3 hanwei hanwei 25 Nov 28 22:59 generated-sources
drwxr-xr-x. 3 hanwei hanwei 30 Nov 28 22:59 generated-test-sources
drwxr-xr-x. 4 hanwei hanwei 101 Nov 28 23:42 graalvm-reachability-metadata
drwxr-xr-x. 2 hanwei hanwei 28 Nov 28 22:59 maven-archiver
drwxr-xr-x. 3 hanwei hanwei 35 Nov 28 22:59 maven-status
drwxr-xr-x. 3 hanwei hanwei 18 Nov 28 22:59 spring-aot
drwxr-xr-x. 2 hanwei hanwei 153 Nov 28 23:03 surefire-reports
drwxr-xr-x. 3 hanwei hanwei 17 Nov 28 22:59 test-classes

对比两种打包方式:jar 包和原生可执行文件,jar 包 18 兆,原生可执行文件因为可以不依赖 java 运行环境而直接运行,所以体积大些,60 兆。

上面是通过 Linux Idea 的 Maven 窗口执行的。也可以通过命令行执行 mvn 命令生成原生二进制文件。

1
2
3
4
5
6
7
8
9
10
11
12
[hanwei@backendcloud-centos9 target]$ which mvn
~/.local/bin/mvn
[hanwei@backendcloud-centos9 target]$ ll ~/.local/bin/mvn
lrwxrwxrwx. 1 hanwei hanwei 107 Nov 28 22:57 /home/hanwei/.local/bin/mvn -> /home/hanwei/.m2/wrapper/dists/apache-maven-3.8.6-bin/1ks0nkde5v1pk9vtc31i9d0lcd/apache-maven-3.8.6/bin/mvn
# 到feature-native目录下
[hanwei@backendcloud-centos9 demo]$ cd ..
[hanwei@backendcloud-centos9 demo]$ mvn clean
# 打包
[hanwei@backendcloud-centos9 demo]$ mvn package -Pnative
# 可执行文件
[hanwei@backendcloud-centos9 demo]$ mvn native:compile-no-fork
# 到target目录下启动可执行文件

从上面的执行效果对比看出,云原生时代的 Spring Boot 3.0: GraalVM 编译的二进制可执行文件,启动速度相对于传统 jar 包提升近 30 倍(1.335 seconds -> 0.052 seconds)。

# 欢迎订阅微信公众号 “InterviewCoder” !

# 关于我

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

InterviewCoder

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

【Java】Graalvm安装配置与springboot3.0尝鲜

InterviewCoder

# 【Java】Graalvm 安装配置与 springboot3.0 尝鲜

# Graalvm 安装配置与 springboot3.0 尝鲜

在这里插入图片描述

Spring 团队一直致力于 Spring 应用程序的原生映像支持已有一段时间了。经过 3 + 年的孵化春季原生 Spring Boot 2 的实验性项目,原生支持将在 Spring Framework 6 和 Spring Boot 3 中正式发布!

# 安装 Graalvm

由于 spring-boot3.0 仅支持 22.3 版本,此处我们选择这个版本作为演示

Release GraalVM Community Edition 22.3.0 · graalvm/graalvm-ce-builds (github.com)

image-20221130150340996

选择对应版本下载即可,此处我选择的是 windows 的 java 17 版本

解压之后我们可以得到这样的目录结构

1
前置路径\graalvm-ce-java17-22.3.0

image-20221130150524482

# 配置 Graalvm 环境变量

配置变量到 GRAALVM_HOME``你的解压路径

image-20221130150648155

在中添加 Path``%GRAALVM_HOME%\bin

image-20221130150848751

确定保存

# 安装成功

打开控制面板输入命令 Java -version

image-20221130151006878

可以看到显示了我们刚刚安装的 GraalVM 的信息

输入命令 gu

image-20221130151120759

可以看见命令也能正常使用 gu

# 安装 native-image

控制台输入命令即可 gu install native-image

但是可能会出现以下错误

image-20221130151315638

解决方案

在 github 上下载对应版本的 native-image 安装包进行本地安装

还是熟悉的地址:

Release GraalVM Community Edition 22.3.0 · graalvm/graalvm-ce-builds (github.com)

下拉,找到与你版本对应的安装包 native-image

image-20221130151500003

控制台输入命令如图所示 gu install -L 你的下载位置

image-20221130151626018

# 安装成功

控制台输入如图所示 gu list

image-20221130151715833

可以看见 native-image 的版本信息,说明安装成功

# 配置 msvc 环境

安装 vs2019 及以上版本

image-20221130152533185

安装时确保勾选了以上两个选项

# 环境变量

在 Path 中配置 D:\vs2022\VC\Tools\MSVC\14.29.30133\bin\HostX64\x64D:\vs2022\VC\Tools\MSVC\14.29.30133\bin\HostX64\x64

image-20221130160020916

配置环境变量 INCLUDE

image-20221130160604371

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\ucrt;

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um;

C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\shared;

D:\vs2022\VC\Tools\MSVC\14.29.30133\include;

(vs2022 是我的安装路径)

配置环境变量 LIB

image-20221130161557414

D:\vs2022\VC\Tools\MSVC\14.29.30133\lib\x64;

C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\um\x64;

C:\Program Files (x86)\Windows Kits\10\Lib\10.0.18362.0\ucrt\x64;

# 运行 spring-boot3.0 测试项目

# 构建项目

引入 GraalVM Native 依赖,此次我们引入 Web 依赖用于测试

image-20221130190451952

pom 文件如下

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fate</groupId>
<artifactId>boot3</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot3</name>
<description>boot3</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<configuration>
<!-- 此处是入口类,必须与实际代码一致,否则无法打包成功-->
<mainClass>com.fate.boot3.Boot3Application</mainClass>
<!-- 生成的exe文件名-->
<imageName>boot-test</imageName>
<buildArgs>
<buildArg>--verbose</buildArg>
</buildArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

# 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.fate.boot3.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* @author fate
* @date 2022/11/30
* @Description
*/
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping
public String test(){
return "test boot3.0";
}
}

# 测试运行

image-20221130190851328

# 打包编译

点击 maven 侧边栏插件,如图所示

image-20221130191339227

或者直接运行 maven 命令 mvn -Pnative package

开始漫长的编译打包,即使是这样简单的项目也需要好几分钟,并且期间你的电脑也会很卡,

image-20221130191604020

image-20221130191900654

可以看见,耗时近三分钟,编译之后我们发现,target 目录下变得不一样了,生成了 exe 可执行文件,当然你也可能注意到了 spring-aot 这个文件夹,虽然我还不是很了解 aot, 但是很明显,大的来了

image-20221130193829658

直接在 cmd 中运行

image-20221130193741304

image-20221130193757182

# 大功告成

# 关于我

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

InterviewCoder

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

Hbase整合SpringBoot的问题

InterviewCoder

在一个 Demo 中,springboot 整合 Hbase 出了一个难解决的问题:

tried to access method com.google.common.base.Stopwatch.()V from class org.apache.hadoop.hbase.zookeeper.MetaTableLocator

调用 hbase 就会报错,这是因为 swagger 和 guava 依赖冲突导致的,整合 habse 的 1.0 版本,依赖中不能有 swagger,guava 的版本要低于 17.

# 关于我

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

InterviewCoder

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

【GOSN】Gosn是什么?怎么使用?

InterviewCoder

#

Gson 是 google 开发的一个开源 Json 解析库,使用十分的方便,在 maven 当中导入的方式为:

1
2
3
4
5
<dependency>  
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.2</version>
</dependency>

其中 2.8.2 为版本号,最新版本以及源码可以在官方的 github 上查看:https://github.com/google/gson

这里给出最简单的 Gson 的使用方法:

1
2
3
4
5
6
7
8
9
Object obj = new Object();

//Object转Json字符串

String obstr = new Gson().toJson(object);

//Json字符串转Object

Object object = new Gson().fromJson(obstr);

# 关于我

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

InterviewCoder

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

如何使用Flutter+SpringBoot+Mysql开发一个简易的抽奖APP(Android教学

InterviewCoder

# 微信公众号搜索 InterviewCoder 回复关键词《吃啥》获取源码以及开发教程~

二维码mini

# 前言:

# Weat 中文译为:吃啥

# 吃啥来自于女朋友的一个问题,问我可不可以做个抽奖的 APP,奖品都是菜,抽中那个今天就做那个菜吃,我灵机一动,使用 InterviewCoder 公众号里面的 chatGPT 小程序编辑了下面的文案:
# 吃啥是一款新奇有趣的应用,旨在帮助你找到今天吃什么菜。它通过精心的菜谱抽奖模式,为你提供多样而有色彩的食物,让你的用餐经历变得更加美好。不仅如此,它还可以添加朋友和家人的偏好,为大家带来更多惊喜。加入吃啥,让你的用餐经历变得更加完美!

# 架构方面:

移动端:Flutter

后台服务:SpringBoot、Mysql

# 没有安装 dart、flutter、Android Studio、vscode 的同学可以看看以前的教程,这里就不 一 一 介绍了

本文将详细介绍 weatapp 的开发流程,前后端代码编写阶段,以及后台代码部署。

# 一、APP 创建:

  1. 打开项目路径,输入 cmd 进入到命令行,输入 flutter create weat 进行 flutter 项目创建。

    image-20230223201656540

  2. 打开 Android Studio 整理项目,修改仓库配置

    1
    2
    3
    maven { url 'https://maven.aliyun.com/repository/google' }
    maven { url 'https://maven.aliyun.com/repository/jcenter' }
    maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }

image-20230223201935306

3. 点击 Open for Editing in Android Studio 进入安卓视图,拉取 gradle 库

image-20230223202038338

4. 当 Android 主文件不暴红,说明配置完毕了,gradle 库已经拉下来了

image-20230223202433131

# 二、APP 启动:

​ 1. 打开一个安卓模拟器,有条件的同学可以直接使用真机,老师这里为了方便就直接用模拟器了

image-20230223202625528

​ 2. 使用 vscode 打开项目并启动 flutter 项目

image-20230223203129146

至此,一个 flutter 项目,创建完成,并启动了!

# 三、APP 图标配置

需要打开两个网址:

https://logo.aliyun.com/logo#/name 阿里云 LOGO 服务

https://icon.wuruihong.com/ 图标工厂

1. 打开阿里云 LOGO 输入 APP 名字,吃啥

image-20230223203651967

2. 生成图标:

image-20230223203733745

购买一个你喜欢的图标,如果不购买的话,自己在网上找一个也行,但是一定要有商业授权!

image-20230223204102921

得到图标后,进入图标工厂,生成一套 ios 和一套 Android 的图标文件:

image-20230223205448783

image-20230223205507890

进入安卓文件夹,将该目录的文件复制到你的项目下面 \android\app\src\main\res

image-20230223205604112

# 成效:

image-20230223210003933

至此,APP 图标设置完成!

# 四、APP 后台开发(SpringBoot+Mysql)

# 为什么先开发 APP 部分?,这个纯属个人习惯,我比较喜欢先有数据和接口,直接开发 APP 的感觉会更轻松!

# 整理需求:

1
2
每次进入app随机获取食物列表
点击开始抽奖 随机跳转 点击停止 按钮回显食物名称 点击查看菜谱 弹窗提示做法

根据如上需求,我明确了客户到底想要什么

1. 这是一个服务类型的 APP,服务于不知道吃什么的客户

2. 每次进入抽奖页面需要获取不同的菜谱奖品列表

3. 这个页面需要有个按钮,可以来重置奖品,并且要限制次数

4. 点击抽奖按钮,开始抽奖,再次点击,或者超过限制时间,停止选择,并将按钮置为:查看食谱

5. 点击查看食谱,进入食谱详情,展示过程

根据我的聪明思考,画出了如下 UI:

image-20230223210734630

# 开玩笑,画的很烂,但是就是这个意思!。

那么,开始开发后台数据,为我们的 APP 提供一个接口~!

经过一系列操作,得到以下数据:

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
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for iv_dishes
-- ----------------------------
DROP TABLE IF EXISTS `iv_dishes`;
CREATE TABLE `iv_dishes` (
`id` int(50) NOT NULL AUTO_INCREMENT COMMENT '唯一ID',
`dishes_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品名字',
`dishes_step` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品做法',
`dishes_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '菜品图片地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜品表' ROW_FORMAT = Compact;

-- ----------------------------
-- Records of iv_dishes
-- ----------------------------
INSERT INTO `iv_dishes` VALUES (1, '凉拌黄瓜虾仁', '1.小米辣 白芝麻蒜未辣椒面 淋上少许热油\r\n2.生抽2勺 油 醋各1代糖半勺搅匀备用\r\n3.虾煮熟去壳 木耳焯水捞出 黄瓜拍块\r\n4.淋上酱汁拌匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224081935481.png');
INSERT INTO `iv_dishes` VALUES (2, '低卡酱油鸡蛋', '1.生抽4勺 醋2香油1勺一把葱花\r\n2.鸡蛋冷水下锅煮8分钟盖盖焖1分钟后泡冷水\r\n3.温开水4勺搅匀\r\n4.鸡蛋剥壳切对半淋上酱汁拌匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082035768.png');
INSERT INTO `iv_dishes` VALUES (3, '低卡葱香鸡腿', '1.蒜末 葱花 白艺麻小米辣淋上少许热油\r\n2.生抽1勺 许盐搅匀\r\n3.鸡腿熟捞出微凉后撕成小块\r\n4.淋上酱汁拌匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082101091.png');
INSERT INTO `iv_dishes` VALUES (4, '低卡平菇炒蛋', '1.鸡蛋炒熟盛出\r\n2.油热下从蒜炒香\r\n3.下平菇炒出汁,倒入炒好的鸡蛋\r\n4.生抽 油 辣椒粉各1勺 少许盐炒匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082137439.png');
INSERT INTO `iv_dishes` VALUES (5, '凉拌虾仁西兰花', '1.蒜未 葱花 辣椒面白芝麻淋上少许热油\r\n2.生抽2勺 醋各1勺搅拌均匀\r\n3.西兰花焯水捞出,虾焯水去壳鸡蛋白切块\r\n4.淋上酱汁拌匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082204322.png');
INSERT INTO `iv_dishes` VALUES (6, '凉拌黄瓜木耳鸡蛋', '1.木耳鸡蛋各煮熟捞出 黄瓜拍块\r\n2.生抽2勺醋香油各11勺温开水拌匀即可开吃', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082230282.png');
INSERT INTO `iv_dishes` VALUES (7, '凉拌黄瓜豆腐', '1.白芝麻辣椒面小米辣,蒜未 葱花 淋少许热油\r\n2.生抽2勺 醋油各1勺 少许盐代糖拌匀\r\n3.黄瓜拍块去籽豆腐煮熟捞出\r\n4.撒上香葱淋上酱汁拌匀即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082404878.png');
INSERT INTO `iv_dishes` VALUES (8, '木耳炒鸡蛋', '1.鸡蛋炒熟盛出\r\n2.蒜未和胡萝人炒软倒木耳炒熟再倒鸡蛋\r\n3.耗油生抽各1勺少许盐-小半碗淀粉水\r\n4.炒匀下葱段即可', 'https://brath.oss-cn-shanghai.aliyuncs.com/pigo/image-20230224082512562.png');

SET FOREIGN_KEY_CHECKS = 1;

接下来创建一个 SpringBoot 项目,并写出 entity、 controller、service、impl、mapper、xml 等需要的代码

entity:

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
/**
* <p>
* 菜品表
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("iv_dishes")
@ApiModel(value="IvDishes对象", description="菜品表")
public class IvDishes implements Serializable {

private static final long serialVersionUID = 1L;

@ApiModelProperty(value = "唯一ID")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;

@ApiModelProperty(value = "菜品名字")
private String dishesName;

@ApiModelProperty(value = "菜品做法")
private String dishesStep;

@ApiModelProperty(value = "菜品图片地址")
private String dishesUrl;
}

controller:

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
/**
* <p>
* 菜品表 前端控制器
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@RestController
@RequestMapping("/dishes")
public class IvDishesController {

/***
* SLF4J日志
*/
private Logger logger = LoggerFactory.getLogger(IvDishesController.class);

/**
* 菜品服务接口
*/
@Autowired
private IvDishesService dishesService;

/***
* 获取菜品列表
*
* @param page
* @param size
* @return
*/
@GetMapping("/getDishes")
public Object getDishes(@RequestParam(value = "page", defaultValue = "1") Integer page, @RequestParam(value = "size", defaultValue = "8") Integer size) {
logger.info("【用户服务】获取菜品列表,开始");
Map<Object, Object> result = new HashMap<>();
IPage<IvDishes> prizeRecords = dishesService.getDishes(page, size);
if (CollectionUtils.isEmpty(prizeRecords.getRecords())) {
result.put("fail", ResponseCode.DATA_DOES_NOT_EXIST);
logger.error("【用户服务】获取菜品列表,服务错误:{}", ResponseCode.DATA_DOES_NOT_EXIST);
}
result.put("prizeRecords", prizeRecords.getRecords());
logger.info("【用户服务】获取菜品列表,完毕");
return ResponseUtil.ok(result);
}
}

service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* <p>
* 菜品表 服务类
* </p>
*
* @author Brath
* @since 2023-02-23
*/
public interface IvDishesService extends IService<IvDishes> {

/**
* 获取菜品列表
*
* @param page
* @param size
* @return
*/
IPage<IvDishes> getDishes(Integer page, Integer size);

}

impl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* <p>
* 菜品表 服务实现类
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@Service
public class IvDishesServiceImpl extends ServiceImpl<IvDishesMapper, IvDishes> implements IvDishesService {

/**
* 获取菜品列表
*
* @param page
* @param size
* @return
*/
@Override
public IPage<IvDishes> getDishes(Integer page, Integer size) {
return baseMapper.getDishes(new Page<>(page, size), new QueryWrapper<>());
}
}

mapper:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* <p>
* 菜品表 Mapper 接口
* </p>
*
* @author Brath
* @since 2023-02-23
*/
@Mapper
public interface IvDishesMapper extends BaseMapper<IvDishes> {

/**
* 获取菜品列表
*
* @param objectPage
* @param objectQueryWrapper
* @return
*/
IPage<IvDishes> getDishes(Page<Object> objectPage, QueryWrapper<Object> objectQueryWrapper);

}

xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.brath.mapper.IvDishesMapper">

<!-- 通用查询映射结果 -->
<resultMap id="DishesMap" type="cn.brath.entity.IvDishes">
<id column="id" property="id"/>
<result column="dishes_name" property="dishesName"/>
<result column="dishes_step" property="dishesStep"/>
<result column="dishes_url" property="dishesUrl"/>
</resultMap>
<select id="getDishes" resultType="cn.brath.entity.IvDishes">
select d.*
from iv_dishes d
ORDER BY RAND()
</select>

</mapper>

application.yaml 配置:

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
server:
port: 9999
spring:
datasource:
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/iv-user-services?createDatabaseIfNotExist=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true
username: 'root'
password: 'root'
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 6000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
#Oracle需要打开注释
# validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
#达梦数据库,需要注释掉,其他数据库可以打开
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true

经过如上一系列配置,我们项目启动成功并可以访问到接口:

image-20230224124319493

接下来只要把程序部署上线就搞定了!

部署后台程序:

将我们的 Dockerfile、运行脚本、jar 包传入服务器

image-20230224124656682

Dockerfile:

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
#java8环境
FROM openkbs/jdk11-mvn-py3

#root用户
USER root

#设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

#Auth
MAINTAINER Brath

#设置工作目录集
WORKDIR /root/weatWork

#复制jars和命令
ADD *.jar /root/weatWork/
ADD run.sh /root/weatWork/run.sh

#脚本权限设置
RUN chmod +x /root/weatWork/run.sh

#暴露端口
EXPOSE 9999

# 1.Dockerfile 打包镜像

1
2
1.进入工作目录
2.docker build -t weat .

image-20230224124821870

# 2. 运行容器

1
docker run -dit -p 9999:9999 --privileged=true -P  --name  weat weat /bin/bash -c "tail -f /dev/null" -g "daemon off;"

image-20230224124959281

# 3. 启动 Jar 包

1
2
3
4
5
6
#进入容器
docker exec -it weat bash
#运行脚本
sh run.sh
#查看日志
tail -100f weatlog.log

image-20230224125037992

# 4. 联调接口

image-20230224125210437

# 五、APP 移动端开发(Flutter)

# weatApp 架构设计大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
lib:

​ common 通用层

​ core 核心层

​ routers 路由曾

​ utils 工具层

​ viewmodel 视图模型层

​ views 视图层

需要安装的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#网络请求
dio: ^4.0.6
#getx
get: ^4.6.5
#透明弹出框
fluttertoast: ^8.0.8
#屏幕适配
flutter_screenutil: ^5.5.3+2
#全局状态管理
provider: ^6.0.1
#轮播图
flutter_swiper_plus: ^2.0.4
# GET WIDGET UI库
getwidget: ^2.0.5
#图片缓存
cached_network_image: ^3.2.0
#图片放大缩小
photo_view: ^0.13.0
#权限申请
permission_handler: ^9.2.0
#贝壳组件库
bruno: ^2.2.0
#easyloading
flutter_easyloading: ^3.0.3

# 代码如下:

# common.dart

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
import 'dart:io';
import 'dart:math';

import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:getwidget/getwidget.dart';
import 'package:provider/provider.dart';
import 'package:weat/main.dart';

/*
* Common组件库
* @Auth: Brath
*/

/*
* 获取title的AppBar
*/
AppBar getAppBar(String title, {required context}) {
return AppBar(
toolbarHeight: 40.0.h,
centerTitle: true,
shadowColor: Color.fromARGB(255, 59, 82, 76),
backgroundColor: Colors.transparent,
leading: null,
leadingWidth: 30.w,
elevation: 0.0,
actions: [],
title: (Text(
title,
style: TextStyle(
fontSize: 16.sp,
color: Colors.black,
fontFamily: '',
fontWeight: FontWeight.bold),
)));
}

/*
* 获取卡片圆形背景容器
*/
class getCardContainer extends StatelessWidget {
getCardContainer({
Key? key,
this.isBorder,
this.BorderCircular,
this.width,
this.height,
this.widget,
this.margin,
}) : super(key: key);

bool? isBorder;
double? BorderCircular;
double? height;
double? width;
Widget? widget;
EdgeInsetsGeometry? margin;

@override
Widget build(BuildContext context) {
return Container(
margin: margin,
decoration: isBorder == true
? BoxDecoration(
color: Colors.white,
shape: BoxShape.rectangle,
boxShadow: [
BoxShadow(
color: Colors.grey[200]!,
offset: Offset(1.w, 1.h),
blurRadius: 1.r,
spreadRadius: 1.r,
),
],
border: Border.all(color: Colors.white, width: 1.w), // border
borderRadius:
BorderRadius.circular((BorderCircular ?? 15.r)), // 圆角
)
: null,
width: width,
height: height,
child: widget,
);
}
}

# ZoomImage.dart

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
import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

class ZoomImage extends StatefulWidget {
ZoomImage({Key? key, this.url}) : super(key: key);
String? url;
@override
State<StatefulWidget> createState() {
return _ZoomImage();
}
}

class _ZoomImage extends State<ZoomImage> with SingleTickerProviderStateMixin {
AnimationController? _controller;
Animation<Offset>? _animation;
Offset _offset = Offset.zero;
double _scale = 1.0;
Offset? _normalizedOffset;
double? _previousScale;
final double _kMinFlingVelocity = 600.0;
bool _isEnlarge = false;
bool _isHideTitleBar = false;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this);
_controller?.addListener(() {
setState(() {
_offset = _animation!.value;
});
});
}

@override
void dispose() {
super.dispose();
_controller!.dispose();
}

Offset _clampOffset(Offset offset) {
final Size size = context.size!;
// widget的屏幕宽度
final Offset minOffset = Offset(size.width, size.height) * (1.0 - _scale);
// 限制他的最小尺寸
return Offset(
offset.dx.clamp(minOffset.dx, 0.0), offset.dy.clamp(minOffset.dy, 0.0));
}

_handleOnScaleStart(ScaleStartDetails details) {
setState(() {
_isHideTitleBar = true;
_previousScale = _scale;
_normalizedOffset = (details.focalPoint - _offset) / _scale;
// 计算图片放大后的位置
_controller!.stop();
});
}

_handleOnScaleUpdate(ScaleUpdateDetails details) {
setState(() {
_scale = (_previousScale! * details.scale).clamp(1.0, 3.0);
// 限制放大倍数 1~3倍
_offset = _clampOffset(details.focalPoint - _normalizedOffset! * _scale);
// 更新当前位置
});
}

_handleOnScaleEnd(ScaleEndDetails details) {
_setSystemUi();
final double magnitude = details.velocity.pixelsPerSecond.distanceSquared;
if (magnitude < _kMinFlingVelocity) return;
final Offset direction = details.velocity.pixelsPerSecond / magnitude;
// 计算当前的方向
final double distance = (Offset.zero & context.size!).shortestSide;
// 计算放大倍速,并相应的放大宽和高,比如原来是600*480的图片,放大后倍数为1.25倍时,宽和高是同时变化的
_animation = _controller!.drive(Tween<Offset>(
begin: _offset, end: _clampOffset(_offset + direction * distance)));
_controller!
..value = 0.0
..fling(velocity: magnitude / 1000.0);
}

_onDoubleTap() {
_isHideTitleBar = true;
_setSystemUi();
Size size = context.size!;
_isEnlarge = !_isEnlarge;
setState(() {
if (!_isEnlarge) {
_scale = 2.0;
_offset = Offset(-(size.width / 2), -(size.height / 2));
} else {
_scale = 1.0;
_offset = Offset.zero;
}
});
}

_onTap() {
setState(() {
_isHideTitleBar = !_isHideTitleBar;
});
_setSystemUi();
}

@override
Widget build(BuildContext context) {
return Stack(
children: [_bodyView(), _titleBar()],
);
}

_bodyView() {
return GestureDetector(
onScaleStart: _handleOnScaleStart,
onScaleUpdate: _handleOnScaleUpdate,
onScaleEnd: _handleOnScaleEnd,
onDoubleTap: _onDoubleTap,
onTap: _onTap,
child: Container(
color: _isHideTitleBar ? Colors.black : Colors.white,
child: SizedBox.expand(
child: ClipRect(
child: Transform(
transform: Matrix4.identity()
..translate(_offset.dx, _offset.dy)
..scale(_scale),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 700.0.w,
height: 600.0.h,
child: Image.network(widget.url!)),
Container()
],
),
),
),
),
),
);
}

_titleBar() {
return Offstage(
child: Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(
top: MediaQueryData.fromWindow(window).padding.top,
left: ScreenUtil().setWidth(24)),
color: const Color.fromARGB(255, 32, 32, 32),
height: MediaQuery.of(context).size.height * 0.1,
width: MediaQuery.of(context).size.width,
child: GestureDetector(
child: Icon(
Icons.arrow_back,
size: 30.0.w,
color: Colors.white,
),
onTap: () {
Navigator.pop(context);
},
),
),
offstage: _isHideTitleBar,
);
}

_setSystemUi() {
if (_isHideTitleBar) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual);
}
}
}

# Global.dart

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
import 'package:dio/dio.dart';

class Global {
static String BaseUrl = 'http://127.0.0.1:9999/';

/*请求dio对象 */
late Dio dio;
/*通用超时 */
int timeOut = 50000;
/*请求单例 */
static Global? _instance;

/*获取实例 */
static Global? getInstance() {
if (_instance == null) _instance = Global();
return _instance;
}

Global() {
dio = Dio();
dio.options = BaseOptions(
baseUrl: BaseUrl,
connectTimeout: timeOut,
sendTimeout: timeOut,
receiveTimeout: timeOut,
contentType: Headers.jsonContentType,
headers: {
"Access-Control-Allow-Origin": "*",
});
// 请求拦截器 and 响应拦截机 and 错误处理
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
print("\n================== 请求数据 ==========================");
print("url = ${options.uri.toString()}");
print("headers = ${options.headers}");
print("params = ${options.data}");
print("\n================== 请求数据 ==========================");
return handler.next(options);
}, onResponse: (response, handler) {
print("\n================== 响应数据 ==========================");
print("code = ${response.statusCode}");
print("data = ${response.data}");
print("\n================== 响应数据 ==========================");
handler.next(response);
}, onError: (DioError e, handler) {
print("\n================== 错误响应数据 ======================");
print("type = ${e.type}");
print("message = ${e.message}");
print("\n================== 错误响应数据 ======================");
return handler.next(e);
}));
}
}

# routes.dart

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
// ignore_for_file: prefer_const_constructors

import 'package:flutter/cupertino.dart';
import 'package:weat/views/index/IndexView.dart';

Map<String, WidgetBuilder> routes = {
"/": (context) => IndexView(),
};

/*
* 渐隐跳转路由
*/
void opcityPush(BuildContext context, Widget view, {int? milliseconds}) {
Navigator.push(
context,
PageRouteBuilder(
transitionDuration:
Duration(milliseconds: milliseconds ?? 300), //动画时间为300毫秒
pageBuilder: (BuildContext context, Animation<double> animation,
Animation secondaryAnimation) {
return FadeTransition(
//使用渐隐渐入过渡,
opacity: animation,
child: view, //路由B
);
},
),
);
}

/*
* 带路由树跳转
*/
void Push(BuildContext context, Widget view) {
Navigator.push(
context,
CupertinoPageRoute(
builder: (context) => view,
));
}

void NavigatorPop(BuildContext context) {
Navigator.of(context).pop();
}

/*
* 返回上一级路由树,没有上级会黑屏
*/
void Pop(BuildContext context, Widget view) {
Navigator.pop(
context,
CupertinoPageRoute(
builder: (context) => view,
));
}

# showmessage_util.dart

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
// ignore_for_file: prefer_equal_for_default_values, sized_box_for_whitespace

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:fluttertoast/fluttertoast.dart';

/// @author brath
/// @创建时间:2022/5/10
/// 封装自定义弹框
class DialogUtils {
/// 显示普通消息
static showMessage(String msg,
{toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.CENTER,
timeInSecForIosWeb: 2,
textColor: Colors.black,
backgroundColor: Colors.grey,
fontSize: 16.0}) {
// 先关闭弹框再显示对应弹框
Fluttertoast.cancel();
Fluttertoast.showToast(
msg: msg,
toastLength: toastLength,
webShowClose: true,
gravity: gravity,
textColor: textColor,
timeInSecForIosWeb: timeInSecForIosWeb,
backgroundColor: Colors.grey[50],
fontSize: 13.0.sp,
);
}

# index_viewmodel.dart

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
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';

class IndexViewModel extends ChangeNotifier {
bool? isLotte = false;
Map? currentDishes = {};

bool? get getIsLotte {
return isLotte;
}

void setIsLotte(bool isLotte) {
this.isLotte = isLotte;
notifyListeners();
}

Map? get getCurrentDishes {
return currentDishes;
}

void setCurrentDishes(Map currentDishes) {
this.currentDishes = currentDishes;
notifyListeners();
}
}

# IndexView.dart

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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:getwidget/getwidget.dart';
import 'package:provider/provider.dart';
import 'package:weat/common/ZoomImage.dart';
import 'package:weat/core/Global.dart';
import 'package:weat/routers/routes.dart';
import 'package:weat/utils/showmessage_util.dart';
import 'package:weat/viewmodel/index_viewmodel.dart';
import 'package:weat/views/index/DishesDetail.dart';

/*
* 首页视图
*/
class IndexView extends StatefulWidget {
const IndexView({Key? key}) : super(key: key);

@override
State<IndexView> createState() => _IndexViewState();
}

class _IndexViewState extends State<IndexView> {
@override
Widget build(BuildContext context) {
return const LotterView();
}
}

/*
* 轮播抽奖页面
*/
class LotterView extends StatefulWidget {
const LotterView({Key? key}) : super(key: key);

@override
State<LotterView> createState() => _LotterViewState();
}

class _LotterViewState extends State<LotterView> {
//抽奖控制器
final SimpleLotteryController _simpleLotteryController =
SimpleLotteryController();

int page = 1;
int size = 8;

//奖品列表
List dishesList = [];

//奖品loding
bool load = false;

//初始化奖品列表
_initPrizeList() async {
Global.getInstance()!.dio.get("dishes/getDishes", queryParameters: {
'page': 1,
'size': 8,
}).then((value) => {
dishesList = value.data['data']['prizeRecords'],
load = true,
setState(() {})
});
}

@override
void initState() {
super.initState();
_initPrizeList();
}

@override
void dispose() {
_simpleLotteryController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: MediaQuery.removePadding(
removeTop: true,
context: context,
child: ListView(
children: [
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft, //左中
end: Alignment.bottomRight, //右中
colors: [
Theme.of(context).primaryColor,
const Color.fromRGBO(180, 0, 0, 0.1),
const Color.fromRGBO(187, 0, 0, 0.1),
Theme.of(context).primaryColor
]),
),
child: Column(
children: [
Container(
padding: EdgeInsets.only(top: 50.h),
child: Wrap(
children: [
Text('今日菜品',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.w600,
fontFamily: 'jinbu',
color: Colors.white,
)),
Text(
'大放送',
style: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.w600,
fontFamily: 'jinbu',
color: const Color.fromARGB(255, 252, 217, 61)),
)
],
),
),
Container(
margin: EdgeInsets.only(top: 10.h),
alignment: Alignment.center,
child: Text(
'猜猜今天是什么菜系?',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
fontFamily: 'jinbu',
),
),
),
SizedBox(
height: 10.h,
),
Padding(
padding: EdgeInsets.all(12.0.sp),
child: Container(
height: 400.h,
padding: EdgeInsets.all(14.sp),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft, //左中
end: Alignment.bottomRight, //右中
colors: [
Theme.of(context).primaryColor,
const Color.fromRGBO(180, 0, 0, 0.1),
const Color.fromRGBO(187, 0, 0, 0.1),
Theme.of(context).primaryColor
]),
// color: const Color.fromARGB(255, 205, 221, 235),
border: Border.all(
color: Colors.transparent, width: 14.w),
borderRadius:
BorderRadius.all(Radius.elliptical(20.r, 20.r))),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
load
? SimpleLotteryWidget(
dishesList: dishesList,
simpleLotteryController:
_simpleLotteryController)
: SizedBox(
width: double.infinity,
height: 260.h,
child: const GFLoader(
type: GFLoaderType.ios,
loaderColorOne: Colors.blue,
loaderColorTwo: Colors.blue,
loaderColorThree: Colors.blue,
duration: Duration(milliseconds: 300)),
),
//抽奖按钮
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
child: Provider.of<IndexViewModel>(context,
listen: true)
.getIsLotte!
? SizedBox(
height: 34.h,
width: MediaQuery.of(context).size.width -
120.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.red[200], //chan
onPrimary: Colors
.white, //change text color of button
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(30.r),
),
elevation: 3.0.h,
),
onPressed: () {
//跳转详情页面
Push(
context,
DishesDetailView(
dishes:
Provider.of<IndexViewModel>(
context,
listen: false)
.getCurrentDishes));
//设置状态
Provider.of<IndexViewModel>(context,
listen: false)
.setIsLotte(false);
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
SizedBox(
width: 4.w,
),
Text(
'点击查看:${Provider.of<IndexViewModel>(context, listen: false).getCurrentDishes!['dishesName']}',
style: TextStyle(
fontWeight: FontWeight.w500,
fontFamily: '',
color: Colors.white,
fontSize: 14.0.sp),
),
],
),
))
: SizedBox(
height: 34.h,
width: MediaQuery.of(context).size.width -
220.w,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Colors.red[200], //chan
onPrimary: Colors
.white, //change text color of button
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(30.r),
),
elevation: 3.0.h,
),
onPressed: () {
//开始抽奖
_simpleLotteryController.start(
Random.secure().nextInt(2)); //开启
setState(() {});
},
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
SizedBox(
width: 4.w,
),
Text(
'吃啥',
style: TextStyle(
fontWeight: FontWeight.w500,
fontFamily: '',
color: Colors.white,
fontSize: 14.0.sp),
),
],
),
)),
)
],
),
),
),
],
),
),
],
),
),
);
}
}

//奖品参数
class SimpleLotteryValue {
SimpleLotteryValue(
{this.target = 0, this.isFinish = false, this.isPlaying = false});

/// 中奖目标
int target = 0;

bool isPlaying = false;
bool isFinish = false;

SimpleLotteryValue copyWith({
int target = 0,
bool isPlaying = false,
bool isFinish = false,
}) {
return SimpleLotteryValue(
target: target, isFinish: isFinish, isPlaying: isPlaying);
}

@override
String toString() {
return "target : $target , isPlaying : $isPlaying , isFinish : $isFinish";
}
}

//抽奖控制器
class SimpleLotteryController extends ValueNotifier {
SimpleLotteryController() : super(SimpleLotteryValue());

/// 开启抽奖
///
/// [target] 中奖目标
void start(int target) {
// 九宫格抽奖里范围为0~8
assert(target >= 0 && target <= 8);
if (value.isPlaying) {
return;
}
value = value.copyWith(target: target, isPlaying: true);
}

void finish() {
value = value.copyWith(isFinish: true);
}
}

/*
* 奖品列表容器
*/
class SimpleLotteryWidget extends StatefulWidget {
final SimpleLotteryController simpleLotteryController;
final List dishesList;
const SimpleLotteryWidget(
{Key? key,
required this.dishesList,
required this.simpleLotteryController})
: super(key: key);

@override
State<SimpleLotteryWidget> createState() => _SimpleLotteryWidgetState();
}

class _SimpleLotteryWidgetState extends State<SimpleLotteryWidget>
with TickerProviderStateMixin {
Future<int>? future; // 标识

@override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
return Container(
margin: EdgeInsets.all(5.h),
width: double.infinity,
height: 280.h,
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 9,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.h),
itemBuilder: (context, index) {
if (index != 4) {
return commodity(index);
}
return Image.network(
'https://brath.cloud/app_log.png',
fit: BoxFit.cover,
);
}));
},
);
}

// 奖品列表
Widget commodity(int index) {
final int toIndex;
toIndex = _deserializeMap[index];
return GestureDetector(
onTap: () {
opcityPush(context,
ZoomImage(url: '${widget.dishesList[toIndex]['dishesUrl']}'));
},
child: Stack(
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(10.r)),
color: widget.dishesList[toIndex]['id']! > 5
? Colors.red[300]!.withAlpha(32)
: widget.dishesList[toIndex]['id']! >= 4 &&
widget.dishesList[toIndex]['id'] <= 5
? Colors.amber[300]!.withAlpha(32)
: Colors.blue[300]!.withAlpha(32),
),
child: SizedBox(
width: double.infinity,
height: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(
'${widget.dishesList[toIndex]['dishesUrl']}',
fit: BoxFit.cover,
width: 60.w,
height: 60.w,
),
SizedBox(
height: 5.h,
),
Text(
'${widget.dishesList[toIndex]['dishesName']}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: 'jinbu',
fontSize: 9.sp,
color: Theme.of(context).primaryColor,
),
),
],
),
),
),
Container(
decoration: BoxDecoration(
color: index == _currentSelect
? Colors.yellow.withOpacity(0.5)
: Colors.transparent,
borderRadius: BorderRadius.all(Radius.circular(10.r)),
)),
],
),
);
}

Animation? _selectedIndexTween;
AnimationController? _startAnimateController;
int _currentSelect = -1;
int _target = 0;

/// 旋转的圈数
final int repeatRound = 4;
VoidCallback? _listener;

/// 选中下标的映射
final Map _selectMap = {0: 0, 1: 3, 2: 6, 3: 7, 4: 8, 5: 5, 6: 2, 7: 1};

//反下标的映射
final Map _deserializeMap = {
0: 0,
3: 1,
4: 8,
6: 2,
7: 3,
8: 4,
5: 5,
2: 6,
1: 7
};
simpleLotteryWidgetState() {
_listener = () {
// 开启抽奖动画
if (widget.simpleLotteryController.value.isPlaying) {
_startAnimateController?.reset();
_target = widget.simpleLotteryController.value.target;
_selectedIndexTween = _initSelectIndexTween(_target);

_startAnimateController?.forward();
}
};
}

/// 初始化tween
///
/// [target] 中奖的目标
Animation _initSelectIndexTween(int target) =>
StepTween(begin: 0, end: repeatRound * 8 + target).animate(
CurvedAnimation(
parent: _startAnimateController!, curve: Curves.easeOutQuart));

@override
void initState() {
super.initState();

future = Future.value(42);

_startAnimateController =
AnimationController(vsync: this, duration: const Duration(seconds: 5));
_selectedIndexTween = _initSelectIndexTween(_target);

//开启动画
simpleLotteryWidgetState();

// 控制监听
widget.simpleLotteryController.addListener(_listener!);

// 动画监听
_startAnimateController?.addListener(() {
// 更新选中的下标
_currentSelect = _selectMap[_selectedIndexTween?.value % 8];

if (_startAnimateController!.isCompleted) {
widget.simpleLotteryController.finish();
_currentSelect = -1;
dynamic dishes = widget.dishesList[_target];

Provider.of<IndexViewModel>(context, listen: false).setIsLotte(true);
Provider.of<IndexViewModel>(context, listen: false)
.setCurrentDishes(dishes);
DialogUtils.showMessage('恭喜你,今天做:${dishes['dishesName']},点击按钮查看菜品做法!');
}

setState(() {});
});
}

@override
void deactivate() {
widget.simpleLotteryController.removeListener(_listener!);
super.deactivate();
}

@override
void dispose() {
_startAnimateController?.dispose();
super.dispose();
}
}

//获取当前奖品出现的次数
getNumberTimesCurrentPrizeAppears(String? id, List<dynamic> arr) {
//定义集合
Map obj = {};
List idList = [];
for (var i = 0; i < arr.length; i++) {
idList.add(arr[i]['id']);
}
for (var i = 0; i < idList.length; i++) {
if (obj.containsValue([idList[i]])) {
obj[idList[i]]++;
} else {
obj[idList[i]] = 1;
}
}
return obj[int.parse(id!)];
}

int next(int min, int max) {
int res = min + Random().nextInt(max - min + 1);
return res;
}

# DishesDetail.dart

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
import 'package:bruno/bruno.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_swiper_plus/flutter_swiper_plus.dart';
import 'package:weat/common/common.dart';

class DishesDetailView extends StatefulWidget {
DishesDetailView({Key? key,required this.dishes}) : super(key: key);
var dishes;
@override
State<DishesDetailView> createState() => _DishesDetailViewState();
}

class _DishesDetailViewState extends State<DishesDetailView> {
List steplist = [];
@override
void initState() {
super.initState();
//分割步骤
steplist = widget.dishes['dishesStep'].toString().split('\r\n');
}

@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () {
BrnDialogManager.showConfirmDialog(context,
showIcon: false,
barrierDismissible: false,
title: "你正在做菜,要退出吗~",
confirm: "退出",
cancel: "不", onConfirm: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
}, onCancel: () {
Navigator.of(context).pop();
});
return Future.value(false);
},
child: Scaffold(
appBar: getAppBar(widget.dishes['dishesName'], context: context),
body: ListView(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ImageFulWidget(imageList: [widget.dishes['dishesUrl']]),
),
getCardContainer(
margin: EdgeInsets.only(
top: 10.h, bottom: 10.h, left: 10.w, right: 10.w),
height: 220.0.h,
isBorder: true,
BorderCircular: 5.r,
width: double.infinity,
widget: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(left: 10.w, top: 10.h),
child: BrnCSS2Text.toTextView(
'步骤:',
maxLines: 1,
textOverflow: TextOverflow.ellipsis,
defaultStyle:
TextStyle(fontSize: 20.sp, fontFamily: 'jinbu'),
),
),
Padding(
padding: EdgeInsets.only(left: 10.w, top: 10.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: steplist.map((e) {
return Container(
margin: EdgeInsets.only(top: 5.h),
child: BrnCSS2Text.toTextView(
'$e',
maxLines: 1,
textOverflow: TextOverflow.ellipsis,
defaultStyle: TextStyle(
fontSize: 13.sp, fontFamily: 'jinbu'),
),
);
}).toList(),
),
),
],
),
)
],
)),
);
}
}

/*
* 顶部轮播图Widget
*/
class ImageFulWidget extends StatelessWidget {
ImageFulWidget({
Key? key,
required this.imageList,
}) : super(key: key);

List<String>? imageList;

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(20.h), bottom: Radius.circular(20.h)),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.25,
width: MediaQuery.of(context).size.width - 60.0.w,
child: Swiper(
autoplay: false,
duration: 2000,
curve: Curves.linearToEaseOut,
itemBuilder: (BuildContext context, int index) {
return Image.network(
imageList![index],
height: MediaQuery.of(context).size.height * 0.25,
width: MediaQuery.of(context).size.width - 60.0.w,
fit: BoxFit.cover,
);
},
itemCount: imageList!.length,
pagination: SwiperPagination(
builder: DotSwiperPaginationBuilder(
size: 8.sp, // 设置未选中的小点大小
activeSize: 10.sp, // 设置选中的小点大小
color: const Color.fromARGB(255, 219, 219, 219), // 设置为未选中的小点颜色
activeColor: Colors.blue, // 设置选中的小点颜色
),
),
control: const SwiperControl(color: Colors.transparent),
),
),
);
}
}

# main.dart

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
import 'dart:async';
import 'dart:io';
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get_navigation/src/root/get_material_app.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:weat/routers/routes.dart';
import 'package:weat/viewmodel/index_viewmodel.dart';

//全局路由key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

/*
* 主函数
*/
void main() async {
initializeDateFormatting().then((_) => runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => IndexViewModel()),
],
child: MyApp(),
)));

//安卓屏蔽顶部阴影
if (Platform.isAndroid) {
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}

//根节点
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
_init();
}

//全局路由key
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

/// 程序初始化
_init() async {
await EasyLoading.init();
EasyLoading.instance
..displayDuration = const Duration(milliseconds: 2000)
..indicatorType = EasyLoadingIndicatorType.fadingCircle
..loadingStyle = EasyLoadingStyle.dark
..indicatorSize = 45.0
..radius = 10.0
..progressColor = Colors.yellow
..backgroundColor = Colors.green
..indicatorColor = Colors.yellow
..textColor = Colors.yellow
..maskColor = Colors.blue.withOpacity(0.5)
..userInteractions = true
..dismissOnTap = false;

await WidgetsFlutterBinding.ensureInitialized();
await SystemChrome.setPreferredOrientations(
[
// 竖屏 Portrait 模式
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
],
);
}

@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: Size(375, 667),
minTextAdapt: true,
splitScreenMode: true,
builder: (context, child) {
return GetMaterialApp(
theme: ThemeData(
primarySwatch: Colors.blueGrey, //全局主题颜色
splashColor: Colors.transparent, // 点击时的高亮效果设置为透明
// backgroundColor: Color.fromARGB(255, 255, 255, 255), //系统背景主题颜色
backgroundColor: Color(0xFFF3F4F6), //系统背景主题颜色
highlightColor: Colors.transparent, // 长按时的扩散效果设置为透明
primaryColor: Color.fromARGB(188, 0, 0, 97), //系统原色
focusColor: Color.fromARGB(235, 73, 74, 116), //焦点主题颜色
hoverColor: Color.fromARGB(235, 103, 104, 172), //悬停主题颜色
disabledColor: Color.fromARGB(235, 105, 106, 133), //禁用主题颜色
primaryColorLight: Colors.white, // 白色主题颜色
primaryColorDark: Color.fromARGB(235, 73, 74, 116), //黑色主题颜色
selectedRowColor: Color.fromARGB(255, 104, 80, 145), //选中效果颜色
appBarTheme: AppBarTheme(backgroundColor: Colors.white),
buttonTheme: ButtonThemeData(
focusColor: Color.fromARGB(235, 73, 74, 116),
hoverColor: Color.fromARGB(235, 103, 104, 172),
),
tabBarTheme: TabBarTheme(
labelColor: Color.fromARGB(235, 103, 104, 172),
),
progressIndicatorTheme: ProgressIndicatorThemeData(
color: Color.fromARGB(235, 101, 102, 158),
),
),
navigatorKey: navigatorKey,
routes: routes,
debugShowCheckedModeBanner: false,
initialRoute: "/", //初始化进入加载页面
builder: (context, widget) {
return MediaQuery(
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: widget!,
);
},
);
});
}
}

# 代码编写完毕后 flutter build apk 打包到真机运行

# 最终呈现出我们想要的效果:

image-20230224132323576

抽奖中:

image-20230224132344126

中奖:

详情:

image-20230224132350748

# 本期教程到此结束,欢迎你的观看~

# 微信公众号搜索 InterviewCoder 回复关键词《吃啥》获取源码以及开发教程~

二维码mini

# 关于我

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

InterviewCoder

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

【Gmail】国内怎么注册Gmail邮箱?

# 【Gmail】国内怎么注册 Gmail 邮箱?

InterviewCoder

​ 谷歌邮箱(Gmail)是在国际上使用最为频繁的邮箱,不管我们是用来收发邮件,还是用来注册国外的网站,大多数情况下都离不开谷歌邮箱。甚至,不少网站都直接支持谷歌邮箱登录,这就是为什么,国内很多网友想要注册 Gmail 谷歌邮箱的原因。

​ 但是,大部分网友都无法自己完整注册,今天来做一篇完整的谷歌邮箱注册教程,非常详细,一步一步照做即可!

img

# 谷歌邮箱怎么注册?

# 注册前准备

1、手机号码一个(没有注册过谷歌邮箱的手机号码)

2、加速器(魔法)(国内使用谷歌,必须要加速器才行哦,加速器需自备,不然你会连谷歌邮箱官网都无法进入)

# 谷歌 Gmail 邮箱注册教程

1、我们使用浏览器打开谷歌邮箱官网(gmail.google.com),进入谷歌邮箱的登录主页,我们点击左下方的创建账号按钮,选择个人用途。

image-20230420135757242

2、在进入的界面我们不要着急填写资料,我们先修改语言,点击左下方的简体中文。

img

3、这个时候,会弹出国家语言列表,我们选择 “English(United States)”。

img

4、这个时候,界面会变成英语。我们在填写个人信息,如下图填写。填好过后,我们要记住账号和密码信息哦,后面登录要用的。

img

5、这个时候,会跳转到手机号码验证页面,我们选择中国 + 86,填写自己的手机号,点击 Next。如果顺利,你会进入下一步,填写验证码。如果你的加速器美国 IP 质量不行,这里就会提示你 “此电话号码无法用于验证”。遇到这个提示,建议,切换一个优质美国 IP,在操作。

img

6、输入手机收到的验证码,点击 “verify”。

img

7、这个界面,我们只需要填写出生日期即可,别的可不填。出生日期,大家一定要选择成年的年龄哦,别乱填,填好点 next。

img

8、来到这个界面,我们不用管,直接点击 skip。

img

9、隐私条款确认,我们点击 I agree。

img

这个时候,你会发现,账号注册成功了,并且,页面会直接跳转到谷歌邮箱的界面。

# 注册失败原因汇总

其实注册流程并不复杂,很简单,也就只需要几步。可是,国内的网友注册,总是无法注册成功。这里,把最容易导致注册失败的原因,给大家汇总一下,自己操作的时候就要注意了!

# 1、手机号码注册过

你用来注册谷歌邮箱的手机号码,你自己之前注册过谷歌邮箱,再次注册有可能会出现无法注册的情况。虽然,谷歌没有说明一个手机能够不能注册多个账号,但是,我们最好还是不要用注册过的手机号。

# 2、加速器 IP 质量不行

很多网友使用的加速器,IP 地址已经被人多次用来注册过谷歌邮箱了,到你注册的时候,早就被谷歌锁定了,识别你为恶意注册。这种情况,自然无法成功注册。那么,怎么才算高质量的 IP 地址呢?我们尽量使用那些没怎么被人用过的 IP,冷僻的线路即可。

# 3、语言没设置

大家看教程,应该知道,我注册的时候是设置了英语的。再加上使用的 IP 为美国,这样自然,谷歌会认为我是一个美国人在注册,审核机制自然就成了美区的审核机制,没那么严格,成功率会高一些。

# 4、如果以上都不行,那么建议使用 IPHONE 手机注册一下试试,很大几率可以成功!

# 关于我

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

图片

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

ELASTICSEARCH7.X安全性之访问密码设置

InterviewCoder

# ELASTICSEARCH7.X 安全性之访问密码设置

1
当我们安装完ElasticSearch的时候发现,访问过程中我们没有任何安全认证就可以直接访问并操作。如果是生产环境,端口向外暴露的话,那么对数据的安全性是无法得到保障的。

一般解决方案有

  • 开启 ElasticSearch 认证插件,访问的时候添加账密
  • 当然也可以通过 nginx 作代理防护

本文主要讲解通过启用 X-Pack 来设置 ElasticSearch 的访问密码。

集群与单据环境都适合次方法

  • 集群与单据环境配置的区别就是,集群需要在某一台生成证书然后拷贝到其它节点目录下。
  • 集群环境重设置密码的时候需要整个集群节点都已启动,可在任一台处修改。

# 2.X-PACK 简介

1
2
X-Pack是Elastic Stack扩展功能,提供安全性,警报,监视,报告,机器学习和许多其他功能。 ES7.0+之后,默认情况下,当安装Elasticsearch时,会自动安装X-Pack,无需单独再安装。自6.8以及7.1+版本之后,基础级安全永久免费了。
在使用的时候主要需要配置一下证书,以及修改配置文件(config/elasticsearch.yml )

# 3. 证书配置

# 3.1 生成节点证书

切换到 elasticsearch 安装文件目录 bin 下 :示例:/usr/local/elasticsearch-7.4.0/bin
借助 elasticsearch-certutil 命令生成证书:

1
./elasticsearch-certutil ca -out config/certs/elastic-certificates.p12 -pass

这里单独设置了一个 证书文件目录 config/certs
在这里插入图片描述

生成后的证书
在这里插入图片描述

# 3.2 修改配置

配置通信证书 > 需要在 config 目前下 elasticsearch.yml 配置

1
2
3
4
5
6
7
8
9
# 开启xpack
xpack.security.enabled: true
xpack.license.self_generated.type: basic
xpack.security.transport.ssl.enabled: true
# 证书配置
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: certs/elastic-certificates.p12

其它配置(可选)

1
2
3
4
#跨域配置
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type

注:若是集群环境则需要将证书文件目录,以及配置文件,在所有集群环境下都修改一下。

# 3.3. 重启生效

需要重启 elasticsearch

注:若是集群环境下则需要启动所有集群节点,再统一设置密码

注:重启异常情况,若出现报错,类似 failed to load plugin class [org.elasticsearch.xpack.core.XPackPlugin]
请检查是否是使用 root 用户生成的证书,启动用户无权限导致。

# 4. 设置用户密码

执行设置用户名和密码的命令,内置了部分用户
切换到 elasticsearch 安装文件目录 bin 下 :示例:/usr/local/elasticsearch-7.4.0/bin/

1
2
# 手动配置每个用户密码模式(需要一个一个的输入)
./elasticsearch-setup-passwords interactive

也可以先自动配置密码后续再修改

1
2
#自动配置每个用户密码(随机生成并返回字符串密码,需要保存好)
./elasticsearch-setup-passwords auto

下图 1 是自动生成密码情况(一定拷贝下来要牢记密码)
在这里插入图片描述

下图 2 是自定义密码情况
分别为多个用户设置密码例如:elastic, kibana, logstash_system,beats_system,
设置密码的时候需要连续输入 2 遍。
在这里插入图片描述

部分内置账号的角色权限解释如下:

  • elastic 账号:拥有 superuser 角色,是内置的超级用户。
  • kibana 账号:拥有 kibana_system 角色,用户 kibana 用来连接 elasticsearch 并与之通信。Kibana 服务器以该用户身份提交请求以访问集群监视 API 和 .kibana 索引。不能访问 index。
  • logstash_system 账号:拥有 logstash_system 角色。用户 Logstash 在 Elasticsearch 中存储监控信息时使用。

至此单节点安全配置完毕,重启 es 后访问 9200 会出现用户名和密码的提示窗口,我们就可以通过用户生成的密码过行访问了

# 5. 测试访问

通过查看证书方式,顺便测试一下密码是否生效了
浏览器输入 http://IP:9200/_license 可以看到,弹窗出来,需要输入密码了
在这里插入图片描述
在这里插入图片描述

# 附录:常见问题

# 1. 如何修改账号密码

以 elastic 账号为例,注意需要在 elasticsearch 服务已启动的情况下进行

1
curl -H 'Content-Type: application/json' -u elastic:123456 -XPUT 'http://localhost:9200/_xpack/security/user/elastic/_password' -d '{ "password" : "1234567" }'

# 2. 客户端 ES-HEAD 连接问题

连接失败情况下先检查是否是跨域问题

1
2
3
http.cors.enabled: true
http.cors.allow-origin: "*"
http.cors.allow-headers: Authorization,X-Requested-With,Content-Length,Content-Type

例如下图连接的时候报错未授权
在这里插入图片描述

解决方案:在访问的 URL 中拼接授权账号信息
示例:?auth_user=elastic&auth_password=1234567

示例:指定服务端地址以及账户

1
http://IP:9100/?base_uri=http://IP:9200&auth_user=elastic&auth_password=1234567

# 3. 启动报 XPACK 相关错

在这里插入图片描述

DecoderException: javax.net.ssl.SSLHandshakeException: No available authentication scheme
在这里插入图片描述

解决方案:请通过上文配置步骤,排查,检查证书是否已经配置好,以及配置是否填写正确

# 关于我

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

InterviewCoder

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

腾讯云服务器liux系统下无法通过springBoot内置mail发送邮件的解决方案

InterviewCoder

# 腾讯云服务器 liux 系统下无法通过 springBoot 内置 mail 发送邮件的解决方案

原因

原来是腾讯云基于安全考虑,禁用了端口 25。改成 465 或者解封 25 就可以发邮件了。

原始配置 本地可发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
mail:
username: ***********
password: ***********
host: smtp.163.com
port: 25
default-encoding: UTF-8
properties:
mail:
smtp:
timeout: 10000
auth: true
starttls:
enable: true
required: true
socketFactory:
port: 25
class: javax.net.ssl.SSLSocketFactory
fallback: false

修改的配置 测试 部署到服务器上是可以发送的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
mail:
username: ***********
password: ***********
host: smtp.163.com
port: 465
default-encoding: UTF-8
properties:
mail:
smtp:
timeout: 10000
auth: true
starttls:
enable: true
required: true
socketFactory:
port: 465
class: javax.net.ssl.SSLSocketFactory
fallback: false

# 关于我

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

InterviewCoder

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

docker中设置elasticsearch、kibana用户名密码、修改密码

InterviewCoder

# docker 中设置 elasticsearch、kibana 用户名密码、修改密码

#

# 前言

之前在 docker 中安装过 elasticsearch 和 elasticsearchhead 以及 kibana 都没有配置密码,在此记录下设置过程。

# 一、elasticsearch 设置密码

参考 官方文档
xpack.security.enabled: true
设置引导性密码

The setup-passwords tool is the simplest method to set the built-in users’ passwords for the first time. It uses the elastic user’s bootstrap password to run user management API requests. For example, you can run the command in an “interactive” mode, which prompts you to enter new passwords for the elastic, kibana, and logstash_system users:

# 首先开启 X-Pack

修改容器内或者修改挂载出来的 elasticsearch.yml

1
2
3
docker exec -it elasticsearch /bin/bash		# 进入容器内部
vi /data/elasticsearch/config/elasticsearch.yml # 挂载目录
1

elasticsearch.yml 文件添加

1
2
3
4
5
6
cluster.name: "docker-cluster-01"
network.host: 0.0.0.0
http.cors.enabled: true
http.cors.allow-origin: "*"
# 此处开启xpack
xpack.security.enabled: true

重新启动 elasticsearch。

1
docker restart elasticsearch

进入 docker 中的 elasticsearch 中,设置密码,执行

1
/usr/share/elasticsearch/bin/x-pack/setup-passwords interactive

依次设置用户:elastic、apm_system、kibana_system、logstash_system、beats_system、remote_monitoring_user 共 6 个用户。
内部用户
X-Pack 安全有三个内部用户(_system、_xpack 和_xpack_security),负责在 Elasticsearch 集群中进行的操作。

这些用户仅由源自集群内的请求使用。出于这个原因,它们不能用于对 API 进行身份验证,并且没有密码可以管理或重置。

有时,您可能会在日志中找到对这些用户之一的引用,包括审计日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.
You will be prompted to enter passwords as the process progresses.
Please confirm that you would like to continue [y/N]y
Enter password for [elastic]:
Reenter password for [elastic]:
Enter password for [apm_system]:
Reenter password for [apm_system]:
Enter password for [kibana_system]:
Reenter password for [kibana_system]:
Enter password for [logstash_system]:
Reenter password for [logstash_system]:
Enter password for [beats_system]:
Reenter password for [beats_system]:
Enter password for [remote_monitoring_user]:
Reenter password for [remote_monitoring_user]:
Changed password for user [apm_system]
Changed password for user [kibana_system]
Changed password for user [kibana]
Changed password for user [logstash_system]
Changed password for user [beats_system]
Changed password for user [remote_monitoring_user]
Changed password for user [elastic]
12345678910111213141516171819202122

# 测试是否设置成功

1
curl localhost:9200

结果显示:

1
2
[root@VM-24-15-centos config]# curl localhost:9200
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":"Basic realm=\"security\" charset=\"UTF-8\""}}],"type":"security_exception","reason":"missi

显示这个则设置成功。
使用密码访问 elasticsearch 测试是否可以访问。

1
curl localhost:9200 -u elastic

就可以看到 elasticsearch 信息。

# 修改密码

# 已知密码修改

1
2
3
4
5
6
7
POST _xpack/security/user/_password
POST _xpack/security/user/<username>/_password
# 将用户elastic 密码改为elastic
curl -u elastic -H "Content-Type: application/json" -X POST "localhost:9200/_xpack/security/user/elastic/_password" --data '{"password":"elastic"}'
# 测试是否修改成功
curl localhost:9200 -u elastic
123456

登录成功的结果展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 {
"name" : "384cda4775e5",
"cluster_name" : "docker-cluster-01",
"cluster_uuid" : "SOH21TLnQdSZnJq0ZW2iDw",
"version" : {
"number" : "7.14.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "6bc13727ce758c0e943c3c21653b3da82f627f75",
"build_date" : "2021-09-15T10:18:09.722761972Z",
"build_snapshot" : false,
"lucene_version" : "8.9.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

# 忘记密码

创建本地超级账户,然后使用 api 接口本地超级账户重置 elastic 账户的密码

  1. 停止 elasticsearch 服务
  2. 确保你的配置文件中支持本地账户认证支持,如果你使用的是 xpack 的默认配置则无需做特殊修改;如果你配置了其他认证方式则需要确保配置本地认证方式在 ES_HOME/config/elasticsearch.yml 中。
  3. 使用命令 ES_HOME/bin/x-pack/users 创建一个基于本地问价认证的超级管理员。
  4. 进入 docker 容器中 elasticsearch 中,执行
1
2
docker exec -it elasticsearch /bin/bash
bin/x-pack/users useradd test_admin -p test_password -r superuser
  1. 启动 elasticsearch 服务
1
docker restart elasticsearch
  1. 通过 api 重置 elastic 超级管理员的密码
1
curl -u test_admin -XPUT  -H 'Content-Type: application/json' 'http://localhost:9200/_xpack/security/user/elastic/_password' -d '{"password" : "新密码"}'
  1. 校验下密码是否重置成功
1
curl localhost:9200 -u elastic

# 二、kibana 配置 elasticsearch 密码

文档
修改容器内或者修改挂载出来的 kibana.yml

1
2
docker exec -it kibana /bin/bash		# 进入容器内部
vi /data/kibana/config/kibana.yml # 挂载目录

kibana.yml 文件添加

1
2
3
4
5
6
7
8
9
# Default Kibana configuration for docker target
server.host: "0"
server.shutdownTimeout: "5s"
elasticsearch.hosts: [ "http://172.17.0.3:9200" ]
monitoring.ui.container.elasticsearch.enabled: true
i18n.locale: "zh-CN"
# 此处设置elastic的用户名和密码
elasticsearch.username: elastic
elasticsearch.password: elastic

重新启动 elasticsearch。

1
docker restart kibana

访问网址:
在这里插入图片描述
搞定!

新手最近开始写文章,手敲不易,请多多支持!在此感谢每位读者 0.0

# 关于我

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

InterviewCoder

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

【Java】使用 Java 8 中的 Stream ,可以让你写代码事半功倍

InterviewCoder

# 【Java】使用 Java 8 中的 Stream ,可以让你写代码事半功倍

# Stream

Java 8 中一个主要的新功能是引入了流(Stream)功能。在 java.util.stream 中包含用于处理元素序列的类。其中,最重要的类是 Stream<T> 。下面我们就来看看如何使用现有的数据源创建流。

# 创建 Stream

可以使用 stream()of() 方法从不同的数据源(例如:集合、数组)创建流:

1
2
3
4
String[] arr = new String[]{"面", "试", "记};
Stream<String> stream = Arrays.stream(arr);
stream = Stream.of("", "", "");
123

Collection 接口新增了一个 stream() 默认方法,允许使用任何集合作为数据源来创建 Stream<T>

1
2
3
4
5
List<String> list = new ArrayList();
list.add("面");
list.add("试");
list.add("记");
Stream<String> stream = list.stream();

# 在多线程中使用 Stream

Stream 还通过提供 parallelStream() 方法来简化多线程操作,该方法以并行模式运行对流元素的操作。

下面的代码可以对流的每个元素并行运行 doWork() 方法:

1
2
3
4
5
List<String> list = new ArrayList();
list.add("面");
list.add("试");
list.add("记");
list.parallelStream().forEach(element -> doWork(element));

接下来,我们将介绍一些基本的 Stream 操作。

# Stream 操作

在流上可以执行许多有用的操作。它们被分为中间操作(返回 Stream<T> )和终端操作(返回明确定义类型的结果),中间操作允许链接。

我需要注意的是,流上的操作不会改变数据源。

下面是一个简单的例子:

1
2
3
4
5
List<String> list = new ArrayList();
list.add("面");
list.add("试");
list.add("记");
long count = list.stream().distinct().count();

在上面的例子中, distinct() 方法表示一个中间操作,它创建了前一个流的唯一元素的新流。而 count() 方法是一个终端操作,它返回流的大小。

# 迭代

Stream 帮助我们替代了 for、for-each 和 while 循环。它可以让我们把精力集中在操作的逻辑上,而不是在迭代元素序列上。

比如下面的代码:

1
2
3
4
5
for (String string : list) {
if (string.contains("试")) {
return true;
}
}

这段代码只需要一行 Stream 代码就可以实现:

1
boolean isExist = list.stream().anyMatch(element -> element.contains("试"));

# 过滤

filter() 方法可以让我们选择满足谓词条件的元素流。

比如下面的代码:

1
2
3
4
5
List<String> list = new ArrayList();
list.add("面");
list.add("试");
list.add("记");
Stream<String> stream = list.stream().filter(element -> element.contains("试"));

在上面的例子中,创建了一个 List<String>Stream<String> ,查找该流中所有包含字符 “试” 的元素,并创建一个只包含筛选后元素的新流。

# 映射

为了通过将特殊函数应用于流元素来转换它们,并将这些新元素收集到流中,我们可以使用 map() 方法。

比如下面的代码:

1
2
3
4
5
List<String> list = new ArrayList();
list.add("1");
list.add("2");
list.add("3");
Stream<Integer> stream = list.stream().map(str -> Integer.valueOf(str));

在上面的例子中,通过对初始流的每个元素应用特定的 lambda 表达式将 Stream<String> 转换为 Stream<Integer>

如果您有一个流,其中每个元素都包含其自己的元素序列,并且您想创建这些内部元素的流,则应使用 flatMap() 方法。

比如下面的代码:

1
2
3
4
5
6
7
8
public class Writer {
private String name;
private List<String> books;
}

List<Writer> writers = new ArrayList<>();
writers.add(new Writer());
Stream<String> stream = writers.stream().flatMap(writer -> writer.getBooks().stream());

在上面的例子中,我们有一个类型为 Writer 的元素列表。 Writer 类包含一个类型为 List<String> 的字段 books 。使用 flatMap () 方法,字段 books 中的每个元素将被提取并添加到新的结果流中。之后,最开始的 Stream 将会丢失。

# 匹配

Stream 提供了一组方便的工具,根据一些谓词验证一个序列的元素。我们可以使用以下方法之一:

  • anyMatch() :只要有一个条件满足即返回 true
  • allMatch() :必须全部都满足才会返回 true
  • noneMatch() :全都不满足才会返回 true

它们都返回 boolean 的终端操作。

比如下面的代码:

1
2
3
4
5
6
7
List<String> list = new ArrayList();
list.add("面");
list.add("试");
list.add("记");
list.stream().anyMatch(element -> element.contains("面")); // true
list.stream().allMatch(element -> element.contains("面")); // false
list.stream().noneMatch(element -> element.contains("面")); // false

对于空的 Stream,无论给定的谓词是什么,allMatch () 方法都将返回 true:

1
Stream.empty().anyMatch(Objects::nonNull); // false

这是一个合理的值,因为我们找不到不满足谓词的任何元素。

同样地,对于空的 Stream,anyMatch () 方法总是返回 false:

1
Stream.empty().anyMatch(Objects::nonNull); // false

同样地,这也是合理的,因为我们找不到满足这个条件的元素。

# 合并

我可以使用类型为 Stream 的 reduce() 方法,根据指定的函数将一系列元素合并为某个值。这个方法有两个参数:第一个是起始值,第二个是累加器函数。

比如下面的代码:

1
2
List<Integer> integers = Arrays.asList(1, 2, 3);
Integer reduced = integers.stream().reduce(4, (a, b) -> a + b);

在上面的例子中,有一个 List<Integer> ,我们将这些元素加起来,并加上一个初始的整数(在这个例子中是 4)。那么,运行以下代码的结果是 10(4 + 1 + 2 + 3)。

# 收集

在 Stream 类型中,也可以通过 collect() 方法来进行收集。这个操作非常方便,可以将一个流转换为 CollectionMap ,也可以将一个流表示为单个字符串。 Collectors 是一个实用类,提供了几乎所有典型的收集操作的解决方案。对于一些不太常见的任务,可以创建自定义的收集器。

下面的代码使用终端操作 collect()Stream<String> 转换为 List<String>

1
2
List<String> resultList 
= list.stream().map(element -> element.toUpperCase()).collect(Collectors.toList());

# 最后

Stream 的高级示例非常丰富,本文的目的是为了让我们快速了解 Stream 功能的用法,并启发我们继续探索和深入记习。

Stream 是 Java 8 中非常强大和实用的 API,它为开发人员提供了一种更加简便的方式来处理数据。希望我们通过本文的介绍和示例,可以快速上手使用 Stream,并继续深入记习和探索。

# 关于我

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

InterviewCoder

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

【Java】canal服务运行一段时间客户端遍历不到数据

InterviewCoder

# 【Java】canal 服务运行一段时间客户端遍历不到数据

canal:阿里巴巴的一款开源中间件,用于读取数据库 binlog 日志,实时发送给客户端。

问题:使用 canal 后运行一段时间,客户端一直轮询,但是 batchId 始终为 - 1,手动改动数据库数据后,仍然还是获取不到。

1.canal 服务读取出错了
检查 canal 日志关键是 logs\example 下的,看是否由报错提示,如报错列不匹配,或者类型转换异常等,可能是你修改了表结构导致,直接配置 tsdb 数据库,然后删除 meta.dat 缓存即可。可参考上一篇文章

2.canal 读取的 binlog 已被删除
这里首先要介绍 meta.data
位置位于 conf\example 下

{“clientDatas”:[{“clientIdentity”:{“clientId”:1001,“destination”:“example”,“filter”:".\…"},“cursor”:{“identity”:{“slaveId”:-1,“sourceAddress”:{“address”:“rm-m5epj85txc6175on5zo.mysql.rds.aliyuncs.com”,“port”:3306}},“postion”:{“gtid”:"",“included”:false,“journalName”:“mysql-bin.002943”,“position”:151295060,“serverId”:1430559368,“timestamp”:1627023747000}}}],“destination”:“example”}。

这个主要是用来记录 binlog 读取位置的。
首先我们找到 journalName,对应的是 master 数据库 binlog 日志文件名。
position 为读取到 binlog 的位置。
而数据库的 binlog 会随着运行越来越多,所以它会自动删除之前的 binlog 日志。

然后去数据库执行

1
show master logs

数据库binlog

log_name 则为现有的 binlog 日志,查找 binlog 是否和 meta.dat 里面记录的一致,若 meta.data 记录的 binlog 不在里面,则表示已被删除。
可将 journalName 值改为现有的 binlog 日志,然后把 position 置为 4

为什么 canal 读取的 binlog 会被删除呢
1. 手动删除,服务器磁盘容易满,所以偶尔会有人手动删除 binlog 文件。

2.client 端报错,客户端读取到 canal 发过来的数据后进行处理,若处理出错,程序在逻辑上没有 ack 此 batchId,而是去反复执行,那么 client 端会一直执行此条 batchId 数据,此时后面数据会进入 client,而在后面的 batchid 数据进来后会报 canal 的错误,batchId ***,大致意思就是执行的前面的 batchId 发现后面的 batchid 了,然后 canal 就卡在这了,不在读取后面的 binlog,当后面的 binlog 被删除后,就和数据库的 binlog 对应不上了

首先要检查 client 端是否有报错,然后检查 canal 服务是否有报错,在检查 binlog 是否一致,若不一致再分析原因

# 关于我

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

InterviewCoder

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

腾讯云申请的免费证书部署到阿里云CDN和云盾证书中心

InterviewCoder

# 腾讯云申请的免费证书部署到阿里云 CDN 和云盾证书中心

tips:以下内容仅支持有域名的小伙伴配置,没有域名请先申请域名

小伙伴们在有了自己的不知道如何部署 HTTPS?请看下面的操作

​ 为什么不在阿里云申请 SSL 证书

​ 答:阿里云免费证书很慢,而且付费的超级贵

所以本教程让大家在腾讯云官方申请 SSL 证书,注意:有效期只能为 1 年,过期不续费,只能重新申请和配置

1. 进入腾讯云证书官网: https://console.cloud.tencent.com/ssl

2. 点击申请免费证书

image-20230211101221781

3. 输入绑定的域名、校验方式选择自动 DNS 验证、邮箱填写常用邮箱image-20230211101232921

点击提交申请,等待校验通过

4. 申请通过后会颁发证书

image-20230211101359187

5. 点击右侧下载,选择 Nginx 证书压缩包

image-20230211101422722

解压缩后可以看见如下文件

image-20230211101454344

6. 进入阿里云 云盾证书管理 https://yundunnext.console.aliyun.com/?spm=5176.11785003.domainDetail.14.4251142fPqrVnU&p=cas#/certExtend/upload

点击上传证书

image-20230211101539890

image-20230211101638094

image-20230211101727709

点击确定后上传成功。

接下来就可以在阿里云使用你在腾讯云申请的证书来支持你的 CDN HTTPS 服务拉!

image-20230211101919544

# 关于我

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

InterviewCoder

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

Typora使用gitee作为图库用picgo上传图片的教程~

InterviewCoder

# 前言

# 环境配置预览

# 01. 安装 nodejs

请百度自行安装,不过多介绍

node 和 npm 环境检测:

1
2
3
4
5
C:\>node -v
v14.16.0

C:\>npm -v
6.14.11

# 02.Gitee 账户配置

# 新建仓库

img

img

# 私人令牌 token 配置获取

  • 找到 设置 -> 安全设置 -> 私人令牌 ,点击生成新令牌

img

  • 选择下面的选项即可!

img

  • 点击提交,账户安全验证即可

img

  • 保存后面生成的私人令牌

v2-cf484c849d232aaf07e91460899b0a0e_720w

  • gitee 配置到此完毕!

# 03.PicGo 配置

# 插件安装

img

# 插件配置

img

# 上传尝试

img

  • 这里,您可以随便截图一张,剪贴板图片上传试试效果
  • 图片放大看看,本文的图片是不是就在下面这里!哈哈哈!

img

# 04.Typora 配置

  • 说实话,写这篇文章,就是想用到 markdown 自动上传图片,自动同步图片到各个平台!
  • 废话不多说,建议直接和博主配置一样就行!

image-20220428093756533

# 关于我

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

InterviewCoder

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