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

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


  • 首页

  • 归档

  • 搜索

DDD战略战术 道 战略 战术 总结

发表于 2021-08-07

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

《DDD开篇总结》的前三篇已经阐述了几个内容

  1. DDD是什么
  2. 复杂系统的特征
  3. DDD如何应对复杂系统
  4. 模型概念
  5. 软件开发流程

但一般DDD资料中都会分为两部分讲述:战略和战术,所以按这两种分类,重新归纳整合一下

道

在讨论战略和战术前,先表述一下“道”

DDD是一种软件开发的方法论,任何方法论,都必须落实到“减少代码复杂度”

那么“道”是什么呢?

一直认为DDD的战略就是道,结果搞错了

软件开发的终极“道”就是“高内聚、低耦合”,它是任何有价值思想和方法的具象

如何才能达到这个终极道呢?

    1. DRY
    1. 分离关注点
      • 2.1. 业务和技术分离
      • 2.2. 业务和部署分离
      • 2.3. 变与不变分离
    1. 缩小依赖范围
    1. 向稳定方向依赖

战略

DDD战略主要包含统一语言和限界上下文

统一语言

在以往OO开发过程中,会经过OOA,再到OOD,复杂系统中,没有人能全方位了解系统并实现系统,术业有专工,专业人士干伟业事,这是正确的

但分工后,团队合作时沟通至关重要,,在整个系统开发过程中,是有一根主线的,那就是业务知识,业务知识在系统落地前经过层层传递,走样变形是常有的事,从开发人员经过多少次的返工情况就很清楚

如果解决这个问题呢?DDD引入了统一语言,把业务名词含义事先确定好,减少不必要的翻译过程,车同轨,书同文,行同伦

这也消除了业务与技术之间的重复,共同使用业务原语对话

代码就是文档,代码就是领域知识

1
2
scss复制代码userService.love(Jack, Rose) => Jack.love(Rose)
companyService.hire(company,employee) => Company.hire(employee)

界限上下文

界限上下文囊括了实现道的方方面面,如分离关注点,每个上下文围绕一个关注点,通过整洁架构让各层向稳定方向依赖,合理的划分界限,使各个上下文之间减小依赖

说白了界限上下文就是把一个大系统分而治之

界限上下文算是DDD中的核心知识点,但常被技术人员忽视,对于实用主义的程序员来讲,战术常常更吸引人,其实大到微服务,小到实体类,背后都渗透着上下文的概念

引入限界上下文的目的,不在于如何划分边界,而在于如何控制边界

Alberto Brandolini认为bounded context are a mean of safety(限界上下文意味着安全),safety的意思是being in control and no surprise,对限界上下文是可控制的,就意味着你的系统架构与组织结构都是可控的

显然,限界上下文并不是像大多数程序员理解的那样,是模块、服务、组件或子系统,而是你对领域模型、团队合作以及技术风险的控制

限界上下文是“分而治之”架构原则的体现,我们引入它的目的其实为了控制(应对)软件的复杂度,它并非某种固定的设计单元,我们不能说它就是模块、服务或组件,而是通过它来帮助我们做出高内聚低耦合的设计。只要遵循了这个设计,则限界上下文就可能成为模块、服务或组件。所以,文章《Bounded Contexts as a Strategic Pattern Beyond DDD》才会写到:“限界上下文体现的是高层的抽象机制,它并非编程语言或框架的产出工件,而是体现了人们对领域思考的本质。”

战术

对于开发人员而,战术是最实用的,比如聚合、实体、值对象、工厂、仓储、领域事件等等,
使用这些战术组件建模工具,DDD满足了软件真正的技术需求。这些战术设计工具使开发人员能够按照领域专家的思维开发软件

战略部分讲了,界限上下文的思想是核心,在战术组件中都有体现,比如实体,实体就是一个最小上下文,聚合就是相对实体大一点的上下文

但残酷的现实是,花费了大量的精力来学习这些DDD战术组件,却在实现项目中却用不上,为什么呢?因为事务脚本思维太深,分层也大多是从技术角度出发,没有抽象出领域模型,也就是没有OO抽象,没有一个完整的对象,实体都没有,像工厂,值对象也就成了水中花

这也是我们虽然常重构代码,也不过是大类变小类,大函数拆分成小函数,符合一下代码规范,但对整个项目而言,其实没有实质性改进

如何能有实质性改进,对于复杂系统如何运用上这些战术组件呢?

此时,结构性思维发挥作用了

第一步:过程分解,把一个复杂的系统按流程拆解成各个阶段和步骤,这也是事务脚本的强项

第二步:对象建模,过程性拆解虽然可以降低了开发难度,但领域知识被割裂,代码的业务语义也不明确,在这方面OO是强项,提升代码复用性和内聚性

结合这两步,自上而下的结构化分解+自下而上的面向对象建模,过程化分析更好地清理了模型之间的关系,而对象模型提升代码复用性和业务语义表达能力

总结

DDD是一套很好的方法论,有时我们常在理论纯洁性与实战性之间徘徊。这也许是初级阶段常有的纠结点

DDD有适用场景,事务脚本有存在的优势

不能因为现在人们开口闭口都是DDD,就硬要开展DDD

DDD难以落地除了本身带来了很多概念,还需要团队整体素质

软件开发没有银弹,不能偏执于一种理论,实际开发是场硬仗,像混合格斗一样,不在于一招一式是哪门哪派,制敌才是终极目标,所以需要根据自身情况进行裁剪,灵活运用

本文转载自: 掘金

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

Sentinel 应用集成方式

发表于 2021-08-07

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

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

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

Github : 👉 github.com/black-ant

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

一 . 前言

文章目的

  • 梳理 Sentinel 的集成方式
  • 深入 Sentinel 的集成原理

二 . 使用教程

使用共分为2步 : 构建规则和传输实体

2.1 构建规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public void buildRule(String resourceName) {

List<FlowRule> rules = new ArrayList<>();
// 准备流量规则对象
FlowRule rule = new FlowRule();

// 设置 Resource 的 ID -> SphU.entry("HelloWorld")
rule.setResource(resourceName);

// 通过 QPS 限流 已经限流数量
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(20);

// 添加以及加载 Rule
rules.add(rule);
FlowRuleManager.loadRules(rules);
}

2.2 使用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public String flowControlSync(Integer qpsNum) {
logger.info("------> [初始化 Sentinel Rule] <-------");
logger.info("------> [Step 1 : 发起业务流程 , 通过 Sentinel API ] <-------");

for (int i = 0; i < qpsNum; i++) {
Entry entry = null;

try {
// 资源名可使用任意有业务语义的字符串
SphU.asyncEntry("FlowControlSync");
logger.info("------> [进入 Flow Control 业务逻辑 :{}] <-------", i);
} catch (BlockException e) {
logger.error("E----> error :{} -- content :{}", e.getClass(), e.getMessage());
} finally {
if (entry != null) {
entry.exit();
}
}
}

return "success";
}

三 . 深入源码

3.1 FlowRuleManager 加载 rules

在2.1 中通过 FlowRuleManager 构建了一个 Rule :

  1. 创建 FlowRule 对象
  2. 为 Rule 设置资源
  3. 设置限流的策略
  4. FlowRuleManager.loadRules 加载规则

这里来详细看一下 Rules 的处理逻辑 :

Step 1 : 加载资源 , 这里可以看到是放在了 SentinelProperty 里面

1
2
3
4
5
6
7
8
9
10
java复制代码private static SentinelProperty<List<FlowRule>> currentProperty = 
new DynamicSentinelProperty<List<FlowRule>>();

public static void loadRules(List<FlowRule> rules) {
currentProperty.updateValue(rules);
}

// 补充一 : SentinelProperty 对象
- 该对象是一个接口 , 保存配置的当前值,并负责在配置更新时通知所有添加到该配置上的PropertyListener
- 有2个实现类 : DynamicSentinelProperty / NoOpSentinelProperty (空实现)

Step 2 : PropertyListener 监听配置改变

当配置完成后 , 会通知 PropertyListener 相关配置已经改变 , 先来看一下 PropertyListener 体系

System-PropertyListener.png

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码private static final Map<String, List<FlowRule>> flowRules = new ConcurrentHashMap<String, List<FlowRule>>();


C- FlowPropertyListener
public void configUpdate(List<FlowRule> value) {
Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(value);
if (rules != null) {
// 先清空 , 后添加 , 所以此处重复添加无效
flowRules.clear();
flowRules.putAll(rules);
}
}

最终可以得到如下的对象 :

image.png

至此配置部分就完成了 , 下面来看一下拦截的部分 >>>

3.2 执行流程

在这个部分有2个主要的对象 : Entry + SphU

Step 1 : 发起处理请求

1
2
3
4
java复制代码// SphU.entry("FlowControl")
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}

Step 2 : 逻辑判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
// NullContext 表示上下文的数量已经超过阈值,
if (context instanceof NullContext) {
// 如果操过法指, 只初始化一个 entry , 不进行 Rule 校验
return new CtEntry(resourceWrapper, null, context);
}

if (context == null) {
// 如果 context 为空 , 则使用默认 Context
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}

// 全局开关关闭,不进行规则检查
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}

// 获取 Slot 列表链
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

// 如果 chain 为 null. 表示资源数量(槽链)超过常量
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}

Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 执行链表
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}

补充 : resourceWrapper

image.png

Step 2-1 : 初始化 Context

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
java复制代码protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
// private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// 节点为 null ,且不能超过最大容积 -> PRO21001
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
LOCK.lock();
// 这里有点类似于单例的方式
node = contextNameNodeMap.get(name);
if (node == null) {
// MAX_CONTEXT_NAME_SIZE = 2000
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
// PRO21002 : EntranceNode 是什么 ?
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
// 构建 Node
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}

return context;
}

// 问题 PRO21001 : 为什么有个最大容积的概念 ?


// 问题 PRO21002
public class DefaultNode extends StatisticNode {

// 与节点关联的资源
private ResourceWrapper id;
// 子节点集合 ,节点保存唯一性
private volatile Set<Node> childList = new HashSet<>();
// 相关的集群节点
private ClusterNode clusterNode;
}

Step 2-1 : lookProcessChain 获取 Slot 链

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
java复制代码ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
// private static final Object LOCK = new Object();
// 通过对象上锁
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 如果为空 ,构建一个新的 SlotChain
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}


// 补充 : ProcessorSlotChain 结构
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
// 类似于单向的责任链 , 只指向下一个 slot
private AbstractLinkedProcessorSlot<?> next = null;

}

// PS : DefaultProcessorSlotChain 有一个 first
// AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>()

Step 3 : DefaultProcessorSlotChain 处理

调用流程如下 :

  • C- SphU # entry
  • C- CtSph # entry
  • C- CtSph # entryWithPriority
  • C- DefaultProcessorSlotChain # entry
  • C- AbstractLinkedProcessorSlot # transformEntry
  • C- DefaultProcessorSlotChain # entry
  • C- AbstractLinkedProcessorSlot # fireEntry
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
throws Throwable {
first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
}

// 补充 : ProcessorSlot
ProcessorSlot 是一个接口 , 他包括以下几个方法 :
I- ProcessorSlot
M- void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized,Object... args)
M- void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized,Object... args)
M- void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args)
M- void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args)

可以看到这里会分别执行多个 slot ,TODO 具体的 slot 下回分析
image.png

Step 4 : FlowSlot 的处理

这里我们只关注流程 , 下一篇再过一遍 slot , 限流的判断逻辑在 FlowSlot 中 :

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
JAVA复制代码C- FlowSlot
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized);

fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

// Step 2 : 调用 flow 逻辑判断
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

// Step 3 : Rule 判断处理

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
// 获取 rule 集合
Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
if (rules != null) {
for (FlowRule rule : rules) {
// 逐个过滤 ,失败抛出异常
if (!canPassCheck(rule, context, node, count, prioritized)) {
throw new FlowException(rule.getLimitApp(), rule);
}
}
}
}

四 . 容器的处理

4.1 构建 ContextUtil

Step 1 : Context 的存储

1
2
3
4
5
6
java复制代码public class ContextUtil {

// 通过 ThreadLocal 存储 Context
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();

}

Step 2 : Context 的清除

1
2
3
4
5
6
java复制代码public static void exit() {
Context context = contextHolder.get();
if (context != null && context.getCurEntry() == null) {
contextHolder.set(null);
}
}

总结

作为 sentinel 的开篇 ,比较简单 , 主要是过流程

本文转载自: 掘金

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

docker编排参数详解(docker-composeym

发表于 2021-08-07

​这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

docker compose 在 Docker 容器运用中具有很大的学习意义,docker compose 是一个整合发布应用的利器。而使用 docker compose 时,懂得如何编排 docker compose 配置文件是很重要的。

一. 前言

关于 docker compose 技术可以查看官方文档 Docker Compose

以下的内容是确立在已经下载好 Docker 以及 Docker Compose,可参看 Docker Compose 的官方安装教程 Install Docker Compose

二. Docker Compose 配置文件的构建参数说明

首先,官方提供了一个 docker-compose.yml 配置文件的标准例子

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
yaml复制代码version: "3"
services:

redis:
image: redis:alpine
ports:
- "6379"
networks:
- frontend
deploy:
replicas: 2
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure

db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
deploy:
placement:
constraints: [node.role == manager]

vote:
image: dockersamples/examplevotingapp_vote:before
ports:
- 5000:80
networks:
- frontend
depends_on:
- redis
deploy:
replicas: 2
update_config:
parallelism: 2
restart_policy:
condition: on-failure

result:
image: dockersamples/examplevotingapp_result:before
ports:
- 5001:80
networks:
- backend
depends_on:
- db
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure

worker:
image: dockersamples/examplevotingapp_worker
networks:
- frontend
- backend
deploy:
mode: replicated
replicas: 1
labels: [APP=VOTING]
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
window: 120s
placement:
constraints: [node.role == manager]

visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
stop_grace_period: 1m30s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]

networks:
frontend:
backend:

volumes:
db-data:

此文件配置了多个服务,关于此配置文件的各个语句含义就需要弄懂配置选项的含义了

文件配置

compose 文件是一个定义服务、 网络和卷的 YAML 文件 。Compose 文件的默认路径是 ./docker-compose.yml

提示:可以是用 .yml 或 .yaml 作为文件扩展名

服务定义包含应用于为该服务启动的每个容器的配置,就像传递命令行参数一样 docker container create。同样,网络和卷的定义类似于 docker network create 和 docker volume create。

正如 docker container create 在 Dockerfile 指定选项,如 CMD、 EXPOSE、VOLUME、ENV,在默认情况下,你不需要再次指定它们docker-compose.yml。

可以使用 Bash 类 ${VARIABLE} 语法在配置值中使用环境变量。

配置选项

1.bulid

服务除了可以基于指定的镜像,还可以基于一份 Dockerfile,在使用 up 启动之时执行构建任务,这个构建标签就是 build,它可以指定 Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器

1
javascript复制代码build: /path/to/build/dir1

也可以是相对路径

1
bash复制代码build: ./dir1

设定上下文根目录,然后以该目录为准指定 Dockerfile

1
2
3
yaml复制代码build:
context: ../
dockerfile: path/of/Dockerfile123

例子

1
2
3
4
yaml复制代码version: '3'
services:
webapp:
build: ./dir

如果 context 中有指定的路径,并且可以选定 Dockerfile 和 args。那么 args 这个标签,就像 Dockerfile 中的 ARG 指令,它可以在构建过程中指定环境变量,但是在构建成功后取消,在 docker-compose.yml 文件中也支持这样的写法:

1
2
3
4
5
6
7
8
yaml复制代码version: '3'
services:
webapp:
build:
context: ./dir
dockerfile: Dockerfile-alternate
args:
buildno: 1

与 ENV 不同的是,args值可以为空值

1
2
3
markdown复制代码args:
- buildno
- password

如果要指定 image 以及 build ,选项格式为

1
2
makefile复制代码build: ./dir
image: webapp:tag

这会在 ./dir 目录生成一个名为 webaapp 和标记为 tag 的镜像

Note:当用(Version 3) Compose 文件在群集模式下部署堆栈时,该选项被忽略。因为 docker stack 命令只接受预先构建的镜像

2. context

context 选项可以是 Dockerfile 的文件路径,也可以是到链接到 git 仓库的 url.

当提供的值是相对路径时,它被解析为相对于撰写文件的路径,此目录也是发送到 Docker 守护进程的 context

1
2
yaml复制代码build:
context: ./dir

3. dockerfile

使用此 dockerfile 文件来构建,必须指定构建路径

1
2
3
yaml复制代码build:
context: .
dockerfile: Dockerfile-alternate

4. args

添加构建参数,这些参数是仅在构建过程中可访问的环境变量

首先, 在Dockerfile中指定参数:

1
2
3
4
5
bash复制代码ARG buildno
ARG password

RUN echo "Build number: $buildno"
RUN script-requiring-password.sh "$password"

然后指定 build 下的参数,可以传递映射或列表

1
2
3
4
5
yaml复制代码build:
context: .
args:
buildno: 1
password: secret

或

1
2
3
4
5
ini复制代码build:
context: .
args:
- buildno=1
- password=secret

指定构建参数时可以省略该值,在这种情况下,构建时的值默认构成运行环境中的值

1
2
3
markdown复制代码args:
- buildno
- password

Note: YAML 布尔值(true,false,yes,no,on,off)必须使用引号括起来,以为了能够正常被解析为字符串

5. cache_from

编写缓存解析镜像列表

1
2
3
4
5
yaml复制代码build:
context: .
cache_from:
- alpine:latest
- corp/web_app:3.14

6. labels

使用 Docker标签 将元数据添加到生成的镜像中,可以使用数组或字典。

建议使用反向 DNS 标记来防止签名与其他软件所使用的签名冲突

1
2
3
4
5
6
yaml复制代码build:
context: .
labels:
com.example.description: "Accounting webapp"
com.example.department: "Finance"
com.example.label-with-empty-value: ""

或

1
2
3
4
5
6
makefile复制代码build:
context: .
labels:
- "com.example.description=Accounting webapp"
- "com.example.department=Finance"
- "com.example.label-with-empty-value"

7.shm_size

设置容器 /dev/shm 分区的大小,值为表示字节的整数值或表示字符的字符串

1
2
3
yaml复制代码build:
context: .
shm_size: '2gb'

或

1
2
3
yaml复制代码build:
context: .
shm_size: 10000000

8. target

根据对应的 Dockerfile 构建指定 Stage

1
2
3
yaml复制代码build:
context: .
target: prod

9. cap_add、cap_drop

添加或删除容器功能,可查看 man 7 capabilities

1
2
3
4
5
6
makefile复制代码cap_add:
- ALL

cap_drop:
- NET_ADMIN
- SYS_ADMIN

Note:当用(Version 3) Compose 文件在群集模式下部署堆栈时,该选项被忽略。因为 docker stack 命令只接受预先构建的镜像

10. command

覆盖容器启动后默认执行的命令

1
bash复制代码command: bundle exec thin -p 30001

该命令也可以是一个列表,方法类似于 dockerfile:

1
bash复制代码command: ["bundle", "exec", "thin", "-p", "3000"]

11. configs

使用服务 configs 配置为每个服务赋予相应的访问权限,支持两种不同的语法。

Note: 配置必须存在或在 configs 此堆栈文件的顶层中定义,否则堆栈部署失效

  • SHORT 语法

SHORT 语法只能指定配置名称,这允许容器访问配置并将其安装在 /<config_name> 容器内,源名称和目标装入点都设为配置名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码version: "3.3"
services:
redis:
image: redis:latest
deploy:
replicas: 1
configs:
- my_config
- my_other_config
configs:
my_config:
file: ./my_config.txt
my_other_config:
external: true

以上实例使用 SHORT 语法将 redis 服务访问授予 my_config 和 my_other_config ,并被 my_other_config 定义为外部资源,这意味着它已经在 Docker 中定义。可以通过 docker config create 命令或通过另一个堆栈部署。如果外部部署配置都不存在,则堆栈部署会失败并出现 config not found 错误。

Note: config 定义仅在 3.3 版本或在更高版本的撰写文件格式中受支持,YAML 的布尔值(true, false, yes, no, on, off)必须要使用引号引起来(单引号、双引号均可),否则会当成字符串解析。

  • LONG 语法

LONG 语法提供了创建服务配置的更加详细的信息

  • source:Docker 中存在的配置的名称
  • target:要在服务的任务中装载的文件的路径或名称。如果未指定则默认为 /
  • uid 和 gid:在服务的任务容器中拥有安装的配置文件的数字 UID 或 GID。如果未指定,则默认为在Linux上。Windows不支持。
  • mode:在服务的任务容器中安装的文件的权限,以八进制表示法。例如,0444 代表文件可读的。默认是 0444。如果配置文件无法写入,是因为它们安装在临时文件系统中,所以如果设置了可写位,它将被忽略。可执行位可以设置。如果您不熟悉 UNIX 文件权限模式,Unix Permissions Calculator

下面示例在容器中将 my_config 名称设置为 redis_config,将模式设置为 0440(group-readable)并将用户和组设置为 103。该 `redis 服务无法访问 my_other_config 配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码version: "3.3"
services:
redis:
image: redis:latest
deploy:
replicas: 1
configs:
- source: my_config
target: /redis_config
uid: '103'
gid: '103'
mode: 0440
configs:
my_config:
file: ./my_config.txt
my_other_config:
external: true

可以同时授予多个配置的服务相应的访问权限,也可以混合使用 LONG 和 SHORT 语法。定义配置并不意味着授予服务访问权限。

12. cgroup_parent

可以为容器选择一个可选的父 cgroup_parent

1
makefile复制代码cgroup_parent: m-executor-abcd

注意:当 使用(Version 3)Compose 文件在群集模式下部署堆栈时,忽略此选项

13. container_name

为自定义的容器指定一个名称,而不是使用默认的名称

1
makefile复制代码container_name: my-web-container

因为 docker 容器名称必须是唯一的,所以如果指定了一个自定义的名称,不能扩展一个服务超过 1 个容器

14. credential_spec

为托管服务账户配置凭据规范,此选项仅适用于 Windows 容器服务

在 credential_spec 上的配置列表格式为 file:// 或 registry://

使用 file: 应该注意引用的文件必须存在于CredentialSpecs,docker 数据目录的子目录中。在 Windows 上,该目录默认为 C:\ProgramData\Docker\。以下示例从名为C:\ProgramData\Docker\CredentialSpecs\my-credential-spec.json 的文件加载凭证规范 :

1
2
yaml复制代码credential_spec:
file: my-credential-spec.json

使用 registry: 将从守护进程主机上的 Windows 注册表中读取凭据规范。其注册表值必须位于:

1
复制代码HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\Containers\CredentialSpecs

下面的示例通过 my-credential-spec 注册表中指定的值加载凭证规范:

1
2
yaml复制代码credential_spec:
registry: my-credential-spec

15. deploy

指定与部署和运行服务相关的配置

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码version: '3'
services:
redis:
image: redis:alpine
deploy:
replicas: 6
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure

这里有几个子选项

  • endpoint_mode

指定连接到群组外部客户端服务发现方法

  • endpoint_mode:vip :Docker 为该服务分配了一个虚拟 IP(VIP),作为客户端的 “前端“ 部位用于访问网络上的服务。
  • endpoint_mode: dnsrr : DNS轮询(DNSRR)服务发现不使用单个虚拟 IP。Docker为服务设置 DNS 条目,使得服务名称的 DNS 查询返回一个 IP 地址列表,并且客户端直接连接到其中的一个。如果想使用自己的负载平衡器,或者混合 Windows 和 Linux 应用程序,则 DNS 轮询调度(round-robin)功能就非常实用。

version: “3.3”

services:
wordpress:
image: wordpress
ports:

  • 8080:80
    networks:
  • overlay
    deploy:
    mode: replicated
    replicas: 2
    endpoint_mode: vip

mysql:
image: mysql
volumes:

  • db-data:/var/lib/mysql/data
    networks:
  • overlay
    deploy:
    mode: replicated
    replicas: 2
    endpoint_mode: dnsrr

volumes:
db-data:

networks:
overlay:

相关信息:Swarm 模式 CLI 命令 、Configure 服务发现

  • labels

指定服务的标签,这些标签仅在服务上设置。

1
2
3
4
5
6
7
yaml复制代码version: "3"
services:
web:
image: web
deploy:
labels:
com.example.description: "This label will appear on the web service"

通过将 deploy 外面的 labels 标签来设置容器上的 labels

1
2
3
4
5
6
yaml复制代码version: "3"
services:
web:
image: web
labels:
com.example.description: "This label will appear on all containers for the web service"
  • mode

global:每个集节点只有一个容器

replicated:指定容器数量(默认)

1
2
3
4
5
6
yaml复制代码version: '3'
services:
worker:
image: dockersamples/examplevotingapp_worker
deploy:
mode: global
  • placement

指定 constraints 和 preferences

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码version: '3'
services:
db:
image: postgres
deploy:
placement:
constraints:
- node.role == manager
- engine.labels.operatingsystem == ubuntu 14.04
preferences:
- spread: node.labels.zone
  • replicas

如果服务是 replicated(默认),需要指定运行的容器数量

1
2
3
4
5
6
7
8
9
10
yaml复制代码version: '3'
services:
worker:
image: dockersamples/examplevotingapp_worker
networks:
- frontend
- backend
deploy:
mode: replicated
replicas: 6
  • resources

配置资源限制

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码version: '3'
services:
redis:
image: redis:alpine
deploy:
resources:
limits:
cpus: '0.50'
memory: 50M
reservations:
cpus: '0.25'
memory: 20M

此例子中,redis 服务限制使用不超过 50M 的内存和 0.50(50%)可用处理时间(CPU),并且 保留 20M 了内存和 0.25 CPU时间

  • restart_policy

配置容器的重新启动,代替 restart

condition:值可以为 none 、on-failure 以及 any(默认)

delay:尝试重启的等待时间,默认为 0

max_attempts:在放弃之前尝试重新启动容器次数(默认:从不放弃)。如果重新启动在配置中没有成功 window,则此尝试不计入配置max_attempts 值。例如,如果 max_attempts 值为 2,并且第一次尝试重新启动失败,则可能会尝试重新启动两次以上。windows:在决定重新启动是否成功之前的等时间,指定为持续时间(默认值:立即决定)。

1
2
3
4
5
6
7
8
9
10
yaml复制代码version: "3"
services:
redis:
image: redis:alpine
deploy:
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
  • update_config

配置更新服务,用于无缝更新应用(rolling update)

parallelism:一次性更新的容器数量

delay:更新一组容器之间的等待时间。

failure_action:如果更新失败,可以执行的的是 continue、rollback 或 pause (默认)

monitor:每次任务更新后监视失败的时间(ns|us|ms|s|m|h)(默认为0)

max_failure_ratio:在更新期间能接受的失败率

order:更新次序设置,top-first(旧的任务在开始新任务之前停止)、start-first(新的任务首先启动,并且正在运行的任务短暂重叠)(默认 stop-first)

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码version: '3.4'
services:
vote:
image: dockersamples/examplevotingapp_vote:before
depends_on:
- redis
deploy:
replicas: 2
update_config:
parallelism: 2
delay: 10s
order: stop-first

不支持 Docker stack desploy 的几个子选项 build、cgroup_parent、container_name、devices、tmpfs、external_links、inks、network_mode、restart、security_opt、stop_signal、sysctls、userns_mode

16. devices

设置映射列表,与 Docker 客户端的 –device 参数类似 :

1
2
makefile复制代码devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"

17. depends_on

此选项解决了启动顺序的问题

在使用 Compose 时,最大的好处就是少打启动命令,但是一般项目容器启动的顺序是有要求的,如果直接从上到下启动容器,必然会因为容器依赖问题而启动失败。例如在没启动数据库容器的时候启动了应用容器,这时候应用容器会因为找不到数据库而退出,为了避免这种情况我们需要加入一个标签,就是 depends_on,这个标签解决了容器的依赖、启动先后的问题。

指定服务之间的依赖关系,有两种效果

docker-compose up 以依赖顺序启动服务,下面例子中 redis 和 db 服务在 web 启动前启动docker-compose up SERVICE 自动包含 SERVICE 的依赖性,下面例子中,例如下面容器会先启动 redis 和 db 两个服务,最后才启动 web 服务:

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码version: '3'
services:
web:
build: .
depends_on:
- db
- redis
redis:
image: redis
db:
image: postgres

注意的是,默认情况下使用 docker-compose up web 这样的方式启动 web 服务时,也会启动 redis 和 db 两个服务,因为在配置文件中定义了依赖关系

18. dns

自定义 DNS 服务器,与 –dns 具有一样的用途,可以是单个值或列表

1
2
3
4
makefile复制代码dns: 8.8.8.8
dns:
- 8.8.8.8
- 9.9.9.9

19. dns_search

自定义 DNS 搜索域,可以是单个值或列表

1
2
3
4
makefile复制代码dns_search: example.com
dns_search:
- dc1.example.com
- dc2.example.com

20. tmpfs

挂载临时文件目录到容器内部,与 run 的参数一样效果,可以是单个值或列表

1
2
3
4
bash复制代码tmpfs: /run
tmpfs:
- /run
- /tmp

21. entrypoint

在 Dockerfile 中有一个指令叫做 ENTRYPOINT 指令,用于指定接入点。在 docker-compose.yml 中可以定义接入点,覆盖 Dockerfile 中的定义:

1
javascript复制代码entrypoint: /code/entrypoint.sh

entrypoint 也可以是一个列表,方法类似于 dockerfile

1
2
3
4
5
6
7
ini复制代码entrypoint:
- php
- -d
- zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so
- -d
- memory_limit=-1
- vendor/bin/phpunit

21. env_file

从文件中添加环境变量。可以是单个值或是列表 如果已经用 docker-compose -f FILE 指定了 Compose 文件,那么 env_file 路径值为相对于该文件所在的目录

但 environment 环境中的设置的变量会会覆盖这些值,无论这些值未定义还是为 None

1
bash复制代码env_file: .env

或者根据 docker-compose.yml 设置多个:

1
2
3
4
bash复制代码env_file:
- ./common.env
- ./apps/web.env
- /opt/secrets.env

环境配置文件 env_file 中的声明每行都是以 VAR=VAL 格式,其中以 # 开头的被解析为注释而被忽略

注意环境变量配置列表的顺序*,例如下面例子

docker_compose.yml

1
2
3
4
5
yaml复制代码services:
some-service:
env_file:
- a.env
- b.env

a.env 文件

1
2
ini复制代码# a.env
VAR=1

b.env文件

1
2
ini复制代码# b.env
VAR=2

对于在文件a.env 中指定的相同变量但在文件 b.env 中分配了不同的值,如果 b.env 像下面列在 a.env 之后,则刚在 a.env 设置的值被 b.env 相同变量的值覆盖,此时 $VAR 值为 hello。此外,这里所说的环境变量是对宿主机的 Compose 而言的,如果在配置文件中有 build 操作,这些变量并不会进入构建过程中,如果要在构建中使用变量还是首选 arg 标签

22. environment

添加环境变量,可以使用数组或字典。与上面的 env_file 选项完全不同,反而和 arg 有几分类似,这个标签的作用是设置镜像变量,它可以保存变量到镜像里面,也就是说启动的容器也会包含这些变量设置,这是与 arg 最大的不同。 一般 arg 标签的变量仅用在构建过程中。而 environment 和 Dockerfile 中的 ENV 指令一样会把变量一直保存在镜像、容器中,类似 docker run -e 的效果

1
2
3
4
yaml复制代码environment:
RACK_ENV: development
SHOW: 'true'
SESSION_SECRET:

或

1
2
3
4
ini复制代码environment:
- RACK_ENV=development
- SHOW=true
- SESSION_SECRET

23. expose

暴露端口,但不映射到宿主机,只被连接的服务访问。这个标签与 Dockerfile 中的 EXPOSE 指令一样,用于指定暴露的端口,但是只是作为一种参考,实际上 docker-compose.yml 的端口映射还得 ports 这样的标签

1
2
3
makefile复制代码expose:
- "3000"
- "8000"

24. external_links

链接到 docker-compose.yml 外部的容器,甚至 并非 Compose 项目文件管理的容器。参数格式跟 links 类似

在使用Docker过程中,会有许多单独使用 docker run 启动的容器的情况,为了使 Compose 能够连接这些不在docker-compose.yml 配置文件中定义的容器,那么就需要一个特殊的标签,就是 external_links,它可以让Compose 项目里面的容器连接到那些项目配置外部的容器(前提是外部容器中必须至少有一个容器是连接到与项目内的服务的同一个网络里面)。

格式如下

1
2
3
4
markdown复制代码external_links:
- redis_1
- project_db_1:mysql
- project_db_1:postgresql

25. extra_hosts

添加主机名的标签,就是往 /etc/hosts 文件中添加一些记录,与 Docker 客户端 中的 –add-host 类似:

1
2
3
makefile复制代码extra_hosts:
- "somehost:162.242.195.82"
- "otherhost:50.31.209.229"

具有 IP 地址和主机名的条目在 /etc/hosts 内部容器中创建。启动之后查看容器内部 hosts ,例如:

1
2
复制代码162.242.195.82  somehost
50.31.209.229 otherhost

26.healthcheck

用于检查测试服务使用的容器是否正常

1
2
3
4
5
6
bash复制代码healthcheck:
test: ["CMD", "curl", "-f", "http://localhost"]
interval: 1m30s
timeout: 10s
retries: 3
start_period: 40s

interval,timeout 以及 start_period 都定为持续时间

test 必须是字符串或列表,如果它是一个列表,第一项必须是 NONE,CMD 或 CMD-SHELL ;如果它是一个字符串,则相当于指定CMD-SHELL 后跟该字符串。

1
2
3
4
5
6
bash复制代码# Hit the local web app
test: ["CMD", "curl", "-f", "http://localhost"]

# As above, but wrapped in /bin/sh. Both forms below are equivalent.
test: ["CMD-SHELL", "curl -f http://localhost || exit 1"]
test: curl -f https://localhost || exit

如果需要禁用镜像的所有检查项目,可以使用 disable:true,相当于 test:[“NONE”]

1
2
yaml复制代码healthcheck:
disable: true

27. image

从指定的镜像中启动容器,可以是存储仓库、标签以及镜像 ID

1
2
3
4
5
arduino复制代码image: redis
image: ubuntu:14.04
image: tutum/influxdb
image: example-registry.com:4000/postgresql
image: a4bc65fd

如果镜像不存在,Compose 会自动拉去镜像

28. isolation

Linux 上仅仅支持 default 值

29. labels

使用 Docker 标签将元数据添加到容器,可以使用数组或字典。与 Dockerfile 中的 LABELS 类似:

1
2
3
4
5
6
7
8
9
vbnet复制代码labels:
com.example.description: "Accounting webapp"
com.example.department: "Finance"
com.example.label-with-empty-value: ""

labels:
- "com.example.description=Accounting webapp"
- "com.example.department=Finance"
- "com.example.label-with-empty-value"

30.links

链接到其它服务的中的容器,可以指定服务名称也可以指定链接别名(SERVICE:ALIAS),与 Docker 客户端的 –link 有一样效果,会连接到其它服务中的容器

1
2
3
4
5
markdown复制代码web:
links:
- db
- db:database
- redis

使用的别名将会自动在服务容器中的 /etc/hosts 里创建。例如:

1
2
3
复制代码172.12.2.186  db
172.12.2.186 database
172.12.2.187 redis

相应的环境变量也将被创建

31. logging

配置日志服务

1
2
3
4
yaml复制代码logging:
driver: syslog
options:
syslog-address: "tcp://192.168.0.42:123"

该 driver值是指定服务器的日志记录驱动程序,默认值为 json-file,与 –log-diver 选项一样

1
2
3
vbnet复制代码driver: "json-file"
driver: "syslog"
driver: "none"

注意:只有驱动程序 json-file 和 journald 驱动程序可以直接从 docker-compose up 和 docker-compose logs 获取日志。使用任何其他方式不会显示任何日志。

对于可选值,可以使用 options 指定日志记录中的日志记录选项

1
2
3
vbnet复制代码driver: "syslog"
options:
syslog-address: "tcp://192.168.0.42:123"

默认驱动程序 json-file 具有限制存储日志量的选项,所以,使用键值对来获得最大存储大小以及最小存储数量

1
2
3
arduino复制代码options:
max-size: "200k"
max-file: "10"

上面实例将存储日志文件,直到它们达到max-size:200kB,存储的单个日志文件的数量由该 max-file 值指定。随着日志增长超出最大限制,旧日志文件将被删除以存储新日志

docker-compose.yml 限制日志存储的示例

1
2
3
4
5
6
7
8
yaml复制代码services:
some-service:
image: some-service
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "10"

32. network_mode

网络模式,用法类似于 Docke 客户端的 –net 选项,格式为:service:[service name]

1
2
3
4
5
vbnet复制代码network_mode: "bridge"
network_mode: "host"
network_mode: "none"
network_mode: "service:[service name]"
network_mode: "container:[container name/id]"

可以指定使用服务或者容器的网络

33. networks

加入指定网络

1
2
3
4
5
yaml复制代码services:
some-service:
networks:
- some-network
- other-network

34. aliases

同一网络上的其他容器可以使用服务器名称或别名来连接到其他服务的容器

1
2
3
4
5
6
7
8
9
10
yaml复制代码services:
some-service:
networks:
some-network:
aliases:
- alias1
- alias3
other-network:
aliases:
- alias

下面实例中,提供 web 、worker以及db 服务,伴随着两个网络 new 和 legacy 。

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
yaml复制代码version: '2'

services:
web:
build: ./web
networks:
- new

worker:
build: ./worker
networks:
- legacy

db:
image: mysql
networks:
new:
aliases:
- database
legacy:
aliases:
- mysql

networks:
new:
legacy:

相同的服务可以在不同的网络有不同的别名

35. ipv4_address、ipv6_address

为服务的容器指定一个静态 IP 地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码version: '2.1'

services:
app:
image: busybox
command: ifconfig
networks:
app_net:
ipv4_address: 172.16.238.10
ipv6_address: 2001:3984:3989::10

networks:
app_net:
driver: bridge
enable_ipv6: true
ipam:
driver: default
config:
-
subnet: 172.16.238.0/24
-
subnet: 2001:3984:3989::/64

36. PID

1
vbnet复制代码pid: "host"

将 PID 模式设置为主机 PID 模式,可以打开容器与主机操作系统之间的共享 PID 地址空间。使用此标志启动的容器可以访问和操作宿主机的其他容器,反之亦然。

37. ports

映射端口

  • SHORT 语法

可以使用 HOST:CONTAINER 的方式指定端口,也可以指定容器端口(选择临时主机端口),宿主机会随机映射端口

1
2
3
4
5
6
7
8
9
makefile复制代码ports:
- "3000"
- "3000-3005"
- "8000:8000"
- "9090-9091:8080-8081"
- "49100:22"
- "127.0.0.1:8001:8001"
- "127.0.0.1:5000-5010:5000-5010"
- "6060:6060/udp"

注意:当使用 HOST:CONTAINER 格式来映射端口时,如果使用的容器端口小于 60 可能会得到错误得结果,因为YAML 将会解析 xx:yy 这种数字格式为 60 进制,所以建议采用字符串格式。

  • LONG 语法

LONG 语法支持 SHORT 语法不支持的附加字段

target:容器内的端口

published:公开的端口

protocol: 端口协议(tcp 或 udp)

mode:通过host 用在每个节点还是哪个发布的主机端口或使用 ingress 用于集群模式端口进行平衡负载,

1
2
3
4
5
yaml复制代码ports:
- target: 80
published: 8080
protocol: tcp
mode: host

38. secrets

通过 secrets为每个服务授予相应的访问权限

  • SHORT 语法

version: “3.1”
services:
redis:
image: redis:latest
deploy:
replicas: 1
secrets:

  • my_secret
  • my_other_secret
    secrets:
    my_secret:
    file: ./my_secret.txt
    my_other_secret:
    external: true
  • LONG 语法

LONG 语法可以添加其他选项

source:secret 名称

target:在服务任务容器中需要装载在 /run/secrets/ 中的文件名称,如果 source 未定义,那么默认为此值

uid&gid:在服务的任务容器中拥有该文件的 UID 或 GID 。如果未指定,两者都默认为 0。

mode:以八进制表示法将文件装载到服务的任务容器中 /run/secrets/ 的权限。例如,0444 代表可读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码version: "3.1"
services:
redis:
image: redis:latest
deploy:
replicas: 1
secrets:
- source: my_secret
target: redis_secret
uid: '103'
gid: '103'
mode: 0440
secrets:
my_secret:
file: ./my_secret.txt
my_other_secret:
external: true

39. security_opt

为每个容器覆盖默认的标签。简单说来就是管理全部服务的标签,比如设置全部服务的 user 标签值为 USER

1
2
3
sql复制代码security_opt:
- label:user:USER
- label:role:ROLE

40. stop_grace_period

在发送 SIGKILL 之前指定 stop_signal ,如果试图停止容器(如果它没有处理 SIGTERM(或指定的任何停止信号)),则需要等待的时间

1
2
makefile复制代码stop_grace_period: 1s
stop_grace_period: 1m30s

默认情况下,stop 在发送SIGKILL之前等待10秒钟容器退出

41. stop_signal

设置另一个信号来停止容器。在默认情况下使用的 SIGTERM 来停止容器。设置另一个信号可以使用 stop_signal 标签:

1
makefile复制代码stop_signal: SIGUSR

42. sysctls

在容器中设置的内核参数,可以为数组或字典

1
2
3
4
5
6
7
yaml复制代码sysctls:
net.core.somaxconn: 1024
net.ipv4.tcp_syncookies: 0

sysctls:
- net.core.somaxconn=1024
- net.ipv4.tcp_syncookies=0

43. ulimits

覆盖容器的默认限制,可以单一地将限制值设为一个整数,也可以将soft/hard 限制指定为映射

1
2
3
4
5
yaml复制代码ulimits:
nproc: 65535
nofile:
soft: 20000
hard: 40000

44. userns_mode

1
vbnet复制代码userns_mode: "host"

45. volumes

挂载一个目录或者一个已存在的数据卷容器,可以直接使用 HOST:CONTAINER 这样的格式,或者使用 HOST:CONTAINER:ro 这样的格式,后者对于容器来说,数据卷是只读的,这样可以有效保护宿主机的文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
yaml复制代码version: "3.2"
services:
web:
image: nginx:alpine
volumes:
- type: volume
source: mydata
target: /data
volume:
nocopy: true
- type: bind
source: ./static
target: /opt/app/static

db:
image: postgres:latest
volumes:
- "/var/run/postgres/postgres.sock:/var/run/postgres/postgres.sock"
- "dbdata:/var/lib/postgresql/data"

volumes:
mydata:
dbdata:

Compose 的数据卷指定路径可以是相对路径,使用 . 或者 .. 来指定相对目录。

数据卷的格式可以是下面多种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
javascript复制代码volumes:
# 只是指定一个路径,Docker 会自动在创建一个数据卷(这个路径是容器内部的)。
- /var/lib/mysql

# 使用绝对路径挂载数据卷
- /opt/data:/var/lib/mysql

# 以 Compose 配置文件为中心的相对路径作为数据卷挂载到容器。
- ./cache:/tmp/cache

# 使用用户的相对路径(~/ 表示的目录是 /home/<用户目录>/ 或者 /root/)。
- ~/configs:/etc/configs/:ro

# 已经存在的命名的数据卷。
- datavolume:/var/lib/mysql

如果你不使用宿主机的路径,可以指定一个 volume_driver

1
makefile复制代码volume_driver: mydriver
  • SHORT 语法

可以选择在主机(HOST:CONTAINER)或访问模式(HOST:CONTAINER:ro)上指定路径。

可以在主机上挂载相对路径,该路径相对于正在使用的 Compose 配置文件的目录进行扩展。相对路径应始终以 . 或 .. 开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码volumes:
# Just specify a path and let the Engine create a volume
- /var/lib/mysql

# Specify an absolute path mapping
- /opt/data:/var/lib/mysql

# Path on the host, relative to the Compose file
- ./cache:/tmp/cache

# User-relative path
- ~/configs:/etc/configs/:ro

# Named volume
- datavolume:/var/lib/mysql
  • LONG 语法

LONG 语法有些附加字段

type:安装类型,可以为 volume、bind 或 tmpfs

source:安装源,主机上用于绑定安装的路径或定义在顶级 volumes密钥中卷的名称 ,不适用于 tmpfs 类型安装。

target:卷安装在容器中的路径

read_only:标志将卷设置为只读

bind:配置额外的绑定选项

propagation:用于绑定的传播模式

volume:配置额外的音量选项

nocopy:创建卷时禁止从容器复制数据的标志

tmpfs:配置额外的 tmpfs 选项

size:tmpfs 的大小,以字节为单位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码version: "3.2"
services:
web:
image: nginx:alpine
ports:
- "80:80"
volumes:
- type: volume
source: mydata
target: /data
volume:
nocopy: true
- type: bind
source: ./static
target: /opt/app/static

networks:
webnet:

volumes:
mydata:

46. volumes_from

从其它容器或者服务挂载数据卷,可选的参数是 :ro 或 :rw,前者表示容器只读,后者表示容器对数据卷是可读可写的(默认情况为可读可写的)。

1
2
3
4
5
markdown复制代码volumes_from:
- service_name
- service_name:ro
- container:container_name
- container:container_name:rw

47. 用于服务、群集以及堆栈文件的卷

在使用服务,群集和 docker-stack.yml 文件时,请记住支持服务的任务(容器)可以部署在群集中的任何节点上,并且每次更新服务时都可能是不同的节点。

在缺少指定源的命名卷的情况下,Docker 为支持服务的每个任务创建一个匿名卷。关联的容器被移除后,匿名卷不会保留。

如果希望数据持久存在,请使用可识别多主机的命名卷和卷驱动程序,以便可以从任何节点访问数据。或者,对该服务设置约束,以便将其任务部署在具有该卷的节点上。

下面一个例子,Docker Labs 中 votingapp 示例的 docker-stack.yml文件中定义了一个称为 db 的服务。它被配置为一个命名卷来保存群体上的数据, 并且仅限于在节点上运行。下面是来自该文件的部分内容:db postgres manager

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码version: "3"
services:
db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
networks:
- backend
deploy:
placement:
constraints: [node.role == manager]

48. restart

默认值为 no ,即在任何情况下都不会重新启动容器;当值为 always 时,容器总是重新启动;当值为 on-failure 时,当出现 on-failure 报错容器退出时,容器重新启动。

1
2
3
4
vbnet复制代码restart: "no"
restart: always
restart: on-failure
restart: unless-stopped

49. 其他选项

关于标签:cpu_shares、cpu_quota、 cpuse、domainname、hostname、 ipc、 mac_address、privileged、 read_only、 shm_size、stdin_open、tty、 user、 working_dir

上面这些都是一个单值的标签,类似于使用 docker run 的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
vbnet复制代码cpu_shares: 73
cpu_quota: 50000
cpuset: 0,1

user: postgresql
working_dir: /code

domainname: foo.com
hostname: foo
ipc: host
mac_address: 02:42:ac:11:65:43

privileged: true


read_only: true
shm_size: 64M
stdin_open: true
tty: true

50. 持续时间

某些配置选项如 check 的子选项interval以及timeout 的设置格式

1
2
3
4
5
复制代码2.5s
10s
1m30s
2h32m
5h34m56s

支持的单位有 us、ms、s、m 以及 h

51. 指定字节值

某些选项如 bulid 的子选项 shm_size

1
2
3
4
5
复制代码2b
1024kb
2048k
300m
1gb

支持的单位是 b,k,m 以及 g,或 kb, mb 和 gb。目前不支持十进制值

52. extends

这个标签可以扩展另一个服务,扩展内容可以是来自在当前文件,也可以是来自其他文件,相同服务的情况下,后来者会有选择地覆盖原有配置

1
2
3
yaml复制代码extends:
file: common.yml
service: webapp

用户可以在任何地方使用这个标签,只要标签内容包含 file 和 service 两个值就可以了。file 的值可以是相对或者绝对路径,如果不指定 file 的值,那么 Compose 会读取当前 YML 文件的信息。

​

​

本文转载自: 掘金

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

常常听到的数据字典? 数据库教程5:数据字典 数据库的数据

发表于 2021-08-07

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

比较偏概念性的内容

在进行数据库设计时,常常在需求分析的基础上,设计完善的数据字典和需求分析报告。需求分析报告是对业务整体需求的完整描述、分析和总结。

而数据字典则是进行后续数据库概念、逻辑结构、物理存储以及开发等的重要依据。

数据库的数据字典

数据字典是关于数据库中数据的描述,称为元数据。

它不是数据本身,而是数据的数据。是对基础数据的描述。

数据字典在需求分析阶段建立,在数据库设计过程中不断修改、充实、完善。

数据字典是进行详细的数据收集和分析所获得的主要结果。

注意要和DBMS(数据库管理系统)的数据字典的区别。

数据字典的内容

  • 数据项
  • 数据结构
  • 数据流
  • 数据存储
  • 处理过程

数据项

数据项是数据的最小组成单位,是不可再分的数据单位,若干个数据项可以组成一个数据结构。

通过对数据项和数据结构的定义来描述数据流、数据存储的逻辑内容。

数据项描述

数据项描述 = {数据项名,数据项含义说明,别名, 数据类型,长度,取值范围,取值含义,与其他数据项的逻辑关系, 数据项之间的联系 }

需要根据数据依赖的概念分析和抽象数据项之间的联系——函数依赖。

其中的“取值范围”、“与其他数据项的逻辑关系”定义了数据的完整性约束条件,是模式设计、完整性检查条件、触发器、存储过程的设计依据。

比如,以学籍管理系统中学号数据项为例,它的数据字典为:

1
2
3
4
5
6
7
8
sql复制代码数据项: 学号
含义说明:唯一标识每个学生
别名: 学生编号
类型: 字符型
长度: 9
取值范围:0000 00 000至9999 99 999
取值含义:前4位标别该学生入学年份,第5第6位所在专业系编号,后3位按顺序编号,例如202015008
与其他数据项的逻辑关系:学号的值确定了其他数据项的值

数据结构

数据结构反映了数据之间的组合关系。

一个数据结构可以由若干个数据项组成,也可以由若干个数据结构组成,或由
若干个数据项和数据结构混合组成。

对数据结构的描述
数据结构描述= {数据结构名,含义说明,组成: {数据项或数据结构} }

比如,以学籍管理系统中“学生”为例,“学生”是该系统中的一个核心数据结构:

1
2
3
sql复制代码数据结构:学生
含义说明:学籍管理子系统的主体数据结构,定义了一个学生的有关信息
组成: 学号,姓名,性别,年龄,所在系,年级

数据流

数据流是数据结构在系统内部传输的路径。

对数据流的描述
数据流描述={ 数据流名,说明,数据流来源,数据流去向, 组成: {数据结构}, 平均流量,高峰期流量 }

  • 数据流来源:说明该数据流来自哪个处理过程/数据存储
  • 数据流去向:说明该数据流将到哪个处理过程/数据存储区
  • 平均流量:在单位时间(每天、每周、每月等)里的传输次数
  • 高峰期流量:在高峰时期的数据流量

以数据流“体检结果”位列,可如下描述:

1
2
3
4
5
6
7
8
sql复制代码 数据流: 体检结果
说明: 学生参加体格检查的最终报告
数据流来源:体检(处理过程)
数据流去向:批准(处理过程)
组成: { 学号, {血常规},{尿常规},{血液生化},{心电图},
{B超}, … … {其他体检} }
平均流量: 每天200
高峰期流量:每天400

数据存储

数据存储是数据结构停留或保存的地方,也是数据流的来源和去向之一。

对数据存储的描述

数据存储描述={数据存储名,说明,编号,输入的数据流 ,输出的数据流, 组成: {数据结构}, 数据量, 存取频度, 存取方式}

  • 存取频度:每小时、每天或每周存取次数,每次存取的数据量等信息
  • 存取方法:批处理 / 联机处理;检索 / 更新;顺序检索 / 随机检索
  • 输入的数据流:数据来源
  • 输出的数据流:数据去向

以数据存储“学生登记表”为例,可如下描述:

1
2
3
4
5
6
7
8
sql复制代码 数据存储: 学生登记表
说明: 记录学生的基本情况
流入数据流:每学期5000
流出数据流:每学期5000
组成: {学号,姓名,性别,年龄,所在系,年级,{学习成绩},{体检结果},
{奖惩记录} … … }
数据量: 每年10000张
存取方式: 随机存取+按照专业系/班级打印

处理过程

具体处理逻辑一般用判定表或判定树来描述。数据字典中只需要描述处理过程的说明性信息

对处理过程的描述

处理过程描述={ 处理过程名, 说明, 输入:{数据流}, 输出:{数据流}, 处理:{简要说明} }

  • 简要说明:说明该处理过程的功能及处理要求
    • 功能:该处理过程用来做什么
    • 处理要求:
      处理频度要求,如单位时间里处理多少事务,多少数据量、响应时间要求等。
      处理要求是物理设计的输入及性能评价的标准

以处理过程“分配宿舍”为例,可如下描述:

1
2
3
4
5
6
7
8
9
sql复制代码处理过程:分配宿舍
说明:为所有新生分配学生宿舍
输入:学生,宿舍
输出:宿舍安排
处理:在新生报到后,为所有新生分配学生宿舍。
要求同一间宿舍只能安排同一年级同一性别的学生。
一个学生只能安排在一个宿舍中。
每个学生的居住面积不小于6平方米。
安排新生宿舍其处理时间应不超过15分钟。

参考

  • 主要参考自:数据库系统概论(高级篇)

本文转载自: 掘金

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

Eggjs 异常处理、中间件、jwt,实现接口权限控制 一

发表于 2021-08-07

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

一、自定义异常、异常处理中间件

在程序执行时会有各种各样的异常情况,当异常出现我们能从控制台看出异常的原因,但是对前端来说不够人性化,不能够清晰,有些情况要给调用端返回友好的消息提示,利用自定义异常和全局异常处理就能很简单的解决。

Egg 的 中间件 形式是基于洋葱圈模型。每次我们编写一个中间件,就相当于在洋葱外面包了一层。全局异常处理就是在洋葱模型中捕获到异常并处理,响应自定义异常信息。

image-20210806113247115

例如查询一条记录,会判断信息是否存在,如果不存在就会返回一个资源不存在的消息提示。会定义一个固定的消息格式(异常信息,返回数据,http状态码等)。

  1. 自定义异常

在 app 目录下创建 exception 目录,自定义一个HttpExctption。构造方法中初始化成员变量,在后面的异常处理中会用到。我们自定义的类继承Error之后就可以在控制器或者服务中抛出。

假设我们的响应格式是这样的:

1
2
3
4
5
json复制代码{
"code": xxx,
"msg": "xxx",
"data": xxx
}

按照响应定义异常基类,构造函数中接收参数,初始化对应上面响应格式的成员变量。

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码// app/exception/http.js
class HttpException extends Error {
constructor(code = 50000, message = '服务器异常', data = null, httpCode = 500) {
super();
this.code = code; // 自定义状态码
this.msg = message; // 自定义返回消息
this.data = data; // 自定义返回数据
this.httpCode = httpCode; // http状态码
}
}
module.exports = HttpException;

其他自定义业务异常全部继承 HttpExctption,列举一个 NotFoundException。

1
2
3
4
5
6
7
8
javascript复制代码// app/exception/not_found.js
const HttpException = require('./http');
class NotFoundException extends HttpException {
constructor(message = '资源不存在', errCode = 40004) {
super(errCode, message, null, 404);
}
}
module.exports = NotFoundException;
  1. 异常处理中间件

这个操作就相当于在我们程序的外面套了一层 try...catch ,当控制器中抛出异常,中间件中就能捕获到。通过判断异常的类型,例如自定义异常 err instanceof HttpException ,我们就可以根据自定义异常中的信息生成统一的返回信息。

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
javascript复制代码// app/middleware/error_handler.js
const HttpException = require('../exception/http');
module.exports = () => {
return async function errorHandler(ctx, next) {
const method = ctx.request.method;
// 当请求方法为OPTIONS,通常为axios做验证请求,直接响应httpStatus204 no content即可
if (method === 'OPTIONS') {
ctx.status = 204;
return;
}
try { // 在这里捕获程序中的异常
await next();
} catch (err) {
// 判断异常是不是自定义异常
if (err instanceof HttpException) {
ctx.status = err.httpCode;
ctx.body = {
code: err.code,
msg: err.msg,
data: err.data,
};
return;
}
// ... 其他异常处理,例如egg参数校验异常,可以在这里处理

// 最后其他异常统一处理
ctx.status = 500;
ctx.body = {
code: 50000,
msg: err.message || '服务器异常',
data: null,
};
}
};
};

中间件编写完成后,我们还需要手动挂载,在 config.default.js 中加入下面的配置就完成了中间件的开启和配置,数组顺序即为中间件的加载顺序。

1
2
3
4
5
6
7
8
9
10
javascript复制代码// config/config.default.js
module.exports = appInfo => {
const config = exports = {};
// ... 其他配置
// 配置需要的中间件,数组顺序即为中间件的加载顺序
config.middleware = [ 'errorHandler' ];
return {
...config,
};
};
  1. 统一数据响应格式

可以创建一个 BaseController 基类,在类中编写一个 success 方法 ,当控制器继承了 BaseController 就,能使用其中的 success 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码// app/controller/base.js
const Controller = require('egg').Controller;
class BaseController extends Controller {
success(data = null, message = 'success', code = 1) {
const { ctx } = this;
ctx.status = 200;
ctx.body = {
code,
message,
data,
};
}
}
module.exports = BaseController;
  1. 测试异常及中间件

  • 创建路由
1
2
3
4
5
javascript复制代码// app/router.js 路由
module.exports = app => {
const { router, controller } = app;
router.get('/admin/menus/:id', controller.adminMenu.readMenu);
};
  • 创建控制器
1
2
3
4
5
6
7
8
9
javascript复制代码// app/controller/admin_menu.js 控制器
const BaseController = require('./base');
class AdminMenuController extends BaseController {
async readMenu () {
const { id } = this.ctx.params;
const data = await this.service.adminMenu.findMenu(id);
this.success(data);
}
}
  • 创建服务,假设业务是查询某个菜单的详情,当菜单不存在返回给前端相应的消息。
1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// app/service/admin_menu.js 
const Service = require('egg').Service;
class AdminMenuService extends Service {
async findMenu (id) {
const menu = await this.app.model.AdminMenu.findOne({ where: { id }, attributes: { exclude: [ 'delete_time' ] } });
if (menu === null) { // 当数据不存在直接抛出异常
throw new NotFoundException('菜单不存在', 22000);
}
return menu;
}
}
module.exports = AdminMenuService;
  • 测试结果

GET请求url http://127.0.0.1:7001/admin/menus/1 查询到结果

image-20210721113824687

GET请求url http://127.0.0.1:7001/admin/menus/99 查询一个不存在的菜单,没有相应结果

image-20210721113920243

二、权限管理中间件

具体数据库分析及sql请查看文章:

RBAC前后端分离权限管理思路及数据表设计

整体思路:

  1. 当用户登录成功,颁发给用户一个 token ,除了部分公共接口外,其他所有接口都需要携带 token 才能访问。
  2. 当请求到达服务器,通过中间件拦截请求,判断该请求是否需要登录验证、是否需要权限管理、用户是否有权限访问。

image-20210727172910872

  1. 模型及数据结构

  • 用户表 模型

image-20210727164852348

  • 角色表

image-20210727165141521

  • 菜单表 模型

image-20210727165404877

  • 角色与用户绑定关系表 模型

image-20210806142723451

  • 角色与菜单绑定关系表 模型

image-20210806142809620

  1. jwt 使用

  • 安装 egg-jwt 扩展
1
shell复制代码npm i egg-jwt --save
  • 插件中增加 egg-jwt
1
2
3
4
5
6
7
8
javascript复制代码// config/plugin.js
module.exports = {
...
jwt: {
enable: true,
package: 'egg-jwt',
},
};
  • jwt 配置

expire token过期时间,单位秒。

secret token签名秘钥。

1
2
3
4
5
6
7
8
9
10
11
12
javascript复制代码// config/config.default.js
module.exports = appInfo => {
const config = exports = {};
// ... 其他配置
config.jwt = {
expire: 7200,
secret: 'b2ce49e4a541068d',
};
return {
...config,
};
};
  • token校验失败异常,当token校验失败抛出。
1
2
3
4
5
6
7
8
javascript复制代码// app/exception/auth.js
const HttpException = require('./http');
class AuthException extends HttpException {
constructor(message = '令牌无效', errorCode = 10001) {
super(errorCode, message, null, 401);
}
}
module.exports = AuthException;
  • JwtService

exp jwt的过期时间,这个过期时间必须要大于签发时间

nbf jwt的生效时间,定义在什么时间之前,该jwt都是不可用的

iat jwt的签发时间

jti jwt的唯一身份标识

uid 自定义 payload 存放 userId

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
javascript复制代码// app/service/jwt.js
const UUID = require('uuid').v4;
const Service = require('egg').Service;
const dayjs = require('dayjs');
class JwtService extends Service {
// 生成token
async createToken (userId) {
const now = dayjs().unix();
const config = this.app.config.jwt;
return this.app.jwt.sign({
jti: UUID(),
iat: now,
nbf: now,
exp: now + config.expire,
uid: userId,
}, config.secret);
}
// 验证token
async verifyToken (token) {
if (!token) { // 如果token不存在就抛出异常
throw new AuthException();
}
const secret = this.app.config.jwt.secret;
try {
await this.app.jwt.verify(token, secret);
} catch (e) { // 如果token验证失败直接抛出异常
// 通过消息判断token是否过期
if (e.message === 'jwt expired') {
throw new AuthException('令牌过期', 10003);
}
throw new AuthException();
}
return true;
}
// 通过token获取用户id
async getUserIdFromToken (token) {
await this.verifyToken(token);
// 解析token
const res = await this.app.jwt.decode(token);
return res.uid;
}
};
  1. 权限管理中间件

我们不需要中间件会处理每一次请求,这个中间件在app/router.js 中实例化和挂载。

在实例化好中间件调用的时候我们可以向中间件传递参数,参数就是路由名称,对应菜单表中api_route_name 。

只有定义路由的时候传递中间件的接口才会进行登录及权限校验。

1
2
3
4
5
6
7
8
javascript复制代码// app/router.js
module.exports = app => {
const { router, controller } = app;
// 实例化auth中间件
const auth = app.middleware.auth;
// 注册路由
router.get('/admin/menus/:id', auth('get@menus/:id'), controller.adminMenu.readMenu);
};

当进入到中间件之后,name 就是中间件参数。

  • 获取 header 中的 token ,再通过token 获取当前请求的 userId ,当token 不存在或者不正确会自
    动响应给前端。
  • 接下来检测权限,判断是否有路由名称,如果没有名称就通过验证(有些接口不需要权限验证,但需要登录验证)。
  • 在菜单表中查找是否存在这个菜单,如果不存在通过验证(有些接口不需要权限验证,但需要登录验证)。
  • 通过 userId 查询用户所有的角色,当用户存在角色id = 1的时候,也就是超级管理员拥有所有权限,通过验证。
  • 通过角色id和菜单id查询 角色菜单绑定关系表中是否存在,如果存在则通过。不存在则为无权限,抛出异常,异常处理中间件会捕获到响应。
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
javascript复制代码// app/middleware/auth.js
const AuthException = require('../exception/auth');
module.exports = name => { // 此处name为 auth(xxx) 的xxx
return async function auth(ctx, next) {
// 获取token
const token = ctx.request.headers.authorization;
// 通过token获取用户id
const userId = await ctx.service.jwt.getUserIdFromToken(token);
// 校验权限
await checkAuth(userId, ctx);
await next();
};
async function checkAuth (userId, ctx) {
if (!name) {
return true;
}
// 查询菜单是否存在
const menu = await ctx.model.AdminMenu.findOne({ where: { api_route_name: name } });
if (menu === null) {
return true;
}
// 查询用户绑定的角色
const roles = await ctx.model.AdminRoleUser.findAll({ attributes: [ 'role_id' ], where: { user_id: userId } });
const roleIds = roles.map(item => item.role_id);
if (roleIds.includes(1)) {
return true;
}
const Op = ctx.app.Sequelize.Op;
// 查询用户是否有菜单的权限
const hasAccess = await ctx.model.AdminRoleMenu.findOne({ where: { role_id: { [Op.in]: roleIds }, menu_id: menu.id } });
if (hasAccess === null) {
throw new AuthException('权限不足', 10002);
}
}
};

4.测试

  • 有权限

image-20210806161814400

image-20210806162417630

image-20210806161708427

image-20210806162130773

  • 无权限

删除role_menu中id为228的记录

image-20210806162513680

再次请求结果:

image-20210806162538866

完整代码

完整项目参考

Egg.js 后台管理接口

ThinkPHP后台管理接口

本文转载自: 掘金

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

如何在 Windows 搭建 PostgreSQL 数据库环

发表于 2021-08-07

这是我参与 8 月更文挑战的第 7 天,活动详情查看:8 月更文挑战

前言

之前由于学习,所以选择的是受众范围较广的 MySQL。至于 MySQL,在这里就不用说了,想必大家都是很熟悉的。正式参加工作之后,才发现原来不同的公司选择的数据库都不一样,有的选择 Oracle,有的选择 MySQL,而有的则选择 PostgreSQL。Oracle 的确很厉害,不过始终是面向收费的,一般小体量的公司用起来成本太高,所以大家还是更倾向于 MySQL。关于 MySQL 的教程、知识分享博客有很多,而且大家写的也很详细,所以在这里就不在赘述了。我们今天就主要来聊聊另一款数据库 PostgreSQL,揭开他的神秘面纱,一探究竟。

PostgreSQL 的安装

什么是 PostgreSQL

准备工作

经过上面的介绍之后,现在我们就来看看如何安装 PostgreSQL。

在正式安装过程之前,我们先需要准备好安装包,这里我主要是以 Windows 平台为例,理论上来讲普遍适用于全系 Windows 系统。

那么我们首先要做的就是去 PostgreSQL 官网:www.postgresql.org/ 去下载最新版本 PostgreSQL 安装包。

然后选择对应平台进入下载安装包,这里提供 .exe 的安装包,也支持下载 .zip 的压缩包形式解压缩安装,这里根据自己的喜好进行下载即可,下边我主要以 .exe 形式的安装包安装为例。

安装过程

准备工作好了之后,接下来就是正式安装过程了。

  1. 双击我们下载好的安装包之后,进入安装界面,首先是一个欢迎界面,直接 Next 下一步即可;

  1. 然后是选择安装路径,这里默认是在 C:\Program Files\PostgreSQL\13 下,不过我们一般推荐安装到自己平常安装软件的地方,不要直接安装在 C 盘;

  1. 选择所要安装的组件,这里默认是全部安装,如果你们有特殊的要求,那就默认直接点击 Next 下一步即可;

  1. 选择数据数据存放的地方,这里默认实在安装路径下的 data 子目录下,如果你之前在第 2 步中自定义了安装路径,那么这里直接选择 Next 下一步即可;

  1. 设置密码,也就是待会儿我们安装结束后登陆时所需的密码,这里自己设置即可(一定要记住!);

  1. 端口设置,PostgreSQL 默认端口是 5432,如果你不想使用默认端口,可以自定义想要使用的端口,这里如果改动了也要记住,确保不要和其他服务冲突;

  1. 其实就是时区选择,这里直接默认点击 Next 下一步即可;

  1. 然后是确认信息,确保我们已经准备好了要开始 PostgreSQL 的安装,直接点击 Next 下一步即可;

  1. 接下来就是相对漫长的安装过程了,这里等待即可;

  1. 好了,显示此界面就说明我们的安装过程结束了,点击 Finish 完成安装即可。

验证

既然我们的 PostgreSQL 安装好了,那么接下来就是看看如何使用它了。

去我们的程序列表中找到 SQL Shell(psql),也就是下图中的程序打开(这里我因为安装了快捷搜索软件,所以你查找的界面会和我不一样,但是软件是一个)。

然后就是登陆过程了,首先是 Server,也就是说数据库 url,默认是在本地(所以是 localhost),没有该动的就直接回车下一步好了。

接着是 Database,也即我们数据库,这里默认是使用 postgres,因为我们是第一次登陆,所以这里也就直接回车下一步就好了。

再接着是 Port,也就是端口号,默认是 5432,如果你安装的时候改动了,那么此时你最好也改成你当时改的端口,否则可能导致连接失败。

然后是 Username,也就是 PostgreSQL 的用户,这里一般默认是超级用户(postgres,这里不同于 MySQL 的 root,要注意),而我们也是第一次登陆,没有建立新账户,直接默认回车下一步即可。

最后要输入的则是口令,也就是登陆数据库的密码,这里我们已经在上边设置过了,直接输入后回车即可。

如果我们登陆成功,那么就会出现下面图中的提示了。

总结

OK,今天的文章到此就结束了,主要介绍了如何在 Windows 中安装 PostgreSQL,以及 PostgreSQL 的一些简介和如何验证安装是否成功。

原创不易,如果你觉得本文对你有所帮助,那就来个点赞关注吧。

本文转载自: 掘金

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

RBAC、控制权限设计、权限表设计 基于角色权限控制和基于资

发表于 2021-08-07

简单介绍一下权限表设计,也是自己去了解的一个东西。

大家一起加油哦😀😀

一、介绍

现阶段我们知道的大概就是两种权限设计

  1. 一种是基于角色的权限设计
  2. 另一种是基于资源的权限设计

接下来我给大家讲一讲这两种权限的区别,以及那种更好。

在后面也会给出数据库里表的设计的具体代码。

二、基于角色的权限设计

RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权。例如:

比如:主体的角色为总经理可以查 询企业运营报表,查询员工工资信息等,访问控制流程如下:

<img src="security.assets/image-20210415192816477.png" alt="image-20210415192816477" style="zoom:67%;" />

根据上图中的判断逻辑,授权代码可表示如下:

1
2
3
java复制代码if(主体.hasRole("总经理角色id")){
查询工资
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:

1
2
3
java复制代码if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

我们敲代码都知道的 公司中最忌修改源码 因为牵一发而动全身。

所以不是非常必要 就不要随便修改原来的代码。

接下来 我们看一下基于资源的权限控制的设计是什么样子吧。

三、基于资源的权限设计

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须 具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7SAS17gC-1618488195476)(security.assets/image-20210415193430294.png)]

根据上图中的判断,授权代码可以表示为:

1
2
3
java复制代码if(主体.hasPermission("查询工资权限标识")){	
查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改 授权代码,系统可扩展性强。

四、主体、资源、权限关系图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jIHBhnf2-1618488195480)(security.assets/image-20210415193608243.png)]

主体、资源、权限相关的数据模型

主体(用户id、账号、密码、…)

主体(用户)和角色关系(用户id、角色id、…)

角色(角色id、角色名称、…)

角色和权限关系(角色id、权限id、…)

权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)

数据模型关系图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8ODebSE0-1618488195482)(security.assets/image-20210415193819222.png)]

具体表模型SQL:

user表:

1
2
3
4
5
6
7
8
9
10
11
sql复制代码DROP TABLE IF EXISTS `user_db`;
CREATE TABLE `user_db` (
`id` bigint(20) NOT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`fullname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

INSERT INTO `user_db` VALUES (1, 'admin', '123456', '张三', '123');

t_role表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql复制代码DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
`status` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_role_name`(`role_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES ('1', '管理员', NULL, NULL, NULL, '');

t_user_role表:

1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`user_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`creator` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES ('1', '1', NULL, NULL);

t_permission表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '权限标识符',
`description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`url` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_permission
-- ----------------------------
INSERT INTO `t_permission` VALUES ('1', 'p1', '测试资源\r\n1', '/r/r1');
INSERT INTO `t_permission` VALUES ('2', 'p2', '测试资源2', '/r/r2');

t_role_permission表:

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`role_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`permission_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_role_permission
-- ----------------------------
INSERT INTO `t_role_permission` VALUES ('1', '1');
INSERT INTO `t_role_permission` VALUES ('2', '2');

自言自语

今天又完成一篇 ✌

看完这一篇 大家应该也会对权限的设计有了一些浅浅的理解吧。

一起加油哦。

本文转载自: 掘金

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

SpringBoot最全笔记,企业最核心的技术你确定不来看看

发表于 2021-08-07

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

一、SpringBoot入门

1.1、SpringBoot介绍

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。
SpringBoot能够快发开发的原因是因为配置文件从xml转移到了java文件中,减少了配置文件的书写。

1.2、JavaConfig

JavaConfig的技术是用于替代目前繁琐的xml文件的配置,他最为重要的替代方式就是使用了大量的注解。

1.2.1、项目准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
xml复制代码<properties>
<spring.version>5.0.8.RELEASE</spring.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>

1.2.2、配置类替代配置文件之控制反转

1.2.2.1、创建Bean

1
2
java复制代码public class OneBean {
}

1.2.2.2、创建配置类

1
2
3
4
5
6
7
8
9
10
java复制代码// 配置类注解,贴上表明这个类是一个配置类
@Configuration
public class AppConfig {
// Bean实例注解,贴有该注解的方法为实例方法,在功能上等价于:<bean name="someBean" class="cn.linstudy.onfig.OmeBean" ></bean>
@Bean
public OneBean oneBean(){
// 注意:实例方法的返回对象会交由Spring容器管理起来
return new SomeBean();
}
}

1.2.2.3、测试

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RunWith(SpringRunner.class)
// 引用配置类的注解
@ContextConfiguration(classes = AppConfig.class)
public class App {
@Autowired
private ApplicationContext ctx;
@Test
public void testApp(){
OmeBean omeBean = ctx.getBean("omeBean", SomeBean.class);
System.out.println(omeBean);
}
}

1.2.3、配置类替代配置文件之组件扫描

1.2.3.1、Bean组件扫描

学习spring框架时, Spring有4个版型标签(代表的含义相同,仅仅只是为了标注表示的对象不同)。当spring容器扫描器扫描到贴有版型标签的类,会自动创建这些类的实例对象,并交给容器管理。
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Controller  //标记控制层
public class EmployeeController{
}
@Service //标记服务层
public class EmployeeServiceImpl{
}
@Repository //标记持久层
public class EmployeeDAOImpl{
}
@Component //其他类
public class EmployeeListener{
}
我们除了使用上述注解,还需要在xml文件中进行配置。
1
xml复制代码<context:component-scan base-package="指定扫描的包路径"></context:component-scan>
换成了JavaConfig,我们需要贴注解来告诉Spring需要扫描这个类并且创建对象。
1
2
3
java复制代码@Component  //版型标签
public class OmeBean {
}
spring组件扫描注解, 扫描basePackages属性指定包及其子包下所有的贴有版型标签类,并创建对象交给Spring容器管理。**如果不指定basePackages属性,表示扫描当前类所有包及其子包。**
1
2
3
4
5
java复制代码@Configuration
//组件扫描标签
@ComponentScan(basePackages ="cn.linstudy.config")
public class AppConfig {
}

1.2.4、@Bean注解详解

1.2.4.1、创建一个OneBean类

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Setter
@Getter
public class OneBean {
public OneBean() {
System.out.println("OneBean被创建");
}
public void init() {
System.out.println("OneBean被初始化");
}
public void destroy() {
System.out.println("OneBean被销毁");
}
}

1.2.4.2、书写配置类

1
2
3
4
5
java复制代码@Scope("singleton")
@Bean(value = "ob",initMethod = "init", destroyMethod = "destroy")
public OomeBean oomeBean(){
return new OomeBean();
}

1.2.4.3、总结

  1. bean标签中的name属性 = @Bean注解中的name/value属性。
  2. bena标签中的id属性 = 实例方法的方法名。
  3. bean标签中的init-method = @Bean注解中的initMethod属性。
  4. bean标签中destroy-method属性 = @Bean注解中destroyMethod属性。
  5. bean标签中scope属性 = 实例方法中的@Scope注解

1.2.5、配置类替代配置文件之依赖注入

1.2.5.1、创建新类

1
2
java复制代码public class TwoBean {
}
1
2
3
4
5
java复制代码@Setter
@Getter
public class OneBean {
private Two twoBean;
}

1.2.5.1、实现方式一

我们可以直接调用实例方法来实现依赖注入。


JavaConfig实现传统依赖注意需要注意:
  1. omeBean对象可以从容器中获取。
  2. twoBean可以从容器中获取。
  3. omeBean对象通过getTwoBean()方法获得的bean应该从容器获取的twoBean对象相等。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class AppConfig {
@Bean
public OmeBean omeBean(){
OmeBean omeBean = new OmeBean();
OmeBean.setTwoBean(twoBean());
return someBean;
}
@Bean
public TwoBean twoBean(){
return new TwoBean();
}
}
需要注意的点:
  1. 多次调用twoBean()方法, spring容器只会执行一次twoBean对象的构建,原因:twoBean()方法被spring容器代理了,每次调用前都会执行容器bean检查,当发现容器中已经有了,直接从容器中拿。如果没有,则执行方法,并将返回值放置到容器中。

1.2.5.2、实现方式二

方式二是可以通过注入实例对象的方式来实现依赖注入。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class AppConfig {
@Bean
public OneBean omeBean(TwoBean twoBean){
OneBean omeBean = new OneBean();
omeBean.setTwoBean(twoBean);
return omeBean;
}
@Bean
public TwoBean twoBean(){
return new TwoBean();
}
}

1.2.6、配置文件互相导入

1.2.6.1、@import

配置类导入注解,贴在配置类上,作用等价于:<import resource="xxx配置.xml"></import>标签该标签用于配置类与配置类间的导入。

1.2.6.2、@ImportResource

配置文件导入注解,贴在配置类上,作用等价于:<import resource="xxx配置.xml"></import>标签该标签用于配置类与配置文件间的导入。

1.2.7、配置文件的加载与取值

1.2.7.1、加载

我们可以使用@PropertySource这个注解来进行资源配置文件加载注解,贴在配置类上,用于将properties类型文件加载到spring容器中。
1
xml复制代码<context:property-placeholder location="classpath:xxx.perperties"/>

1.2.7.2、取值

当加载了一个配置文件的时候,如果我们需要取值,可以使用@Value注解从properties配置中获取配置的数据。
1.2.7.2.1、创建db.properties
1
2
3
4
properties复制代码jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://192.168.126.129:3306/car_crm
jdbc.username=root
jdbc.password=123456
1.2.7.2.2、定义一个类
1
2
3
4
5
6
7
8
9
java复制代码@Setter
@Getter
@ToString
public class MyDataSource {
private String driverClassName;
private String url;
private String username;
private String password;
}
1.2.7.2.3、接收值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@PropertySource("classpath:db.properties")
@Configuration
public class AppConfig {


@Value("${jdbc.driverClassName}")
private String driverClassName;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

@Bean
public MyDataSource myDataSource(){
MyDataSource source = new MyDataSource();
source.setDriverClassName(driverClassName);
source.setUrl(url);
source.setUsername(username);
source.setPassword(password);
return source;
}
}

1.3、SpringBoot自动装配原理

1.3.1、SpringBoot的核心注解

SpringBoot的核心是@SpringBootApplication这个注解。
1
2
3
4
5
6
7
8
9
10
java复制代码@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

1.3.2、SpringBoot自动装配原理。

1.3.2.1、综述

**SpringBoot启动会加载大量的自动配置类。**`@SpringBootApplication`包含以下三个注解:
  1. @SpringBootConfiguration:我们点进去以后可以发现底层是Configuration注解,说白了就是支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)。
  2. @EnableAutoConfiguration:开启自动配置功能。
  3. @ComponentScan:这个就是扫描注解,默认是扫描当前类下的package。将@Controller/@Service/@Component/@Repository等注解加载到IOC容器中。
我们进入到@EnableAutoConfiguration,就会发现他导入了@implort注解导入了AutoConfigurationImportSelector配置类,该类有一个getCandidateConfiguration方法获取候选的配置方法,可以读取依赖中META-INF/spring.factories中各种候选的配置类,根据你引入的依赖作为条件引入预先设置好的各种配置。

1.3.2.2、AutoConfigurationImportSelector

`@EnableAutoConfiguration`注解导入了一个`AutoConfigurationImportSelector`自动配置类选择器,该类可以实现配置类批量载入spring容器。其核心方法是`getCandidateConfigurations`目的是在于候选的配置。
1
2
3
4
5
6
7
8
9
java复制代码protected List<String> getCandidateConfigurations(
AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

1.3.2.3、SpringFactoriesLoader

`getCandidateConfigurations`方法作用是委托`SpringFactoriesLoader`去读取jar包中的`META-INF/spring.factories`文件, 并加载里面配置的自动配置对象。这是自动装配的核心。
写启动器需要遵循SpringBoot的规范,都需要编写META-INF/spring.factories,里面指定启动器的自动配置类,告诉SpringBoot需要提前放置哪些配置,等满足条件就直接加载。

image-20210328214450062

image-20210328214516160

1.2.3.4、RedisAutoConfiguration分析

我们以`RedisAutoConfiguration`配置类分析。

image-20210328215226181
我们点进去看源码,可以发现有四个注解:

  1. @Configuration:表示这个类是一个配置类。
  2. @ConditionalOnWebApplication(type = Type.SERVLET):表示在满足项目的类是是Type.SERVLET类型。
  3. @ConditionalOnMissingBean({RepositoryRestMvcConfiguration.class}):表示如果环境中没有RepositoryRestMvcConfiguration这个Bean对象才生效。这个就是自定义配置的入口。
  4. @ConditionalOnClass({RepositoryRestMvcConfiguration.class}):表示满足容器中存在RepositoryRestMvcConfiguration这个Bean对象的时候才会生效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnMissingBean({RepositoryRestMvcConfiguration.class})
@ConditionalOnClass({RepositoryRestMvcConfiguration.class})
@AutoConfigureAfter({HttpMessageConvertersAutoConfiguration.class, JacksonAutoConfiguration.class})
@EnableConfigurationProperties({RepositoryRestProperties.class})
@Import({RepositoryRestMvcConfiguration.class})
public class RepositoryRestMvcAutoConfiguration {
public RepositoryRestMvcAutoConfiguration() {
}

@Bean
public SpringBootRepositoryRestConfigurer springBootRepositoryRestConfigurer() {
return new SpringBootRepositoryRestConfigurer();
}
}

1.4、注意

1.4.1、为什么打成war包不是jar包

SpringBoot的默认打包方式是jar包。

在以前的开发中,Tomcat猫和web项目是独立的,必须满足一定的规则,Tomcat猫才可以部署war包。但是SpringBoot的项目是内嵌Tomcat,部署和运行一气呵成,所以就打成jar包方便。

1.4.2、pom.xml文件中的spring-boot-starter的作用

我们在创建SpringBoot项目的时候,可以发现引入了很多的start,他收集了市面上常用的jar包以及各种依赖,**并且对这些依赖进行了版本管理,避免了很多的版本冲突问题**,大大简化了我们的开发,在后续的项目中我们如果需要引入某个依赖,只需引入他的start即可,依赖的版本无需引入。列举额一些SpringBoot的常用的启动器:
1
2
3
4
5
6
7
8
9
10
11
12
13
markdown复制代码spring-boot-starter: 核心启动器 , 提供了自动配置,日志和YAML配置支持

spring-boot-starter-aop: 支持使用 `Spring AOP` 和 `AspectJ` 进行切面编程。

spring-boot-starter-freemarker: 支持使用 `FreeMarker` 视图构建Web 应用

spring-boot-starter-test: 支持使用 `JUnit`, 测试 `Spring Boot` 应用

spring-boot-starter-web: 支持使用 `Spring MVC` 构建 Web 应用,包括 `RESTful` 应用,使用 `Tomcat` 作为默认的嵌入式容器。

spring-boot-starter-actuator: 支持使用 Spring Boot Actuator 提供生产级别的应用程序监控和管理功能。

spring-boot-starter-logging: 提供了对日志的支持 , 默认使用Logback

1.4.3、mave中强大的功能——继承

继承是 Maven 中很强大的一种功能,继承可以使得子POM可以获得 parent 中的部分配置(groupId,version,dependencies,build,dependencyManagement等),可以对子pom进行统一的配置和依赖管理。

1.4.4、DepencyManagement & dependencies区别

在SpringBoot的pom文件中,dependencies是放在DepencyManagement中的,那么这两者的区别何在:
  1. dependencies:即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项(全部继承)。
  2. dependencyManagement:里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本(部分继承)。

1.4.5、SpringBoot在没有Tomcat的情况下如何启动

springboot使用嵌入式tomcat,编程实现,默认端口是8080,可以在application.properties中使用server.port进行设置。

image-20210328220125461

1.4.6、SpringBoot的启动类的main方法中SpringApplication.run(..)详解

启动类中的主方法有四个作用:
  1. 启动SpringBoot程序。
  2. 加载自定义的配置类,完成自动装配。
  3. 将当前项目部署到内嵌的Tomcat中。
  4. 启动Tomcat运行项目。

二、SpringBoot配置文件语法

2.1、SpringBoot配置文件概述

当我们使用spring 初始化器构建完一个Spring Boot项目后,只需引入一个web启动器的依赖,它就变成一个web项目了,而我们什么都没有配置就能通过localhost:8080进行访问了,这是因为Spring Boot在底层已经把配置信息都给我们自动配置好了。
那我们怎么去修改默认配置信息?在使用Spring 初始化器创建一个Springboot项目的时候会在resources目录下自动生成一个文件 application.properties,这是一个空文件,它的作用是提供我们修改默认配置信息的入口。
Spring Boot还提供给我们另外一种风格的配置文件 application.yml,虽然是两个不同的文件但是本质是一样的,区别只是其中的语法略微不同。

2.2、Properties语法

application.properties 配置文件比较简单,他不需要空格进行区分,父属性和子属性之间是以.进行区分的。

1
2
properties复制代码# key = value
server.port=8080

2.3、YML

他是一种全新的语法格式,虽然书写略微繁琐但是他确实SpringBoot官方推荐的配置文件的格式,因为他可以存储比roperties配置文件更复杂的类型。他的语法特点:
  1. 大小写敏感。
  2. k:(空格)v:表示一对键值对(空格必须有),以空格的缩进来控制层级关系。
  3. 只要是左对齐的一列数据,则表示都是同一个层级的。
  4. “#” 表示注释,从这个字符一直到行尾,都会被解析器忽略。
1
2
yaml复制代码server:
port: 8080

三、SpringBoot整合

SpringBoot最重要是整合各种框架,包括我们熟悉的Mybatis、Shiro等。

3.1、连接数据库

3.1.1、引入依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--springboot整合jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

3.1.2、Hikari数据源

3.1.2.1、概述

在springboot2.0之后 , 采用的默认连接池就是**Hikari**, 号称"史上最快的连接池", 所以我们没有添加依赖也能直接用, springboot的自动配置中含有DataSourceAutoConfiguration配置类, 会先检查容器中是否已经有连接池对象, 没有则会使用默认的连接池, 并根据特定的属性来自动配置连接池对象, 用到的属性值来源于DataSourceProperties对象。

3.1.2.2、修改application.properties

1
2
3
4
properties复制代码spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///ssm_carbusiness?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

3.1.3、Druid数据源

3.1.3.1、概述

虽然SpringBoot官方推荐的是Hikari数据源,但是如果我们想使用Druid数据源的话,也很简单。只需要添加依赖即可, 此时加的是**Druid**的springboot自动配置包, 里面包含了`DruidDataSourceAutoConfigure`自动配置类,会自动创建druid的连接池对象, 所以springboot发现已经有连接池对象了,则不会再使用**Hikari**。

3.1.3.2、引入依赖

1
2
3
4
5
6
xml复制代码<!-- druid数据源依赖,记住一定要引带start的 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>

3.1.3.3、修改application.properties

1
2
3
4
properties复制代码spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql:///ssm_carbusiness?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.druid.username=root
spring.datasource.druid.password=123456

3.1.3.4、注意

如果我们在写代码的时候不小心引错了包,引入了普通的依赖(不带start的普通依赖,只有Druid自身的依赖),并不是自动的配置包。那么SpringBoot就不会去进行解析还是需要我们去手动配置才可以生效。我们只需在application.properties中添加一行配置即可。
1
properties复制代码spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

3.2、集成MyBatis

3.2.1、引入依赖

1
2
3
4
5
6
xml复制代码<!--mybatis集成到SpringBoot中的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>

3.2.2、配置接口扫描

在传统的SSM项目中,我们可以在配置文件中告诉Spring我的Mapper接口的位置,从而可以创建Mapper接口实现类的代理对象,在SpringBoot中没有了这个配置文件,那么我们只需在SpringBoot的启动类中添加一行配置即可。
1
2
3
4
5
6
7
8
java复制代码@SpringBootApplication
// 添加这一行配置,告诉SpringBoot我的Mapper接口的位置在哪里
@MapperScan("cn.linstudy.mapper")
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}

3.2.3、配置属性

在以前我们需要在application.xml中配置一些属性,从而更好地使用MyBatis,比如说mapper.xml文件的位置、是否开启懒加载、以及别名等等信息,现在这些信息都要在application.properties中进行配置,如果业务中无需使用,也可以不需要配置。
1
2
3
4
5
6
7
8
9
10
properties复制代码# 是否1开启懒加载
mybatis.configuration.lazy-loading-enabled=true
# 开启懒加载的方法
mybatis.configuration.lazy-load-trigger-methods=clone
# mapper.xml文件的配置
mybatis.mapper-locations=classpath:cn/wolfcode/*/mapper/*Mapper.xml
# 配置别名
mybatis.type-aliases-package=cn.wolfcode.sb.domain
#打印SQL日志
logging.level.cn.linstudy.mapper=trace

3.3、事务管理

3.3.1、引入依赖

1
2
3
4
5
xml复制代码<!-- 支持使用 Spring AOP 和 AspectJ 进行切面编程。 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3.3.2、XML方式配置事务

采取配置类和XML混用的策略, 在配置类上使用@ImportResource("classpath:spring-tx.xml"),我们在这个xml文件中书写事务、切面、切入点表达式。**不推荐使用**。

3.3.3、注解方式配置事务

SpringBoot的自动配置中提供了TransactionAutoConfiguration事务注解自动配置类,我们在引入依赖后,直接在**业务层实现类上或者其方法**上直接贴`@Transactional`注解即可,推荐使用。切记在使用之前一定要看看数据库的引擎是否支持事务。
SpringBoot默认使用的是CGLIB的动态代理,如果想要使用JDK的动态代理,那么仅需在application.properties中写一行配置即可。
1
2
properties复制代码#优先使用JDK代理
spring.aop.proxy-target-class=false

3.4、SpringBoot中静态资源的处理

3.4.1、静态资源的位置

SpringBoot的标准目录和1传统的SSM的maven目录是不一样的,他在resources下还有两个目录专门来存放静态资源:

  1. static:用来存放CSS、JS等样式。
  2. templates:用来存放页面模板。

image-20210329170000484
SpringBoot对于静态文件的处理也是有规则的:

  1. 默认情况下,Springboot会从classpath下的 /static 、 /public 、/resources 、 /META-INF/resources中四个位置下加载静态资源。

image-20210329170927894

  1. 可以在application.properties中配置spring.resources.staticLocations属性来修改静态资源加载地址。
  2. 因为SpringBoot默认对static下静态资源的映射路径为/,所以我们引入的时候无需写static。

3.4.2、路径映射配置

在SpringBoot自动装配中,`WebMvcAutoConfiguration`的自动配置类导入了`DispatcherServletAutoConfiguration`的配置对象,会自动创建`DispatcherServlet`前端控制器,默认的`< url-pattern >` 是 `/`。

3.5、统一异常处理

3.5.1、SpringBoot默认方式

SpringBoot默认情况下,会把所有错误都交给`BasicErrorController`类完成处理,错误的视图导向到 `classpath:/static/error/` 和 `classpath:/templates/error/**` 路径上,http状态码就是默认视图的名称,如果出现了404错误,那么对应的模板为404.html。
如果我们想自己写一个错误页面,那么我们只需在默认的路径下创建一个同名的模板文件即可。

3.5.2、控制器增强的方式

自己定义一个控制器增强器,专门用于统一异常处理,该方式一般用于5xx类错误
1
2
3
4
5
6
7
java复制代码@ControllerAdvice //控制器增强器
public class ExceptionControllerAdvice {
@ExceptionHandler(RuntimeException.class) //处理什么类型的异常
public String handlException(RuntimeException e, Model model) {
return "errorView"; //错误页面视图名称
}
}

3.6、过滤器

3.6.1、过滤器概述

过滤器是基于Servlet 技术实现的, 简单的来说,过滤器就是起到过滤的作用,在web项目开发中帮我们过滤一些指定的 url做一些特殊的处理,他主要的功能如下:
  1. 过滤掉一些不需要的东西,例如一些错误的请求。
  2. 可以修改请求和相应的内容。
  3. 可以拿来过滤未登录用户。

3.6.2、过滤器实现方式

主要有两种实现方式:
  1. 第一种是使用@WebFilter。
  2. 第二种是使用 FilterRegistrationBean。

3.6.3、@WebFilter

3.6.3.1、概述

@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配置将相应的类部署为过滤器。
属性名 类型 描述
filterName String 指定该Filter的名称
urlPatterns String String
value String 与 urlPatterns 一致

3.6.3.2、代码实现

创建一个MyFilter.java实现Filter接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = "/api/*",filterName = "myFilter")
@Order(1)//指定过滤器的执行顺序,值越大越靠后执行
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("初始化过滤器");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request= (HttpServletRequest) servletRequest;
String uri=request.getRequestURI();
String method=request.getMethod();
System.out.println(uri+" "+method+"哈哈我进入了 MyFilter 过滤器了");
filterChain.doFilter(servletRequest,servletResponse);
}
}

启动类加上 @ServletComponentScan 注解

创建一个 FilterController 接口

1
2
3
4
5
6
7
8
9
10
11
java复制代码import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/user/filter")
public String hello(){
return "哈哈我通过了过滤器";
}
}

3.6.4、FilterRegistrationBean 实现

创建 FilterConfig

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
java复制代码import com.yingxue.lesson.filter.MyFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfig {
@Bean
public MyFilter myFilter(){
return new MyFilter();
}
@Bean
public FilterRegistrationBean getFilterRegistrationBean(MyFilter myFilter){
FilterRegistrationBean filterRegistrationBean=new FilterRegistrationBean();
/**
* 设置过滤器
*/
filterRegistrationBean.setFilter(MyFilter());
/**
* 拦截路径
*/
filterRegistrationBean.addUrlPatterns("/api/*");
/**
* 设置名称
*/
filterRegistrationBean.setName("myFilter");
/**
* 设置访问优先级 值越小越高
*/
filterRegistrationBean.setOrder(1);
return filterRegistrationBean;
}
}

修改 MyFilter.java

1
java复制代码//@WebFilter(urlPatterns ={"/api/*"},filterName = "myFilter")

修改启动类

1
java复制代码//@ServletComponentScan

3.7、拦截器

3.7.1、概述

简单的来说,就是一道阀门,在某个方法被访问之前,进行拦截,然后在之前或之后加入某些操作,拦截器是AOP 的一种实现策略,他的主要功能是对正在运行的流程进行干预。

3.7.2、拦截器方法概述

拦截器也主要有三个方法:
  1. preHandle是在请求之前就进行调用,如果该请求需要被拦截,则返回false,否则true。
  2. postHandle是在请求之后进行调用,无返回值。
  3. afterCompletion是在请求结束的时候进行调用,无返回值。

3.7.3、代码实现

创建拦截器类并实现 HandlerInterceptor 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class MyInterceptor implements HandlerInterceptor {
@Value("${open.url}")
private String openUrl;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor....在请求处理之前进行调用(Controller方法调用之前)");
String requestUrl=request.getRequestURI();
System.out.println("过滤器MyFilter拦截了请求为"+requestUrl);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor...请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor....在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)");
}
}

修改 application.properties

我们需要在application.properties中添加一行代码加入开发接口通配地址,表示放行的代码
1
2
xml复制代码#凡是请求地址层级带有 open 都放行
open.url=/**/open/**

创建一个 Java 实现 WebMvcConfigurer,并重写 addInterceptors 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
public class WebAppConfig implements WebMvcConfigurer {
@Value("${open.url}")
private String openUrl;
@Bean
public MyInterceptor getMyInterceptor(){
return new MyInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getMyInterceptor()).addPathPatterns("/api/**").excludePathPatterns(openUrl);
}
}

3.7.4、测试

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@RestController
@RequestMapping("/api")
public class InterceptorController {
@GetMapping("/home/open/info")
public String home(){
return "欢迎来到首页";
}
@GetMapping("/user/interceptor")
public String interceptor(){
return "我被拦截了并通过了拦截器";
}
}

3.8、日志

日志对于我们的系统测试和调试时有着很重要的地位,我们在系统开发的时候,以前时经常使用`System.out.print()`这句话来输出一些系统的信息,但是在实际的工作中却不会用到这种方法来输出日志,原因大概有以下几点:
  1. 比起System.out.println,日志框架更为灵活,可以把日志的输出和代码分离。
  2. 日志框架可以方便的定义日志的输出环境,控制台,文件,数据库。
  3. 日志框架可以方便的定义日志的输出格式和输出级别。

3.8.1、日志介绍

3.8.1.1、SpringBoot中的日志介绍

我们在SpringBoot启动的时候就可以看到时默认开启了日志的。从左往右分别为:**时间** 、**日志级别** **线程ID** 、 **线程名称**、 **日志类**、 **日志说明**。

image-20210331185311118
日志级别,级别越高,输出的内容越少, 如果设置的级别为info, 则debug以及trace级别的都无法显示trace < debug < info < warn < error
Springboot默认选择Logback作为日志框架,也能选择其他日志框架,但是没有必要

20180503123846653

3.8.1.2、输出日志的两种方式

在类中定义一个静态Logger对象

1
2
java复制代码// 这里传入当前类的作用是方便输出日志时可以清晰地看到该日志信息是属于哪个类的
private static final Logger log = LoggerFactory.getLogger(当前类.class);

使用lombok提供的@Slf4j注解

1
2
3
4
5
6
7
8
9
java复制代码@Slf4j
@Service
public class PermissionServiceImpl implements IPermissionService {}
//输出日志中有变量可以使用{}作为占位符
log.info("删除id为{}的数据", id);
log.debug("权限插入成功:{}",expression);
log.info("权限插入成功:{}",expression);
log.warn("权限插入成功:{}",expression);
}

3.8.1.3、Logback配置文件的使用

Logback框架默认会自动加载classpath:logback.xml,作为框架的配置文件。在SpringBoot中使用时,还会额外的支持自动加载classpath:logback-spring.xml。所以推荐使用logback-spring.xml,功能更强大些。
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:开启日志框架的热部署,默认值true表示开启
scanPeriod:热部署的频率,默认值60 second
debug:设置输出框架内部的日志,默认值false
-->
<configuration scan="true" scanPeriod="60 second" debug="false">
<property name="appName" value="springboot demo" />
<contextName>${appName}</contextName>

<!-- appender:日志输出对象,配置不同的类拥有不同的功能
ch.qos.logback.core.ConsoleAppender:日志输出到控制台
-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd-HH:mm:ss} %level [%thread]-%logger{35} >> %msg %n</pattern>
     </encoder>
   </appender>

<!-- ch.qos.logback.core.FileAppender:日志输出到文件中
<appender name="fileAppender" class="ch.qos.logback.core.FileAppender">
<encoder>
<pattern>%-4relative [%thread] %level %logger{35} - %msg %n</pattern>
   </encoder>
<append>true</append>
<file>mylog.log</file>
   </appender>
-->

<!-- root是项目通用的logger,一般情况下都是使用root配置的日志输出
level:按照级别输出日志,日志级别,级别越高,输出的内容越少
trace < debug < info < warn < error
-->
   <root level="info">
<appender-ref ref="STDOUT" />
</root>

<!-- 自定义的logger,用于专门输出特定包中打印的日志
<logger name="cn.wolfcode.crm.mapper" level="trace">
</logger>
-->
</configuration>

3.9、集成前端

3.9.1、集成JSP

提起 Java 不得不说的一个开发场景就是 Web 开发,说到 Web 开发绕不开的一个技术就是 JSP,SpringBoot官方虽然已经不推荐使用JSP了,但是集成JSP还是很重要的。

3.9.1.1、引入依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<!--JSP标准标签库-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<!--内置tocat对Jsp支持的依赖,用于编译Jsp-->
<dependencys>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

3.9.1.2、编辑 project Structure

image-20210331191755605

3.9.1.3、Spring Mvc 视图解析器配置

我们需要修改application .properties,加入Spring Mvc 视图解析器配置
1
2
java复制代码spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

3.9.1.4、总结

所以我们在以后遇到,老旧的项目升级成Spring Boot 项目时候,首先得配置好 webapp 这个跟路径、配置好 web、再配置 ORM 所需的一些配置,最后记得配置视图解析器。所需的配置配置好后就可以直接把代码拷入新的项目了。

3.9.2、集成FreeMarker

在传统的SpringMVC中集成FreeMarker需要把FreeMarkerConfigurer和FreeMarkerViewResolve两个对象配置到Spring容器中,同时两个对象都要分别配置一些属性,还是比较麻烦的,在SpringBoot中,依靠自动配置功能,我们可以非常轻松的实现集成FreeMarker,只需要引入一个依赖即可。
1
2
3
4
5
xml复制代码<!-- SpringBoot集成FreeMarker的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

3.9.2.1、底层原理

SpringBoot的自动配置中含有`FreeMarkerAutoConfiguration`配置对象,该配置对象又导入了`FreeMarkerReactiveWebConfiguration`配置对象,在里面创建了`FreeMarkerConfigurer`和`FreeMarkerViewResolve`两个对象交给Spring管理,并且设置了默认的属性值,这些属性值来源于`FreeMarkerProperties`对象

3.9.2.2、常见属性配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
properties复制代码# 是否开启freemarker支持
spring.freemarker.enabled=true
# 模板编码
spring.freemarker.charset=UTF-8
# 模板contenttype
spring.freemarker.content-type=text/html
# 是否开启session属性暴露,默认false
spring.freemarker.expose-session-attributes = false
# 加载模板时候的前缀
spring.freemarker.prefix: templates
# 模板文件后缀,SpringBoot2.X默认是ftlh,有时候我们的模板后缀是ftl
spring.freemarker.suffix: ftl
# 模板加载地址
spring.freemarker.template-loader-path=classpath:/templates/

#一般我们会做3个配置,其余默认
# 暴露session对象的属性
spring.freemarker.expose-session-attributes=true
# 配置为传统模式,空值自动处理
spring.freemarker.settings.classic_compatible=true
# 重新指定模板文件后缀 springboot 2.2.x 后 默认后缀为 .ftlh
spring.freemarker.suffix=.ftl

3.9.3、整合 Thymeleaf

Thymeleaf是一款用于渲染XML/XHTML/HTML5内容的模板引擎。类似JSP,FreeMaker等, 它也可以轻易的与 Web 框架进行集成作。
为 Web 应用的模板引擎。与其它模板引擎相比, Thymeleaf 最大的特点是能够直接在浏览器中打开并正确显示模板页面,而不需要启动整个Web应用。

thymeLeaf支持Spring Expression Language语言作为方言,也就是SpEL,SpEL是可以用于Spring中的一种EL表达式。
它与我们使用过的JSP不同,thymeleaf是使用html的标签来完成逻辑和数据的传入进行渲染。可以说用 thymeleaf 完全替代 jsp 是可行的。

3.9.3.1、创建项目

我们在创建项目的时候记得勾选这两个依赖选项。

image-20210331192918732

image-20210331192908477

3.9.3.2、Spring Mvc 视图解析器配置

1
2
3
4
5
6
7
8
xml复制代码#thymeleaf
# 前缀 默认读取classpath:/templates/
#无需配置
#spring.thymeleaf.prefix=classpath:/templates/
# 后缀
spring.thymeleaf.suffix=.html
spring.thymeleaf.charset=UTF-8
spring.thymeleaf.servlet.content-type=text/html

3.9.3.3、测试

在templates下创建一个hello.html 必须加入xmlns:th="[www.thymeleaf.org](http://www.thymeleaf.org)" Thymeleaf声明空间。

本文转载自: 掘金

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

面试题:一头牛重800公斤,一座桥承重700公斤,牛应该怎么

发表于 2021-08-07

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

1 问题分析

在知乎上看到了这个有意思的问题,首先这个问题不是为了考察建筑工程学知识,因为面试者并非都具有建筑工程学经验。我认为这个问题是在考察三种分析方法:合理性分析、结构化分析、可行性分析。

800公斤牛过承重700公斤桥.jpeg

2 合理性分析

在职场上是允许争论需求和问题合理性的,拒绝掉一个不合理的需求,其实也是在节约资源和成本。例如产品经理提出业务需求,程序员用代码实现业务需求。在代码开发前大家会进行需求评审,首先评估需求合理性,再评估需求实现细节。如果经过充分讨论后,大家觉得本次需求不合理或者无法实现,那么本次需求会被拒绝。

回到这个问题,一头800公斤的牛要通过承重700公斤的桥,这个需求本身合理吗?那么我们可以从为什么、是否紧急、是否可替代这三个维度提出三个问题:

第一个问题:牛为什么要过桥,到底什么事情非要过桥不可,是否具有必要性

第二个问题:如果非要过桥,那么这个过桥需求紧急吗?不紧急可以从长计议

第三个问题:有没有什么替代方案,是否可以坐船或者绕路走,如果讨论结果是牛可以绕路走,那么无需再考虑桥的承重问题

3 结构化分析

如果经过讨论结果是牛非过桥不可,那么我们再思考牛怎么过桥的方案。这里可以使用结构化思维,将大问题拆分为小维度,尽量做到不遗漏和不重复。影响过桥的因素有这几个维度:桥的维度、牛的维度、资源维度、环境维度。

桥的维度:加固桥使承重大于800公斤

牛的维度:等待牛的体重小于700公斤

资源维度:使用一台吊机把牛运过去

环境维度:取消环境重力

4 可行性分析

我们从桥的维度、牛的维度、资源维度、环境维度给出了方案,那么选择哪个方案呢?这就需要我们进行可行性评估,因时因地在资源制约下选择当前最合适的方案。

加固桥方案经济成本较高,等待牛的体重小于700公斤时间成本较高,取消环境重力技术难度较高,所以使用一台吊机把牛运过去这个方案目前看来最合适。

5 结构化思维延伸

结构化思维的核心思想并不复杂:一件事情可以总结出一个中心思想,这个中心思想可以由三至七个论点支持,每个论点再可以由三至七个论据支持,基本结构图如下:

对于结构化思维仅仅分析到这里是不够的,还应该进一步去分析结构化思维的内在结构,而内在结构我们可以从横向和纵向两个维度去分析。

5.1 纵向结构

金字塔的纵向结构体现了两个原则:结论先行和以上统下,我们分别进行分析。

5.1.1 结论先行

结论先行是指开宗明义地展示中心思想,让听众一开始就明白沟通主旨,而如果把中心思想隐藏在沟通过程中,听众可能因为走神或者沟通信息太多而失焦,根本不知道你在说什么。结论先行具体有以下六个方面:

  • 先重要后次要
  • 先框架后细节
  • 先总体后细分
  • 先论点后论据
  • 先结论后原因
  • 先结果后过程

假设一个同事代码发布上线后导致系统故障,如果不使用结构化方法是这么表述的:

我看监控发现数据库负载升高,可能是没有加索引导致的。我又发现频繁收到重复消息,是不是消息中间件有什么问题?监控还显示创建了大量线程,是不是线程池使用不当导致的?问题排查很难短时间得到结论,我们还是先回滚代码至上一个版本吧

这位同事中心思想是问题原因比较难排查,应该先回滚代码再分析问题,但是他把最重要的观点放在最后,不听到最后不知道他要做什么,而如果结论先行应该怎么表述呢?

我们应立即当回滚代码,因为问题排查比较复杂,还是先恢复系统再排查问题。可能的问题分三类:第一可能是索引使用不当导致的数据库问题,第二可能是中间件问题导致大量重复接收消息,第三可能是线程池使用不当导致线程大量被创建。等到恢复正常之后我们依次排查这些问题

我们比较两段表述不难发现,第二段表述结构清晰很多,信息传达效率显著提升,这就是结论先行的优势所在。

5.1.2 以上统下

以上统下是指任何一个层的思想必须是其下一层思想的总结概括,我们分析一个例子进行说明:小王今天需要买牛肉、鸡蛋、萝卜、果汁、白菜、牛奶、青菜、鸡肉、酸奶,但这么多菜品他记不住,请你想办法帮助小王。

第一步我们要对菜品自下而上进行聚合归纳,这是一个找规律的过程。第二步再以上统下进行结构化表达从而帮助记忆。

自下而上聚合我们不难发现,牛肉、鸡肉、鸡蛋属于肉蛋类,白菜、青菜、萝卜属于蔬菜类,牛奶、果汁、酸奶属于饮品类,这样聚合之后我们再以上统下进行结构化表达。

上述实例比较简单,因为元素之间的关联性比较容易寻找,但是真实场景是不会这么简单的,元素之间关联性并不容易建立,那么我们应该如何从中心思想展开至第二层?

金字塔原理推荐使用疑问-回答式对话,通过设问的方式向下展开结构。那么应该问哪几个问题从而涵盖中心思想的要点?我们可以参考5W2H分析法,尽量做到要点不缺失:

  • What:是什么、做什么
  • Why:为什么、什么原因
  • Where:在哪里、从哪开始
  • When:开始结束时间、里程碑
  • Who:谁负责、谁来做、谁验收
  • How:怎么做、什么方法、从哪切入
  • How Much:做多少、各项指标是多少

在此模型基础上我们可以进行简化,从而减少要素的数量,这样更加易于结构化表达和记忆。我们一般选取What、Why、How这三个核心要素组成2W1H模型。

5.2 横向结构

现在我们需要思考如何组织论据,这就要使用横向结构的两个原则:归类分组和逻辑递进。我们分别进行分析。

5.2.1 归类分组

(1) 归纳推理

我们一般用归纳推理和演绎推理两种方法进行归类分组,我们先看归纳推理。

归纳推理是指把观察到的事实、规律归纳总结为理论。这种推理方法是不严谨的,因为只要观察事实和信息是有限的,那么归纳推理出来的结论就不一定是正确的。这就是逻辑错误中常见的一种:错误归因。

欧洲人看到的天鹅都是白色的,那么他们就归纳总结说所有的天鹅都是白色的。当一只黑天鹅出现时,这个结论就被证明是错误的,这就是黑天鹅事件。

当然我们不可能观察到所有事实,收集到所有信息,而一般是为了解决某个具体问题,我们会收集侧重于某个角度的信息,建立特定模型去分析解决问题,这也不失为一种有效方法。

金字塔原理归纳推理一般有以下四种维度:时间维度、结构维度、程度维度、经验维度。时间维度是根据天然时间线进行归纳,结构维度根据组织结构进行归纳,程度维度是根据程度级别进行归纳,经验维度是根据已有经验进行归纳。我们分别来看上述四种维度的几种常见类型:

时间维度
  • 事前、事中、事后
  • 短期、中期、长期
结构维度
  • 信息部、行政部、人力部
  • 开发组、测试组、运维组
程度维度
  • 高级、中级、初级
  • 重要、次要、不要
经验维度
  • 市场战略3C理论
  • 市场决策4P理论
  • 高扩展、高可用、高性能

我们选取时间维度和结构维度分析一个实例:怎样减少代码上线错误。从时间维度分析事前需要做好代码测试,事中需要监控关键指标,事后需要进行分析复盘。从结构维度分析开发人员需要做好单元测试,测试人员需要做好边界测试,运维人员需要完善监控平台。

(2) 演绎推理

演绎推理是指根据公理、定理或者自己相信的观念,做出推理或者判断,得到结论。

这种方法从逻辑上来说是严谨的。命题A是真的,推理出命题B也是真的,那是因为命题B的真实性包含在命题A中。

需要注意在逻辑上严谨,不是说结论一定是正确的。例如自己相信的观念最终被证明是错误的,那么得到结论也就是错误的。

标准式演绎推理分为大前提、小前提和结论:所有小鸟都会飞,这是一只小鸟,所以它会飞。

演绎推理还可以分为现象、原因和解决方案三个要素:现象是开发代码质量不高,原因是没有统一代码规约,解决方案是制定统一代码规约。

这是一种自上而下的推理方法,由已知的公理、定理或者观念向下推理。使用这种方法,需要在出现问题的领域有一定的经验和积累。

5.2.2 逻辑递进

逻辑递进是指每种思想需要按照一定顺序进行排列,例如时间维度按照事前、事中、事后进行排列,程度级别按照高级、中级、初级进行排列,这样排列的优点是符合理解和记忆的习惯。

6 文章总结

关于更多结构化思考内容请参看我的文章:结构化思维如何指导技术系统优化,回答这类问题结论不是最重要的,因为本质上是考察思考方法,所以思考过程才是最重要的。

欢迎大家关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思维、职场分享、产品思考等等,同时欢迎大家加我个人微信「java_front」一起交流学习

本文转载自: 掘金

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

花几天时间肝了一个在线制作词云图网站 01  功能介绍 02

发表于 2021-08-07

这是我参与8月更文挑战的第7天,活动详情查看: 8月更文挑战

大家好,我是辰哥~

背景:看过辰哥往前文章的都知道,在可视化方面经常绘制词云图、折线图、柱状图等。所以为了方便绘制这些图表,辰哥就把这些可视化图的绘制做成可操作的过程。

最近辰哥也是在利用空闲时间做了一个在线制作词云网站(后面会慢慢补上其他的图表),废话不多说,先看一下演示视频

在线制作词云图(可视化平台)

该网站已部署到公网,并且加上了ssl证书(防止浏览器报不安全,哈哈哈)

网址:show.chenlove.cn/

01 功能介绍

目前该网站只提供绘制词云图,后面会提供其他图的绘制,不过其操作差不多,都是导入数据,选择字段,最后点击生成图。

1
2
3
4
5
6
7
8
9
10
11
12
13
html复制代码<div class="form-group optionss" style="position:absolute;">
<!-- <input type="text" class="form-control form-control-xxx" value=""> -->
<!-- <button class="btn btn-sm btn-danger">-</button> -->
<select class="form-control form-select1" id="excelselect" onchange="getSelectValue();"/>
</select>
<select class="form-control form-select1" id="colorselect" onchange="colorSelectValue();"/>
<option value="">背景颜色</option>
<option value="1">白色</option>
<option value="2">黑色</option>
</select>
<button class="btn btn-sm btn-success" onclick="buttonstartdraw()">生成词云图</button>
<button class="btn btn-sm btn-success" onclick="downloadImg()">导出</button>
</div>

访问网站,可以看到样例图,点击上传数据的Excel数据,这里支持各种excel数据的后缀文件(辰哥这里测试了xls、xlsx、csv这三种常用的后缀都没问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码f = request.files['file']
basepath = os.path.dirname(__file__) # 当前文件所在路径
print(f.filename)
#######################################
# 毫秒级时间戳
file_name = str(round(time.time() * 1000))
dir = str(time.strftime('%y%m%d', time.localtime()))
upload_path = os.path.join(basepath, 'uploads/'+dir)
# 判断文件夹是否存在
if not os.path.exists(upload_path):
os.mkdir(upload_path)
#######################################
file_path = str(file_name)+str(f.filename)
f.save(upload_path+"/"+file_path)

可以预览自己的excel数据,右上角提供搜索excel表功能,翻页等等。这些都是通过开源插件bootstarp-table去实现。

可以选择excel中的任意一个字段去绘制词云图,以及选择词云图的背景颜色。然后点击生成词云图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
css复制代码<style>
#img {
text-align:center;
width:100%;
margin-top: 60px;
}
img{
width: 300px;
overflow: hidden;
margin: 0 auto;

}
#img img{
width: 40%;
transition:all 2s;/*图片放大过程的时间*/
position: relative;
}
img:hover{
cursor: crosshair;
transform: scale(2.0); /*以y轴为中心旋转*/
}
</style>

生成的词云图可以支持放大预览:鼠标放上去,自动放大。

1
2
3
4
5
6
7
8
9
javascript复制代码function downloadImg(){
var img = document.getElementById('cyimg'); // 获取要下载的图片
var url = img.src; // 获取图片地址
var a = document.createElement('a'); // 创建一个a节点插入的document
var event = new MouseEvent('click') // 模拟鼠标click点击事件
a.download = 'beautifulGirl' // 设置a节点的download属性值
a.href = url; // 将图片的src赋值给a节点的href
a.dispatchEvent(event) // 触发鼠标点击事件
}

最后点击导出,可以将生成的词云图导出到本地。

02 小结

辰哥接下来会继续更新更多的可视化图,提供给大家去使用,如果对该网站有更好的建议,欢迎在下方留言或者私信辰哥。

网站也会一直放在公网,提供给大家访问,上传的excel和生成的图片都是在第二天清空(网站不提供存储,也不会记录大家的访问以及上传的数据等)

最后再次附上网站地址:show.chenlove.cn

本文转载自: 掘金

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

1…574575576…956

开发者博客

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