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

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


  • 首页

  • 归档

  • 搜索

你居然还去服务器上捞日志,搭个日志收集系统难道不香么!

发表于 2020-06-22

SpringBoot实战电商项目mall(35k+star)地址:github.com/macrozheng/…

摘要

ELK日志收集系统进阶使用,本文主要讲解如何打造一个线上环境真实可用的日志收集系统。有了它,你就可以和去服务器上捞日志说再见了!

ELK环境安装

ELK是指Elasticsearch、Kibana、Logstash这三种服务搭建的日志收集系统,具体搭建方式可以参考《SpringBoot应用整合ELK实现日志收集》。这里仅提供最新版本的docker-compose脚本和一些安装要点。

docker-compose脚本

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
yaml复制代码version: '3'
services:
elasticsearch:
image: elasticsearch:6.4.0
container_name: elasticsearch
environment:
- "cluster.name=elasticsearch" #设置集群名称为elasticsearch
- "discovery.type=single-node" #以单一节点模式启动
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" #设置使用jvm内存大小
- TZ=Asia/Shanghai
volumes:
- /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins #插件文件挂载
- /mydata/elasticsearch/data:/usr/share/elasticsearch/data #数据文件挂载
ports:
- 9200:9200
- 9300:9300
kibana:
image: kibana:6.4.0
container_name: kibana
links:
- elasticsearch:es #可以用es这个域名访问elasticsearch服务
depends_on:
- elasticsearch #kibana在elasticsearch启动之后再启动
environment:
- "elasticsearch.hosts=http://es:9200" #设置访问elasticsearch的地址
- TZ=Asia/Shanghai
ports:
- 5601:5601
logstash:
image: logstash:6.4.0
container_name: logstash
environment:
- TZ=Asia/Shanghai
volumes:
- /mydata/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf #挂载logstash的配置文件
depends_on:
- elasticsearch #kibana在elasticsearch启动之后再启动
links:
- elasticsearch:es #可以用es这个域名访问elasticsearch服务
ports:
- 4560:4560
- 4561:4561
- 4562:4562
- 4563:4563

安装要点

  • 使用docker-compose命令运行所有服务:
1
bash复制代码docker-compose up -d
  • 第一次启动可能会发现Elasticsearch无法启动,那是因为/usr/share/elasticsearch/data目录没有访问权限,只需要修改/mydata/elasticsearch/data目录的权限,再重新启动;
1
bash复制代码chmod 777 /mydata/elasticsearch/data/
  • Logstash需要安装json_lines插件。
1
bash复制代码logstash-plugin install logstash-codec-json_lines

分场景收集日志

这里为了方便我们查看日志,提出一个分场景收集日志的概念,把日志分为以下四种。

  • 调试日志:最全日志,包含了应用中所有DEBUG级别以上的日志,仅在开发、测试环境中开启收集;
  • 错误日志:只包含应用中所有ERROR级别的日志,所有环境只都开启收集;
  • 业务日志:在我们应用对应包下打印的日志,可用于查看我们自己在应用中打印的业务日志;
  • 记录日志:每个接口的访问记录,可以用来查看接口执行效率,获取接口访问参数。

Logback配置详解

要实现上面的分场景收集日志,主要通过Logback的配置来实现,我们先来了解下Logback的配置吧!

完全配置

在SpringBoot中,如果我们想要自定义Logback的配置,需要自行编写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
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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<!--引用默认日志配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--使用默认的控制台日志输出实现-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!--应用名称-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/>
<!--日志文件保存路径-->
<property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/>
<!--LogStash访问host-->
<springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/>

<!--DEBUG日志输出到文件-->
<appender name="FILE_DEBUG"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--输出DEBUG以上级别日志-->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<!--设置为默认的文件日志格式-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/debug/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>

<!--ERROR日志输出到文件-->
<appender name="FILE_ERROR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--只输出ERROR级别的日志-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<!--设置为默认的文件日志格式-->
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>

<!--DEBUG日志输出到LogStash-->
<appender name="LOG_STASH_DEBUG" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<destination>${LOG_STASH_HOST}:4560</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--ERROR日志输出到LogStash-->
<appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<destination>${LOG_STASH_HOST}:4561</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--业务日志输出到LogStash-->
<appender name="LOG_STASH_BUSINESS" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4562</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--接口访问记录日志输出到LogStash-->
<appender name="LOG_STASH_RECORD" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4563</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"class": "%logger",
"message": "%message"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

<!--控制框架输出日志-->
<logger name="org.slf4j" level="INFO"/>
<logger name="springfox" level="INFO"/>
<logger name="io.swagger" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.hibernate.validator" level="INFO"/>

<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
<!--<appender-ref ref="FILE_DEBUG"/>-->
<!--<appender-ref ref="FILE_ERROR"/>-->
<appender-ref ref="LOG_STASH_DEBUG"/>
<appender-ref ref="LOG_STASH_ERROR"/>
</root>

<logger name="com.macro.mall.tiny.component" level="DEBUG">
<appender-ref ref="LOG_STASH_RECORD"/>
</logger>

<logger name="com.macro.mall" level="DEBUG">
<appender-ref ref="LOG_STASH_BUSINESS"/>
</logger>
</configuration>

配置要点解析

使用默认的日志配置

一般我们不需要自定义控制台输出,可以采用默认配置,具体配置参考console-appender.xml,该文件在spring-boot-${version}.jar下面。

1
2
3
4
xml复制代码<!--引用默认日志配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!--使用默认的控制台日志输出实现-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

springProperty

该标签可以从SpringBoot的配置文件中获取配置属性,比如说在不同环境下我们的Logstash服务地址是不一样的,我们就可以把该地址定义在application.yml来使用。

例如在application-dev.yml中定义了这些属性:

1
2
yaml复制代码logstash:
host: localhost

在logback-spring.xml中就可以直接这样使用:

1
2
3
4
xml复制代码<!--应用名称-->
<springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="springBoot"/>
<!--LogStash访问host-->
<springProperty name="LOG_STASH_HOST" scope="context" source="logstash.host" defaultValue="localhost"/>

filter

在Logback中有两种不同的过滤器,用来过滤日志输出。

ThresholdFilter:临界值过滤器,过滤掉低于指定临界值的日志,比如下面的配置将过滤掉所有低于INFO级别的日志。

1
2
3
xml复制代码<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>

LevelFilter:级别过滤器,根据日志级别进行过滤,比如下面的配置将过滤掉所有非ERROR级别的日志。

1
2
3
4
5
xml复制代码<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>

appender

Appender可以用来控制日志的输出形式,主要有下面三种。

  • ConsoleAppender:控制日志输出到控制台的形式,比如在console-appender.xml中定义的默认控制台输出。
1
2
3
4
5
xml复制代码<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
  • RollingFileAppender:控制日志输出到文件的形式,可以控制日志文件生成策略,比如文件名称格式、超过多大重新生成文件以及删除超过多少天的文件。
1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码<!--ERROR日志输出到文件-->
<appender name="FILE_ERROR"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--设置文件命名格式-->
<fileNamePattern>${LOG_FILE_PATH}/error/${APP_NAME}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<!--设置日志文件大小,超过就重新生成文件,默认10M-->
<maxFileSize>${LOG_FILE_MAX_SIZE:-10MB}</maxFileSize>
<!--日志文件保留天数,默认30天-->
<maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
</rollingPolicy>
</appender>
  • LogstashTcpSocketAppender:控制日志输出到Logstash的形式,可以用来配置Logstash的地址、访问策略以及日志的格式。
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
xml复制代码<!--ERROR日志输出到LogStash-->
<appender name="LOG_STASH_ERROR" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<destination>${LOG_STASH_HOST}:4561</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>Asia/Shanghai</timeZone>
</timestamp>
<!--自定义日志输出格式-->
<pattern>
<pattern>
{
"project": "mall-tiny",
"level": "%level",
"service": "${APP_NAME:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger",
"message": "%message",
"stack_trace": "%exception{20}"
}
</pattern>
</pattern>
</providers>
</encoder>
<!--当有多个LogStash服务时,设置访问策略为轮询-->
<connectionStrategy>
<roundRobin>
<connectionTTL>5 minutes</connectionTTL>
</roundRobin>
</connectionStrategy>
</appender>

logger

只有配置到logger节点上的appender才会被使用,logger用于配置哪种条件下的日志被打印,root是一种特殊的appender,下面介绍下日志划分的条件。

  • 调试日志:所有的DEBUG级别以上日志;
  • 错误日志:所有的ERROR级别日志;
  • 业务日志:com.macro.mall包下的所有DEBUG级别以上日志;
  • 记录日志:com.macro.mall.tiny.component.WebLogAspect类下所有DEBUG级别以上日志,该类是统计接口访问信息的AOP切面类。

控制框架输出日志

还有一些使用框架内部的日志,DEBUG级别的日志对我们并没有啥用处,都可以设置为了INFO以上级别。

1
2
3
4
5
6
xml复制代码<!--控制框架输出日志-->
<logger name="org.slf4j" level="INFO"/>
<logger name="springfox" level="INFO"/>
<logger name="io.swagger" level="INFO"/>
<logger name="org.springframework" level="INFO"/>
<logger name="org.hibernate.validator" level="INFO"/>

Logstash配置详解

接下来我们需要配置下Logstash,让它可以分场景收集不同的日志,下面详细介绍下使用到的配置。

完全配置

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
ini复制代码input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => json_lines
type => "debug"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4561
codec => json_lines
type => "error"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4562
codec => json_lines
type => "business"
}
tcp {
mode => "server"
host => "0.0.0.0"
port => 4563
codec => json_lines
type => "record"
}
}
filter{
if [type] == "record" {
mutate {
remove_field => "port"
remove_field => "host"
remove_field => "@version"
}
json {
source => "message"
remove_field => ["message"]
}
}
}
output {
elasticsearch {
hosts => ["es:9200"]
action => "index"
codec => json
index => "mall-tiny-%{type}-%{+YYYY.MM.dd}"
template_name => "mall-tiny"
}
}

配置要点

  • input:使用不同端口收集不同类型的日志,从4560~4563开启四个端口;
  • filter:对于记录类型的日志,直接将JSON格式的message转化到source中去,便于搜索查看;
  • output:按类型、时间自定义索引格式。

SpringBoot配置

在SpringBoot中的配置可以直接用来覆盖Logback中的配置,比如logging.level.root就可以覆盖<root>节点中的level配置。

  • 开发环境配置:application-dev.yml
1
2
3
4
5
yaml复制代码logstash:
host: localhost
logging:
level:
root: debug
  • 测试环境配置:application-test.yml
1
2
3
4
5
yaml复制代码logstash:
host: 192.168.3.101
logging:
level:
root: debug
  • 生产环境配置:application-prod.yml
1
2
3
4
5
yaml复制代码logstash:
host: logstash-prod
logging:
level:
root: info

Kibana进阶使用

进过上面ELK环境的搭建和配置以后,我们的日志收集系统终于可以用起来了,下面介绍下在Kibana中的使用技巧!

  • 首先启动我们的测试Demo,然后通用调用接口(可以使用Swagger),产生一些日志信息;

  • 调用完成后在Management->Kibana->Index Patterns中可以创建Index Patterns,Kibana服务访问地址:http://192.168.3.101:5601

  • 创建完成后可以在Discover中查看所有日志,调试日志只需直接查看mall-tiny-debug*模式的日志即可;

  • 对于日志搜索,kibana有非常强大的提示功能,可以通过搜索栏右侧的Options按钮打开;

  • 记录日志只需直接查看mall-tiny-record*模式的日志即可,如果我们想要搜索uri为/brand/listAll的记录日志,只需在搜索栏中输入uri : "/brand/listAll";

  • 错误日志,只需直接查看mall-tiny-error*模式的日志即可;

  • 业务日志,只需直接查看mall-tiny-business*模式的日志即可,这里我们可以查看一些SQL日志的输出;

  • 如果日志太大了,可以通过Elasticsearch->Index Management选择删除即可。

项目源码地址

github.com/macrozheng/…

公众号

mall项目全套学习教程连载中,关注公众号第一时间获取。

公众号图片

本文转载自: 掘金

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

面试官:十问泛型,你能扛住吗?

发表于 2020-06-22

问题一:为什么需要泛型?

答:

使用泛型机制编写的代码要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性,也就是说使用泛型机制编写的代码可以被很多不同类型的对象所重用。

问题二:从ArrayList的角度说一下为什么要用泛型?

答:

在Java增加泛型机制之前就已经有一个ArrayList类,这个ArrayList类的泛型概念是使用继承来实现的。

1
2
3
4
5
复制代码public class ArrayList {  
    private Object[] elementData;
    public Object get(int i) {....}
    public void add(Object o) {....}
}

这个类存在两个问题:

  1. 当获取一个值的时候必须进行强制类型转换
  2. 没有错误检查,可以向数组中添加任何类的对象
1
2
3
复制代码ArrayList files = new ArrayList();  
files.add(new File(""));
String filename = (String)files.get(0);

对于这个调用,编译和运行都不会出错,但是当我们在其他地方使用get方法获取刚刚存入的这个File对象强转为String类型的时候就会产生一个错误。

泛型对于这种问题的解决方案是提供一个类型参数。

1
复制代码ArrayList<String> files = new ArrayList<>();

这样可以使代码具有更好的可读性,我们一看就知道这个数据列表中包含的是String对象。
编译器也可以很好地利用这个信息,当我们调用get的时候,不需要再使用强制类型转换,编译器就知道返回值类型为String,而不是Object:

1
复制代码String filename = files.get(0);

编译器还知道ArrayList<String>中add方法中有一个类型为String的参数。这将比使用Object类型的参数安全一些,现在编译器可以检查,避免插入错误类型的对象:

1
复制代码files.add(new File(""));

这样的代码是无法通过编译的,出现编译错误比类在运行时出现类的强制类型转换异常要好得多。

问题三:说说泛型类吧

一个泛型类就是具有一个或多个类型变量的类,对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。

1
2
3
4
复制代码public class Couple<T> {  
   private T one;
   private T two;
}

Singer类引入了一个类型变量T,用尖括号括起来,并放在类名的后面。泛型类可以有多个类型变量:

1
复制代码public class Couple<T, U> {...}

类定义中的类型变量是指定方法的返回类型以及域和局部变量的类型

1
2
3
4
5
6
复制代码//域  
private T one;
//返回类型
public T getOne() { return one; }
//局部变量
public void setOne(T newValue) { one = newValue; }

使用具体的类型代替类型变量就可以实例化泛型类型:

1
复制代码Couple<Rapper>

泛型类可以看成是普通类的工厂,打个比方:我用泛型造了一个模型,具体填充什么样的材质,由使用者去做决定。

问题四: 说说泛型方法的定义和使用

答:

泛型方法可以定义在普通类中,也可以定义在泛型类中,类型变量是放在修饰符的后面,返回类型的前面。

我们来看一个泛型方法的实例:

1
2
3
4
5
6
复制代码class ArrayUtil {  

    public static <T> T getMiddle(T...a){
        return a[a.length / 2];
    }
}

当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:

1
复制代码String middle = ArrayUtil.<String>getMiddle("a","b","c");

在这种情况下,方法调用中可以省略<String>类型参数,编译器会使用类型推断来推断出所调用的方法,也就是说可以这么写:

1
复制代码String middle = ArrayAlg.getMiddle("a","b","c");

问题五:E V T K ? 这些是什么

答:

  • E——Element 表示元素 特性是一种枚举
  • T——Type 类,是指Java类型
  • K—— Key 键
  • V——Value 值
  • ?——在使用中表示不确定类型

问题六:了解过类型变量的限定吗?

答:

一个类型变量或通配符可以有多个限定,例如:

1
复制代码<T extends Serializable & Cloneable>

单个类型变量的多个限定类型使用&分隔,而,用来分隔多个类型变量。

1
复制代码<T extends Serializable,Cloneable>

在类型变量的继承中,可以根据需要拥有多个接口超类型,但是限定中至多有一个类。如果用一个类作为限定,它必定是限定列表中的第一个。

类型变量的限定是为了限制泛型的行为,指定了只有实现了特定接口的类才可以作为类型变量去实例化一个类。

问题七:泛型与继承你知道多少?

答:

首先,我们来看一个类和它的子类,比如 Singer 和 Rapper。但是Couple<Rapper>却并不是Couple<Singer>的一个子类。

无论S和T有什么联系,Couple<S>与Couple<T>没有什么联系。

这里需要注意泛型和Java数组之间的区别,可以将一个Rapper[]数组赋给一个类型为Singer[]的变量:

1
2
复制代码Rapper[] rappers = ...;  
Singer[] singer = rappers;

然而,数组带有特别的保护,如果试图将一个超类存储到一个子类数组中,虚拟机会抛出ArrayStoreException异常。

问题八:聊聊通配符吧

答:

通配符类型中,允许类型参数变化。比如,通配符类型:

1
复制代码Couple<? extends Singer>

表示任何泛型类型,它的类型参数是Singer的子类,如Couple<Rapper>,但不会是Couple<Dancer>。

假如现在我们需要编写一个方法去打印一些东西:

1
2
3
4
5
复制代码public static void printCps(Couple<Rapper> cps) {  
      Rapper one = cp.getOne();
      Rapper two = cp.getTwo();
      System.out.println(one.getName() + " & " + two.getName() + " are cps.");
}

正如前面所讲到的,不能将Couple<Rapper>传递给这个方法,这一点很受限制。解决的方案很简单,使用通配符类型:

1
复制代码public static void printCps(Couple< ? extends Singer> cps)

Couple<Rapper>是Couple< ? extends Singer>的子类型。

我们接下来来考虑另外一个问题,使用通配符会通过Couple< ? extends Singer>的引用破坏Couple<Rapper>吗?

1
2
3
复制代码Couple<Rapper> rapper = new Couple<>(rapper1, rapper2);  
Couple<? extends Singer> singer = rapper;
player.setOne(reader);

这样可能会引起破坏,但是当我们调用setOne的时候,如果调用的不是Singer的子类Rapper类的对象,而是其他Singer子类的对象,就会出错。
我们来看一下Couple<? extends Singer>的方法:

1
2
复制代码? extends Singer getOne();  
void setOne(? extends Singer);

这样就会看的很明显,因为如果我们去调用setOne()方法,编译器之可以知道是某个Singer的子类型,而不能确定具体是什么类型,它拒绝传递任何特定的类型,因为 ? 不能用来匹配。
但是使用getOne就不存在这个问题,因为我们无需care它获取到的类型是什么,但一定是Singer的子类。

通配符限定与类型变量限定非常相似,但是通配符类型还有一个附加的能力,即可以指定一个超类型限定:

1
复制代码? super Rapper

这个通配符限制为Rapper的所有父类,为什么要这么做呢?带有超类型限定的通配符的行为与子类型限定的通配符行为完全相反,可以为方法提供参数,但是却不能获取具体的值,即访问器是不安全的,而更改器方法是安全的:

编译器无法知道setOne方法的具体类型,因此调用这个方法时不能接收类型为Singer或Object的参数。只能传递Rapper类型的对象,或者某个子类型(Reader)对象。而且,如果调用getOne,不能保证返回对象的类型。

总结一下:

带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

问题九:泛型在虚拟机中是什么样呢?

答:

  1. 虚拟机没有泛型类型对象,所有的对象都属于普通类。
    无论何时定义一个泛型类型,都自动提供了一个相应的原始类型。原始类型的名字就是删去类型参数后的泛型类型名。擦除类型变量,并替换成限定类型(没有限定的变量用Object)。这样做的目的是为了让非泛型的Java程序在后续支持泛型的 jvm 上还可以运行(向后兼容)
  2. 当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。
1
2
复制代码Couple<Singer> cps = ...;  
Singer one = cp.getOne();

擦除cp.getOne的返回类型后将返回Object类型。编译器自动插入Singer的强制类型转换。也就是说,编译器把这个方法调用编译为两条虚拟机指令:

对原始方法cp.getOne的调用
将返回的Object类型强制转换为Singer类型。

  1. 当存取一个公有泛型域时也要插入强制类型转换。
1
2
3
4
复制代码//我们写的代码  
Singer one = cps.one;
//编译器做的事情
Singer one = (Singer)cps.one;

问题十:关于泛型擦除,你知道多少?

答:

类型擦除会出现在泛型方法中,程序员通常认为下述的泛型方法

1
复制代码public static <T extends Comparable> T min(T[] a)

是一个完整的方法族,而擦除类型之后,只剩下一个方法:

1
复制代码public static Comparable min(Comparable[] a)

这个时候类型参数T已经被擦除了,只留下了限定类型Comparable。

但是方法的擦除会带来一些问题:

1
2
3
4
5
复制代码class Coupling extends Couple<People> {  
    public void setTwo(People people) {
            super.setTwo(people);
    }
}

擦除后:

1
2
3
复制代码class Coupling extends Couple {  
    public void setTwo(People People) {...}
}

这时,问题出现了,存在另一个从Couple类继承的setTwo方法,即:

1
复制代码public void setTwo(Object two)

这显然是一个不同的方法,因为它有一个不同类型的参数(Object),而不是People。

1
2
3
复制代码Coupling coupling = new Coupling(...);  
Couple<People> cp = interval;
cp.setTwo(people);

这里,希望对setTwo的调用具有多态性,并调用最合适的那个方法。由于cp引用Coupling对象,所以应该调用Coupling.setTwo。问题在于类型擦除与多态发生了冲突。要解决这个问题,就需要编译器在Coupling类中生成一个桥方法:

1
2
3
复制代码public void setTwo(Object second) {  
    setTwo((People)second);
}

变量cp已经声明为类型Couple<LocalDate>,并且这个类型只有一个简单的方法叫setTwo,即setTwo(Object)。虚拟机用cp引用的对象调用这个方法。这个对象是Coupling类型的,所以会调用Coupling.setTwo(Object)方法。这个方法是合成的桥方法。它会调用Coupling.setTwo(Date),这也正是我们所期望的结果。

所以,我们要记住关于Java泛型转换的几个点:

  1. 虚拟机中没有泛型,只有普通的类和方法
  2. 所有的类型参数都用它们的限定类型替换
  3. 桥方法被合成来保持多态
  4. 为保持类型安全性,必要时插入强制类型转换

如果你有学到,请给我点赞👍+关注,这是对一个✊坚持原创作者的最大支持!我是山禾,千篇一律的皮囊,万里挑一的灵魂,一个不太一样的写手。

本文转载自: 掘金

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

Jetpack 成员 Paging3 网络实践及原理分析(二

发表于 2020-06-21

前言

Google 最近更新了几个 Jetpack 新成员 Hilt、Paging 3、App Startup 等等。

在之前的文章里面分别分析 App Startup 实践以及原理 和 Paging3 加载本地数据(一)实践以及原理,如果没有看过可以点击下方地址前去查看:

  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 数据库实践以及源码分析(一)
  • Jetpack 成员 Paging3 网络实践及原理分析(二)
  • Jetpack 成员 Paging3 使用 RemoteMediator 实现加载网络分页数据并更新到数据库中(三)
  • 代码地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

今天这边文章主要来分析 Paging3 加载网络数据及其原理,利用周末的时间参考 Google 文档实现了 Paging3 期间也遇到一些坑,会在文中详细分析,代码已经上传到了 GitHub:Paging3SimpleWithNetWork

通过这篇文章你将学习到以下内容:

  • Paging3 是什么?
  • Paging3 相对之前版本 (Paging1、Paging2) 核心的变化?
  • 关于 Paging 支持的分页策略?
  • 在项目中如何使用 Paging3 去加载网络数据?
  • Paging3 网络异常如何处理?
  • Paging3 如何监听网络请求状态?
  • Paging3 如何进行刷新和重试?

在项目 Paging3SimpleWithNetWork 中用到了 Coil(Kotlin 图片加载库)、Databinding(数据绑定)、Anko(主要用来替换替代 XML 使用的方式)、Koin(Kotlin 依赖注入库)、JDatabinding(基于 Databinding 封装的组件)、Data Mapper(数据映射)、使用 Composing builds 作为依赖库的版本管理、Repository 设计模式、MVVM 架构等等,关于这里一些技术之前没有了解过,可以点击下面连接前往查看。

  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • [译][2.4K Star] 放弃 Dagger 拥抱 Koin
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 项目中封装 Kotlin + Android Databinding
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析

Paging3 是什么?

Paging 是一个分页库,它可以帮助您从本地存储或通过网络加载显示数据。这种方法使你的 App 更有效地使用网络带宽和系统资源。

Google 推荐使用 Paging 作为 App 架构的一部分,它可以很方便的和 Jetpack 组件集成,Paging3 包含了以下功能:

  • 在内存中缓存分页数据,确保您的 App 在使用分页数据时有效地使用系统资源。
  • 内置删除重复数据的请求,确保您的 App 有效地使用网络带宽和系统资源。
  • 可配置 RecyclerView 的 adapters,当用户滚动到加载数据的末尾时自动请求数据。
  • 支持 Kotlin 协程和 Flow, 以及 LiveData 和 RxJava。
  • 内置的错误处理支持,包括刷新和重试等功能。

Paging3 相对于之前类的职能变化

在 Paging3 之前提供了 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 这三个类,在这三个类中进行数据获取的操作。

  • PositionalDataSource:主要用于加载数据有限的数据(加载本地数据库)
  • ItemKeyedDataSource:主要用来请求网络数据,它适用于通过当前页面最后一条数据的 id,作为下一页的数据的开始的位置,例如 Github 的 API。
    • 例如地址 https://api.github.com/users?since=0?per_page=30 当 since = 0 时获取第一页数据,当前页面最后一条数据的 ID 是 46。
    • 将 46 作为开始位置,此时 since = 46,地址变成:https://api.github.com/users?since=46?per_page=30。
  • PageKeyedDataSource:也是用来请求网络数据,它适用于通过页码分页来请求数据。

在 Paging3 之后 ItemKeyedDataSource、PageKeyedDataSource、PositionalDataSource 合并为一个 PagingSource,所有旧 API 加载方法被合并到 PagingSource 中的单个 load() 方法中。

1
kotlin复制代码abstract suspend fun load(params: LoadParams<Key>): LoadResult<Key, Value>

这是一个挂起函数,实现这个方法来触发异步加载,具体实现见下文,另外在 Paging3 中还有以下变化

  • LivePagedListBuilder 和 RxPagedListBuilder 合并为了 Pager。
  • 使用 PagedList.Config 替换 PagingConfig。
  • 使用 RemoteMediator 替换了 PagedList.BoundaryCallback 去加载网络和本地数据库的数据。

四步实现 Paging3 加载网络数据

Google 推荐我们使用 Paging3 时,在应用程序的三层中操作,以及它们如何协同工作加载和显示分页数据,如下图所示:

我们接下来按照 Google 推荐的方式开始实现,只需要四步即可实现 Paging3 加载网络数据,文中只贴出核心代码,具体实现可以看 GitHub 上的 Paging3SimpleWithNetWork 项目。

1. 网络请求部分

这里选择使用的是 GitHub API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码interface GitHubService {

@GET("users")
suspend fun getGithubAccount(@Query("since") id: Int, @Query("per_page") perPage: Int):
List<GithubAccountModel>

companion object {
fun create(): GitHubService {
val client = OkHttpClient.Builder()
.build()

val retrofit = Retrofit.Builder()
.client(client)
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()

return retrofit.create(GitHubService::class.java)
}
}
}

注意: 这里需要在 getGithubAccount 方法前添加 suspend 关键字,否则调用的时候,会抛出以下异常。

1
sql复制代码Unable to create call adapter for XXXXX

2. 在 Repository 层创建 PagingSource 数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码class GitHubItemPagingSource(
private val api: GitHubService
) : PagingSource<Int, GithubAccountModel>(), AnkoLogger {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GithubAccountModel> {

return try {
// key 相当于 id
val key = params.key ?: 0
// 获取网络数据
val items = api.getGithubAccount(key, params.loadSize)
// 请求失败或者出现异常,会跳转到 case 语句返回 LoadResult.Error(e)
// 请求成功,构造一个 LoadResult.Page 返回
LoadResult.Page(
data = items, // 返回获取到的数据
prevKey = null, // 上一页,设置为空就没有上一页的效果,这需要注意的是,如果是第一页需要返回 null,否则会出现多次请求
nextKey = items.lastOrNull()?.id// 下一页,设置为空就没有加载更多效果,如果后面没有更多数据设置为空,即滑动到最后不会在加载数据
)
} catch (e: Exception) {
e.printStackTrace()
LoadResult.Error(e)
}
}
}
  • PagingSource 是一个抽象类,主要用来向 Paging 提供源数据,需要重写 load 方法,在这个方法进行网络请求的处理。需要注意的是 LoadResult.Page 里面的两个参数 prevKey 和 nextKey,这里有个坑。
+ prevKey:上一页,设置为空就没有上一页的效果,这需要注意的是,如果是第一页需要返回 null,否则会出现多次请求,我刚开始忽略了,导致首次加载的时候,出现了两次请求。
+ nextKey:下一页,设置为空就没有加载更多效果,如果后面没有更多数据设置为空,即滑动到最后不会在加载数据。
  • load 方法的参数 LoadParams,它是一个密封类,里面有三个内部类 Refresh、Append、Prepend。
类名 作用
Refresh 在初始化刷新的使用
Append 在加载更多的时候使用
Prepend 在当前列表头部添加数据的时候使用

3. 在 Repository 层创建 Pager 和 PagingData

  • Pager:是主要的入口页面,在其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory。
  • PagingData:是分页数据的容器,它查询一个 PagingSource 对象并存储结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kotlin复制代码class GitHubRepositoryImpl(
val pageConfig: PagingConfig,
val gitHubApi: GitHubService,
val mapper2Person: Mapper<GithubAccountModel, GitHubAccount>
) : Repository {

override fun postOfData(id: Int): Flow<PagingData<GitHubAccount>> {
return Pager(pageConfig) {
// 加载数据库的数据
GitHubItemPagingSource(gitHubApi, 0)
}.flow.map { pagingData ->
// 数据映射,数据源 GithubAccountModel ——> 上层用到的 GitHubAccount
pagingData.map { mapper2Person.map(it) }
}
}
}

在 postOfData 方法中构建了一个 Pager, 其构造方法中接受 PagingConfig、initialKey、remoteMediator、pagingSourceFactory,其中 initialKey、remoteMediator 是可选的,pageConfig 和 pagingSourceFactory 必填的。

pagingSourceFactory 是一个 lambda 表达式,在 Kotlin 中可以直接用花括号表示,在花括号内,执行执行网络请求 GitHubItemPagingSource(gitHubApi, 0)。

最后调用 flow 返回 Flow<PagingData<Value>>,然后通过 Flow 的 map 方法将数据源 GithubAccountModel 转换成上层用到的 GithubAccount。

关于 flow 在上一篇 Jetpack 成员 Paging3 实践以及源码分析(一) 已经分析过了.

4. 最后一步,接受数据,并绑定 UI

在 ViewModel 接受数据,并传递给 Adapter.

1
2
css复制代码val gitHubLiveData: LiveData<PagingData<GitHubAccount>> =
repository.postOfData(0).asLiveData()

LiveData 有三种使用方式,这里演示的是其中一种,其余的在之前的文章
Jetpack 成员 Paging3 实践以及源码分析(一) 已经分析过了。

1
2
3
kotlin复制代码 mMainViewModel.gitHubLiveData.observe(this, Observer { data ->
mAdapter.submitData(lifecycle, data)
})

到这里请求网络数据并显示的在 UI 上就结束了,最后我们来分析一下 Paging3 内置的错误处理支持,包括刷新和重试等功能。

5. 网络状态异常的处理

Paging3 提供了内置的错误处理支持,包括刷新和重试等功能,说到这里 Google 对于 Paging3 的设计相比于之前的设计真的好,基本上进行网络请求地方用 RecyclerView 去展示数据,都需要用到刷新、重试、错误处理等等功能。

1. 错误处理

Paging3 的组件 PagingDataAdapter,PagingDataAdapter 是一个处理分页数据的可回收视图适配器,PagingDataAdapter 提供了三个方法,如下图所示:

方法名 作用
withLoadStateFooter 添加列表底部(类似于加载更多)
withLoadStateHeader 添加列表的头部
withLoadStateHeaderAndFooter 添加头部和底部

Paging3 提供了 LoadStateAdapter 用于实现列表底部和头部样式,只需要继承 LoadStateAdapter 做对应的网络状态处理即可,例如这里实现的 FooterAdapter 加载更多样式。

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
kotlin复制代码class FooterAdapter(val adapter: GitHubAdapter) : LoadStateAdapter<NetworkStateItemViewHolder>() {
override fun onBindViewHolder(holder: NetworkStateItemViewHolder, loadState: LoadState) {
holder.bindData(loadState, 0)
}

override fun onCreateViewHolder(
parent: ViewGroup,
loadState: LoadState
): NetworkStateItemViewHolder {
val view = inflateView(parent, R.layout.recycie_item_network_state)
return NetworkStateItemViewHolder(view) { adapter.retry() }
}

private fun inflateView(viewGroup: ViewGroup, @LayoutRes viewType: Int): View {
val layoutInflater = LayoutInflater.from(viewGroup.context)
return layoutInflater.inflate(viewType, viewGroup, false)
}
}

class NetworkStateItemViewHolder(view: View, private val retryCallback: () -> Unit) :
DataBindingViewHolder<LoadState>(view) {
val mBinding: RecycieItemNetworkStateBinding by viewHolderBinding(view)

override fun bindData(data: LoadState, position: Int) {
mBinding.apply {
// 正在加载,显示进度条
progressBar.isVisible = data is LoadState.Loading
// 加载失败,显示并点击重试按钮
retryButton.isVisible = data is LoadState.Error
retryButton.setOnClickListener { retryCallback() }
// 加载失败显示错误原因
errorMsg.isVisible = !(data as? LoadState.Error)?.error?.message.isNullOrBlank()
errorMsg.text = (data as? LoadState.Error)?.error?.message

executePendingBindings()
}
}
}

在上面分别处理了,正在加载、加载失败并提供重试按钮等等状态。

2. Paging3 同时提供了刷新、重试等等方法,如下图所示:

  • refresh:常用用于下拉更新数据。
  • retry:常用于底部更多样式,当请求网络失败的时候,显示重试按钮,点击调用 retry。

3. Paging3 还帮我处理了如果出现多次网络请求,只会处理最后一次请求,例如由于网络慢,用户频繁的刷新数据等等

6. 监听网路请求状态

刚才分析过 PagingDataAdapter 是一个处理分页数据的可回收视图适配器,并且还提供了两个监听数据状态的方法。

这两个方法的区别是:

  • addDataRefreshListener:当一个新的 PagingData 提交并显示的时候调用。
  • addLoadStateListener:这个方法同 addDataRefreshListener 方法,它们之间的区别是 addLoadStateListener 方法返回了一个 CombinedLoadStates 的对象,如上图所示。

CombinedLoadStates 是一个数据类,里面有三个成员变量 refresh、prepend 和 append。

1
2
3
ini复制代码val refresh: LoadState = (mediator ?: source).refresh
val prepend: LoadState = (mediator ?: source).prepend
val append: LoadState = (mediator ?: source).append
变量 作用
refresh 在初始化刷新的使用
append 在加载更多的时候使用
prepend 在当前列表头部添加数据的时候使用

refresh、prepend 和 append 都是 LoadState 的对象,LoadState 也是一个密封类,每一个 refresh、prepend 和 append 都对应着三种状态。

变量 作用
Error 表示加载失败
Loading 表示正在加载
NotLoading 表示当前未加载

到这里不得不佩服 Google 什么都替我们想好了,这里需要结合自己的项目实际情况,去定制不同的状态处理。

到这里 Paging3 算是完结了,最后贴一下本文案例 Paging3SimpleWithNetWork 已经上传到 GitHub,最后祝大家周末愉快呀。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 如何在项目中封装 Kotlin + Android Databinding
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

关于String实例的trim()方法学习

发表于 2020-06-20

最近在学习GitHub上的一个项目,看到该项目的登录功能有对前端传入的密码字符串调用了trim()方法,用意在于去除用户输入数据首部和尾部的空格。部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码            // 用户名
String username = loginData.get(getUsernameParameter());
// 密码
String password = loginData.get(getPasswordParameter());
// 预防后面的空指针异常
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
// 除去首尾的空格
username = username.trim();

trim()是String的一个方法,百度了一下发现trim()并不仅仅是去掉字符串“首尾空格”那么简单,以下是学习笔记。

P.S:

参考1:string.trim()究竟去掉了什么?

参考2:String源码中的注释“avoid getfield opcode”

直接上源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码public String trim() {
// len表示实例字符串的长度
int len = value.length;
// st表示一个计数器(游标)
int st = 0;
// 在一个方法中需要大量引用实例域变量的时候,使用方法中的局部变量代替引用可以减少getfield操作的次数,提高性能
char[] val = value;

// 第1个while
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//第2个while
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

观察发现:

  1. 无论第1个还是第2个while都有字符char类型之间的比较,如val[st] <= ' '和val[len - 1] <= ' '
  2. trim()方法的最后调用了subString()方法,说明trim()最后实际上执行的是一个截取字符串的动作

分析:

  1. 我们知道,因为char类型在ASCII等字符编码表中有对应的数值,char类型之间的比较实际上可直接当做ASCII表对应的整数的比较。我们可以先参考(ASCII码和Unicode字符编码的对照表)[ascii.911cha.com/]。通过编码对照表,我们可以得出的结论是trim()方法实际上trim掉(除掉)了字符串两端Unicode编码小于等于32(\u0020)的所有字符。大白话一点,trim()实际上就是去除掉了字符串中所有的ASCII的控制字符(这是具有实际意义的,因为我们基本在键盘上是没办法敲出这些字符的),让字符串仅保留ASCII可显示字符,如’a’、’B’等。
  2. 对于subString()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}

我们发现最终返回的String对象是new出来的,new出来的对象位于Heap内存中,而不在方法区的常量池中。这也就说明了一个结果:当String实例调用了trim()方法之后,返回的将是一个新的对象。


总结:

  1. trim()方法除掉的不仅仅是空格,而是除掉了字符串两端Unicode编码小于等于32(\u0020)的所有字符
  2. trim()调用后,返回的将是一个全新的对象

本文转载自: 掘金

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

高并发编程从入门到精通(一) 本节提要

发表于 2020-06-19

面试中最常被虐的地方一定有并发编程这块知识点,无论你是刚刚入门的大四萌新还是2-3年经验的CRUD怪,也就是说这类问题你最起码会被问3年,何不花时间死磕到底。消除恐惧最好的办法就是面对他,奥利给!(这一系列是本人学习过程中的笔记和总结,并提供调试代码供大家玩耍)

本节提要

本节学习完成,你将会清楚地知道什么是并发编程,并发编程主要解决的问题是什么。并且能够自己完成三种实现线程的方法。(熟悉这块的同学可以选择直接点赞👍完成本章学习哦)

本章代码下载

一、 并发编程主要解决问题是什么

不知道大家有没有遇到和我一样的问题,在还没有真正使用这块知识来开发之前,会把前两年常问的双十一案例两者结合到一起,觉得双十一的高并发就是并发编程的产物。这里我想说的是,双十一案例的高并发必然包含有并发编程的内容,但是并发编程不足以解决双十一这种场景,如要学习双十一解决方案的,可以学习下异步化-队列和缓存等知识点。

由于计算机CUP-内存—磁盘三者之间的速度差异较大,导致硬件资源得不到充分的利用,所以高并发编程要解决的问题是最大效率的利用硬件资源,从而达到程序效率提升的效果。

二、“创建”线程的三种方式

这里为什么要给“创建”打上引号呢?

划重点了

一般情况下我们会说创建线程的方式有三种,一种是创建Thread,一种是实现Runnable接口,一种是使用Futuretask。但是这种说法是不严谨的,经过本案例的学习相信大家自然而然就能得出创建线程只有一种方式那就是构造Thread类。

(1) Thread

通过继承Thread类,并重写run()方法。话不多说直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static class ThreadFirstTime extends Thread {

@Override
public void run() {
while (true) {
System.out.println(this.getName() + "create Thread success!");
}
}
}

public static void main(String[] args) {
//第一种写法通过new ThreadFirstTime()来创建对象
ThreadFirstTime threadFirstTime = new ThreadFirstTime();
//通过Thread.start()来启动线程
threadFirstTime.start();

//第二种 通过把new ThreadFirstTime()对象当作参数传入Thread的有参构造器中
Thread thread = new Thread(new ThreadFirstTime());
thread.start();
}

可以看到通过new一个Thread对象调用其自带的start方法,我们的线程就跑起来了
输出:

1
2
3
复制代码Thread-0create Thread success!
Thread-0create Thread success!
Thread-0create Thread success!

注意这里的start方法,后面我们会就这一方法来做进一步解析和探讨

(2)Runnable

通过实现Runnable接口并实现run()方法来实现线程。代码很简单,直接上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码  public static class RunnableFirstTime implements Runnable {

@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "create Runnable success!");
}
}
}

public static void main(String[] args) {
//创建一个Thread线程,使用RunnableFirstTime
Thread thread = new Thread(new RunnableFirstTime());
//调用 Thread.start()方式启动线程
thread.start();
}

这里通过对比我们发现run()方法中由于实现方式的不同会有些许差异:

ThreadFirstTime是通过继承Thread来实现的所以可以直接使用this关键字,可以看到我们使用this.getName()来获取当前线程的名称。

而我们这边的RunnableFirstTime是通过实现Runnable接口来实现run()方法的所以这边使用Thread.currentThread()来获取当前的线程对象。

这里要提一句Thread.currentThread()很常用,也很好用,大家要多多使用哦!

相同的地方也显而易见我们可以看到main方法中我们都是通过创建Thread对象来创建线程,同时使用Thread.start()方法来启动线程,那是不是印证来我们开头所说的那句断言创建线程只有一种方式那就是构造Thread类,别急我们再来看看Futuretask是不是也是这样的。

(3)FutureTask

FutureTask通过实现Callable接口,并重写call()方法,来实现有返回值的一种实现。直接上代码,然后分三步来讲解FutureTask是如何实现线程的执行单元的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码public static class CallerTask implements Callable<String> {

@Override
public String call() throws Exception {
return Thread.currentThread().getName() + "create FutureTask success!";
}
}

public static void main(String[] args) {
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
//创建线程,调用Thread.start()启动线程
new Thread(futureTask).start();

//获取线程返回结果
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}

第一步:

1
2
3
4
5
6
7
复制代码public static class CallerTask implements Callable<String> {

@Override
public String call() throws Exception {
return Thread.currentThread().getName() + "create FutureTask success!";
}
}

创建任务类,通过实现Callable<String>接口,重写call()方法来实现一个任务类,此处可以类比Runnable接口。但是两者不同的是Runnable接口实现的是run()方法是一个void的方法,但是Callable<String>实现的是call()可以看到给的例子里面返回参数是String,是一个有返回值的方法。

第二步:

1
2
复制代码   //创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());

使用FutureTask的有参构造方法来创建可供Thread管理的异步任务。

第三步:
和前面两种方式一样,通过new Thread()来手动创建一个线程,调用Thread.start()来启动线程。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 //创建线程,调用Thread.start()启动线程
new Thread(futureTask).start();

//获取线程返回结果
try {
String result = futureTask.get();
System.out.println(result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}

到这一步我们可以清楚地看到通过FutureTask实现线程,其线程的创建方式也是通过new Thread()来实现的。所以到这里相信大家应该都理解来开篇说的那个结论了,就是创建线程只有一种方式那就是构造Thread类,其实现方式有三种。

各位面试官大大也注意啦,千万别再问线程创建的方式有哪几种这样的问题啦,小心被人当场手撕哦~

三、new Thread()是如何一穿三的呢?

我们看一下这个方法到底是何方神圣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码/**
* Allocates a new {@code Thread} object. This constructor has the same
* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
* {@code (null, target, gname)}, where {@code gname} is a newly generated
* name. Automatically generated names are of the form
* {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
*
* @param target
* the object whose {@code run} method is invoked when this thread
* is started. If {@code null}, this classes {@code run} method does
* nothing.
*/
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

首先点进去看下这个方法,相信大家都不敢相信,因为入参是Runnable target???

此时不妨把光标定位到当前类的声明处,也就是140行

1
复制代码public class Thread implements Runnable

恍然大悟了吧,原来我们的Thread类也是实现类Runanble接口的。
Runnable自然而然是没问题的,毕竟人家才是正主。

同理我们也能够猜到FutureTask他必然也是实现了Runnable接口了

1
2
3
复制代码public class FutureTask<V> implements RunnableFuture<V> 

public interface RunnableFuture<V> extends Runnable, Future<V>

追踪到类的底层关系,我们可以看到FutureTask实现的是RunnableFuture,RunnableFuture继承的是Runnable和Future。

可能有的小伙伴要问了,java不是单继承,多实现吗?这里为什么是多继承呢?
接口是可以多继承的,这点需要注意
。

在这里又一个巧妙的地方是选择继承而不是实现Runnable,其目的是因为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

public abstract void run();这个抽象方法如果RunnableFuture使用implements来实现的话,后续我们的FutureTask也必须实现run()方法,这与我们的call()就会有冲突,所以在RunnableFuture中选择重写run()在后续的实现类中就不需要来实现run()方法了。

这里说的比较绕,但是希望大家对着代码多读两遍,这是一个细节的点。
读懂这层关系之后我们就会发现FutureTask这个对象即可以通过Callable来实现创建,也可以通过Runnable来实现创建,说通俗一点就是FutureTask即支持run()也支持call()。

1
2
3
复制代码 FutureTask<String> futureTask = new FutureTask<>(new CallerTask());

FutureTask<String> futureTask = new FutureTask(new RunnableFirstTime(),"create FutureTask success!");

对这两者关系感兴趣的同学可以继续追踪看一下这段代码和对应的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码    /**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Runnable}, and arrange that {@code get} will return the
* given result on successful completion.
*
* @param runnable the runnable task
* @param result the result to return on successful completion. If
* you don't need a particular result, consider using
* constructions of the form:
* {@code Future<?> f = new FutureTask<Void>(runnable, null)}
* @throws NullPointerException if the runnable is null
*/
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}

可以发现FutureTask完全可以实现和Runnable一样的效果,但是我们一般不这么玩,一般来说FutureTask的cal()方法才是这个类的作用所在,无返回参数的我们建议直接调用Runnable接口来实现。

记得点赞👍点关注继续和我一起学习后续的内容哦~

本文转载自: 掘金

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

PHP 本地开发终极解决方案

发表于 2020-06-18
  1. requirements

  1. git
  2. docker app
  3. docker-compose
  4. docker login
  5. 可能需要科-学-上-网
  6. root 账号权限
  1. 基础

基于 https://github.com/yeszao/dnmp,参考 https://github.com/guanguans/dnmp-plus

2.1 DNMP

DNMP(Docker + Nginx + MySQL + PHP7/5 + Redis)是一款全功能的 LNMP 一键安装程序。

DNMP 项目特点:

  1. 100% 开源
  2. 100% 遵循 Docker 标准
  3. 支持多版本 PHP 共存,可任意切换(PHP5.4、PHP5.6、PHP7.1、PHP7.2、PHP7.3)
  4. 支持绑定任意多个域名
  5. 支持 HTTPS 和 HTTP/2
  6. PHP 源代码、MySQL 数据、配置文件、日志文件都可在 Host 中直接修改查看
  7. 内置完整 PHP 扩展安装命令
  8. 默认支持 pdo_mysql、mysqli、mbstring、gd、curl、opcache 等常用热门扩展,根据环境灵活配置
  9. 可一键选配常用服务:
    • 多 PHP 版本:PHP5.4、PHP5.6、PHP7.1-7.3
    • Web 服务:Nginx、Openresty
    • 数据库:MySQL5、MySQL8、Redis、memcached、MongoDB、ElasticSearch
    • 消息队列:RabbitMQ
    • 辅助工具:Kibana、Logstash、phpMyAdmin、phpRedisAdmin、AdminMongo
  10. 实际项目中应用,确保 100% 可用
  11. 所有镜像源于 Docker 官方仓库,安全可靠
  12. 一次配置,Windows、Linux、MacOs 皆可用
  13. 支持快速安装扩展命令 install-php-extensions apcu

2.2 dnmp-plus

plus = xhgui + xhprof + tideways

dnmp-plus = PHPer 的一键安装开发环境 + PHP 非侵入式监控平台(优化系统性能、定位 Bug 的神器)

dnmp-plus 在 yeszao 的 DNMP 基础上新增:

  • PHP xhprof 扩展 - Facebook 开发的 PHP 性能追踪及分析工具
  • PHP tideways 扩展 - xhprof 的分支,支持 PHP7
  • PHP mongodb 扩展
  • MongoDB 服务
  • Mongo Express - MongoDB 服务管理系统
  • xhgui - xhprof 分析数据数据的 GUI 系统
  1. 步骤

3.1. clone project

1
复制代码git clone https://gitee.com/zouzhipeng/my-dnmp.git dnmp

3.2. install

1
复制代码cd dnmp && docker-compose up -d

视网络情况,可能需要等待十几分钟。如果无法下载,考虑使用代理。

3.3. Go to your browser and type http://localhost, you will see:

image-20200618221524024

image-20200618221524024

PHP7.4 已可以满足大部分需求,PHP5.x 相关已删除。

3.4 安装 PHP 扩展

PHP 的很多功能都是通过扩展实现,而安装扩展是一个略费时间的过程, 所以,除 PHP 内置扩展外,在 env.sample 文件中我们仅默认安装少量扩展, 如果要安装更多扩展,请打开你的.env 文件修改如下的 PHP 配置, 增加需要的 PHP 扩展:

1
复制代码PHP_EXTENSIONS=pdo_mysql,opcache,redis       # PHP 要安装的扩展列表,英文逗号隔开

需要先修改 docker-compose.yml 的 php 部分,注释掉 image,并开启 build,如下

1
2
3
4
5
6
7
8
9
复制代码  php:
# image: zzpwestlife/dnmp_php_xhgui:v1.0
build:
context: ./services/php
args:
PHP_VERSION: php:${PHP_VERSION}-fpm-alpine
CONTAINER_PACKAGE_URL: ${CONTAINER_PACKAGE_URL}
PHP_EXTENSIONS: ${PHP_EXTENSIONS}
TZ: "$TZ"

然后重新 build PHP 镜像。

1
复制代码docker-compose build php

可用的扩展请看同文件的 env.sample 注释块说明。

3.5 快速安装 php 扩展

  1. 进入容器:
1
2
复制代码docker exec -it php /bin/sh
install-php-extensions extension-name

非常方便。可用扩展列表见 https://github.com/mlocati/docker-php-extension-installer

  1. 使用

4.1. Host 中使用 php 命令行 (php-cli)

如在 mac 中,就不需要再单独安装 php,直接使用 docker 中的 php 即可。

  1. 参考 bash.alias.sample 示例文件,将对应 php cli 函数拷贝到主机的 ~/.bashrc 文件。 (或者 ~/.zshrc,视具体情况而定)
1
2
3
4
5
6
7
8
9
10
11
12
复制代码# php7 cli
php () {
tty=
tty -s && tty=--tty
docker run \
$tty \
--interactive \
--rm \
--volume $PWD:/www:rw \
--workdir /www \
zzpwestlife/dnmp_php_xhgui:v1.0 php "$@"
}

如果重新 build 过镜像,zzpwestlife/dnmp_php_xhgui:v1.0 需要改为 dnmp-php
2. 让文件起效:

1
2
复制代码source ~/.bashrc
# source ~/.zshrc
  1. 然后就可以在主机中执行 php 命令了:
1
2
3
4
5
6
复制代码~ php -v
PHP 7.4.1 (cli) (built: Jan 18 2020 03:27:33) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
with Zend OPcache v7.4.1, Copyright (c), by Zend Technologies
with Xdebug v2.9.2, Copyright (c) 2002-2020, by Derick Rethans

4.2. 使用 composer

方法 1:主机中使用 composer 命令

  1. 确定 composer 缓存的路径。比如,我的 dnmp 下载在 /Users/zouzhipeng/www/work/dnmp 目录,那 composer 的缓存路径就是 /Users/zouzhipeng/www/work/dnmp/data/composer。
  2. 参考 bash.alias.sample 示例文件,将对应 php composer 函数拷贝到主机的 ~/.bashrc (~/.zshrc)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码# php7 composer
composer () {
tty=
tty -s && tty=--tty
docker run \
$tty \
--interactive \
--rm \
--user www-data:www-data \
--volume ~//Users/zouzhipeng/www/work/dnmp/data/composer:/tmp/composer \
--volume $(pwd):/app \
--workdir /app \
zzpwestlife/dnmp_php_xhgui:v1.0 composer "$@"
}

同样,如果重新 build 过镜像,zzpwestlife/dnmp_php_xhgui:v1.0 需要改为 dnmp-php
3. 让文件生效:

1
复制代码source ~/.bashrc
  1. 在主机的任何目录下就能用 composer 了:
1
2
3
复制代码cd ~/dnmp/www/
~ composer -V
Composer version 1.10.7 2020-06-03 10:03:56

方法二:容器内使用 composer 命令

还有另外一种方式,就是进入容器,再执行 composer 命令,以 PHP7 容器为例:

1
2
3
复制代码docker exec -it php /bin/sh
cd /www/localhost
composer -V
  1. 使用 Log

Log 文件生成的位置依赖于 conf 下各 log 配置的值。

5.1 nginx 日志

Nginx 日志是我们用得最多的日志,所以我们单独放在根目录 log 下。

log 会目录映射 nginx 容器的 /var/log/nginx 目录,所以在 Nginx 配置文件中,需要输出 log 的位置,我们需要配置到 /var/log/nginx 目录,如:

1
复制代码error_log  /var/log/nginx/nginx.localhost.error.log  warn;

5.2 PHP-FPM 日志

大部分情况下,PHP-FPM 的日志都会输出到 nginx 的日志中,所以不需要额外配置。

另外,建议直接在 PHP 中打开错误日志:

1
2
3
复制代码error_reporting(E_ALL);
ini_set('error_reporting', 'on');
ini_set('display_errors', 'on');

如果确实需要,可按一下步骤开启(在容器中)。

  1. 进入容器,创建日志文件并修改权限:
1
2
3
4
5
复制代码$ docker exec -it php /bin/sh
$ mkdir /var/log/php
$ cd /var/log/php
$ touch php-fpm.error.log
$ chmod a+w php-fpm.error.log
  1. 主机上打开并修改 PHP-FPM 的配置文件
1
复制代码conf/php-fpm.conf

,找到如下一行,删除注释,并改值为:

1
复制代码php_admin_value[error_log] = /var/log/php/php-fpm.error.log
  1. 重启 PHP-FPM 容器。

5.3 MySQL 日志

因为 MySQL 容器中的 MySQL 使用的是 mysql 用户启动,它无法自行在 /var/log 下的增加日志文件。所以,我们把 MySQL 的日志放在与 data 一样的目录,即项目的 mysql 目录下,对应容器中的 /var/lib/mysql/ 目录。

1
2
复制代码slow-query-log-file     = /var/lib/mysql/mysql.slow.log
log-error = /var/lib/mysql/mysql.error.log

以上是 mysql.conf 中的日志文件的配置。

  1. 数据库管理

本项目默认在 docker-compose.yml 中开启了用于 MySQL 在线管理的 phpMyAdmin,以及用于 redis 在线管理的 phpRedisAdmin,可以根据需要修改或删除。

6.1 phpMyAdmin

phpMyAdmin 容器映射到主机的端口地址是:8080,所以主机上访问 phpMyAdmin 的地址是:

1
复制代码http://localhost:8080

MySQL 连接信息:

  • host:(本项目的 MySQL 容器网络)
  • port:3306
  • username:(手动在 phpmyadmin 界面输入)
  • password:(手动在 phpmyadmin 界面输入)

image-20200618223738403

image-20200618223738403

image-20200618223807184

image-20200618223807184

Mac 上建议使用 Sequel Pro 管理 MySQL。

6.2 phpRedisAdmin

phpRedisAdmin 容器映射到主机的端口地址是:8081,所以主机上访问 phpMyAdmin 的地址是:

1
复制代码http://localhost:8081

Redis 连接信息如下:

  • host: (本项目的 Redis 容器网络)
  • port: 6379

image-20200618223843852

image-20200618223843852

Mac 上可以使用 AnotherRedisDesktopManager 管理 redis

image-20200618224945285

image-20200618224945285

  1. 在正式环境中安全使用

要在正式环境中使用,请:

  1. 在 php.ini 中关闭 XDebug 调试
  2. 增强 MySQL 数据库访问的安全策略
  3. 增强 redis 访问的安全策略

8 常见问题

8.1 如何在 PHP 代码中使用 curl?

参考这个 issue:https://github.com/yeszao/dnmp/issues/91

8.2 Docker 使用 cron 定时任务

Docker 使用 cron 定时任务

8.3 Docker 容器时间

容器时间在.env 文件中配置 TZ 变量,所有支持的时区请看时区列表・维基百科或者 PHP 所支持的时区列表・PHP 官网。

8.4 如何连接 MySQL 和 Redis 服务器

这要分两种情况,

第一种情况,在 PHP 代码中。

1
2
3
4
5
6
复制代码// 连接MySQL
$dbh = new PDO('mysql:host=mysql;dbname=mysql', 'root', '123456');

// 连接Redis
$redis = new Redis();
$redis->connect('redis', 6379);

因为容器与容器是 expose 端口联通的,而且在同一个 networks 下,所以连接的 host 参数直接用容器名称,port 参数就是容器内部的端口。更多请参考《docker-compose ports 和 expose 的区别》。

第二种情况,在主机中通过命令行或者 Navicat 等工具连接。主机要连接 mysql 和 redis 的话,要求容器必须经过 ports 把端口映射到主机了。以 mysql 为例,docker-compose.yml 文件中有这样的 ports 配置:3306:3306,就是主机的 3306 和容器的 3306 端口形成了映射,所以我们可以这样连接:

1
2
复制代码$ mysql -h127.0.0.1 -uroot -p123456 -P3306
$ redis-cli -h127.0.0.1

这里 host 参数不能用 localhost 是因为它默认是通过 sock 文件与 mysql 通信,而容器与主机文件系统已经隔离,所以需要通过 TCP 方式连接,所以需要指定 IP。

8.5 容器内的 php 如何连接宿主机 MySQL

  1. 宿主机执行 ifconfig docker0 得到 inet 就是要连接的 ip 地址 (无法验证)
1
2
3
4
复制代码$ ifconfig docker0
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
...
  1. 运行宿主机 Mysql 命令行
1
2
3
4
5
6
7
复制代码 mysql>GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;
mysql>flush privileges;
// 其中各字符的含义:
// *.* 对任意数据库任意表有效
// "root" "123456" 是数据库用户名和密码
// '%' 允许访问数据库的IP地址,%意思是任意IP,也可以指定IP
// flush privileges 刷新权限信息
  1. 接着直接 php 容器使用 172.0.17.1:3306 连接即可
  1. xhgui

9.1 安装

1
2
复制代码cd www/xhgui-branch
composer install

修改 xhgui-branch 配置文件 www/xhgui-branch/config/config.default.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码<?php
return [
...
'debug' => true, // 改为true,便于调试
'mode' => 'development',
...
'extension' => 'tideways', // 改为支持 PHP7 的 tideways
...
'save.handler' => 'mongodb',
'db.host' => 'mongodb://mongo:27017', // 127.0.0.1 改为 mongo
'db.options' => [ // .env 中配置的 mongodb 账号密码
'username' => 'root',
'password' => '123456',
],
...
];

hosts 文件中增加

1
复制代码127.0.0.1             xhgui.test

浏览器访问 xhgui.test

image-20200618225249811

image-20200618225249811

  1. 如何添加项目

1
2
3
复制代码cd www
mkdir -p laravel/public
vim laravel/public/index.php

services/nginx/conf.d 添加一个配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码server {
listen 80;
server_name laravel.test;
root /www/laravel/public;
index index.php;

location / {
try_files $uri $uri/ /index.php$is_args$args;
}

access_log /dev/null;
#access_log /var/log/nginx/nginx.laravel.access.log main;
error_log /var/log/nginx/nginx.laravel.error.log warn;

location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Run our specified PHP script before executing the main program
fastcgi_param PHP_VALUE "auto_prepend_file=/www/xhgui-branch/external/header.php";
}
}

重启 nginx

1
复制代码$ docker-compose restart nginx

浏览器访问 http://laravel.test,再访问 xhgui.test,此时已经有了内容,愉快的查看项目的性能追踪及分析吧

  1. xdebug 断点调试

使用 PHPstorm 打开项目。Preferences->Languages & Frameworks ->PHP 的 CLI Interpreter ,点击右侧的三个点,点击弹出的窗口中左上角加号,选择第一个

Jietu20200618-230202

Jietu20200618-230202

下一步要选择 docker,而不是 docker compose。 选择所需的 php 容器。点击 OK

image-20200618230354250

image-20200618230354250

回到上一步的窗口。这里可以显示 php 及扩展相关信息。

Jietu20200618-230740

Jietu20200618-230740

保存。回到上一步对话窗口。

设置目录映射。先设置 Docker container。

Jietu20200618-230945

Jietu20200618-230945

然后设置 path mapping,与 docker container 一致即可。保存。

打开 Run -> Edit configuration 对话窗口。如图,按顺序配置。

左上角,添加一个 PHP Web Page.

点击 Server 后面的三个点。配置一个 server,并设置好 Host 和 Path mapping

Jietu20200618-231421

Jietu20200618-231421

保存,回到上一级会话窗口。

点击 validate,可以看到一排对勾。

Jietu20200618-231625

Jietu20200618-231625

配置完成,在代码中打断点,打开小电话,请求页面或接口,就可以开始调试了。

image-20200618231802311

image-20200618231802311

本文转载自: 掘金

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

Mybatis源码分析(四)Mybatis执行sql的四大组

发表于 2020-06-18

本文源代码来源于mybatis-spring-boot-starter的2.1.2版本

  SQL语句的执行涉及各个组件,其中比较重要的是Executor,StatementHandler,ParameterHandler和ResultSetHandler。

  Executor对象在创建Configuration对象的时候创建,并且缓存在Configuration对象里,负责管理一级缓存和二级缓存,并提供是事务管理的相关操作。Executor对象的主要功能是调用StatementHandler访问数据库,并将查询结果存入缓存中(如果配置了缓存的话)。StatementHandler首先通过ParammeterHandler完成SQL的实参绑定,然后通过java.sql.Statement对象执行sql语句并得到结果集ResultSet,最后通过ResultSetHandler完成结果集的映射,得到对象并返回。

Executor

1.1 类图

每一个 SqlSession 都会拥有一个Executor 对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为 JDBC 中 Statement 的封装版。也可以理解为 SQL 的执行引擎,要干活总得有一个发起人吧,可以把 Executor 理解为发起人的角色。

如上图所示,位于继承体系最顶层的是Executor执行器,它有两个实现类,分别是和BaseExecutor和CachingExecutor。

  • BaseExecutor

  BaseExecutor是一个抽象类,这种通过抽象的实现接口的方式是的体现,是Executor 的默认实现,实现了大部分 Executor 接口定义的功能,关于查询更新的具体实现由其子类实现。️BaseExecutor 的子类有四个,分别是 SimpleExecutor、ReuseExecutor 、CloseExecutor 和 BatchExecutor。

  1. SimpleExecutor

  简单执行器,是 MyBatis 中默认使用的执行器,每执行一次 update 或 select,就开启一个Statement 对象,用完就直接关闭 Statement 对象(可以是 Statement 或者是 PreparedStatment 对象)

  1. ReuseExecutor

  可重用执行器,这里的重用指的是重复使用Statement,它会在内部使用一个 Map 把创建的Statement 都缓存起来,每次执行 SQL 命令的时候,都会去判断是否存在基于该 SQL 的 Statement 对象,如果存在Statement 对象并且对应的 connection 还没有关闭的情况下就继续使用之前的 Statement 对象,并将其缓存起来。因为每一个 SqlSession都有一个新的 Executor对象,所以我们缓存在 ReuseExecutor 上的 Statement作用域是同一个 SqlSession。

  1. CloseExecutor

  表示一个已关闭的Executor执行期

  1. BatchExecutor

  批处理执行器,用于将多个 SQL 一次性输出到数据库

  • CachingExecutor

  缓存执行器,先从缓存中查询结果,如果存在就返回之前的结果;如果不存在,再委托给Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器。

1.2 Executor的选择和创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//创建Executor
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

SqlSessionFactory通过Configuration来创建sqlSession,Executor也是在这个时候初始化,我们来看下configuration.newExecutor()这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
//不指定就用简单执行器
executor = new SimpleExecutor(this, transaction);
}
//如果cacheEnabled为true,则创建CachingExecutor,然后在其内部持有上面创建的Executor
//cacheEnabled默认为true,则默认创建的Executor为CachingExecutor,并且其内部包裹着SimpleExecutor。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
//使用InterceptorChain.pluginAll为executor创建代理对象。Mybatis插件机制。
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}

ExecutorType类型可以通过xml标签和JavaApi进行赋值,默认为ExecutorType.SIMPLE。

Mybatis插件机制会在其他系列文章里面讲解,这里就不过多介绍了。

1.3 Executor的执行流程

我们从SqlSession的selectList方法入手,其实他们的调用链路都差不多。

1
2
3
4
5
6
7
8
9
10
11
复制代码@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

上面说到cacheEnabled默认为true,则默认创建的Executor为CachingExecutor,并且其内部包裹着SimpleExecutor。所以这里executor还是CachingExecutor,我们来看下CachingExecutor的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码  @Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//是否有缓存
Cache cache = ms.getCache();
if (cache != null) {
//创建缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
//执行SimpleExecutor的query方法
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//执行SimpleExecutor的query方法
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这里的delegate就是SimpleExecutor,也就是说最终还是会执行SimpleExecutor的query实现。到这里,执行器所做的工作就完事了,Executor 会把后续的工作交给继续执行。下面我们来认识一下StatementHandler。

StatementHandler

StatementHandler主要负责操作Statement对象与数据库进行交互,在这里我们先回顾一下原生JDBC的相关知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码        //1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2 获得连接
String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,"root", "root");
//3获得语句执行者
Statement st = conn.createStatement();
//4执行SQL语句
ResultSet rs = st.executeQuery("select * from role");
//5处理结果集
while(rs.next()){
// 获得一行数据
Integer id = rs.getInt("id");
String name = rs.getString("name");
System.out.println(id + " , " + name);
}
//6释放资源
rs.close();
st.close();
conn.close();

JDBC通过java.sql.Connection对象创建Statement对象,Statement对象的execute方法就是执行SQL语句的入口。那么对于Mybtais的StatementHandler是用于管理Statement对象的。

2.1 类图

有木有感觉这个继承关系和Executor极为相似,顶层负接口分别有两个实现BaseStatementHandler和RoutingStatementHandler,而BaseStatementHandler分别有三个实现类SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。
我们不妨先来看下StatementHandler定义的方法:

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
复制代码ublic interface StatementHandler {
//创建Statement对象,即该方法会通过Connection对象创建Statement对象。
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
//对Statement对象参数化,特别是PreapreStatement对象。
void parameterize(Statement statement)
throws SQLException;
//批量执行SQL
void batch(Statement statement)
throws SQLException;
//更新
int update(Statement statement)
throws SQLException;
//查询
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
//根据下标查询
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
// 获取SQl语句
BoundSql getBoundSql();
//获取参数处理器
ParameterHandler getParameterHandler();

}
  • BaseStatementHandler

  它本身是一个抽象类,用于简化StatementHandler 接口实现的难度,属于适配器设计模式体现,它主要有三个实现类:

  1. SimpleStatementHandler

  java.sql.Statement对象创建处理器,管理 Statement 对象并向数据库中推送不需要预编译的SQL语句。

  1. PreparedStatementHandler

  java.sql.PrepareStatement对象的创建处理器,管理Statement对象并向数据中推送需要预编译的SQL语句。

  注意:SimpleStatementHandler 和 PreparedStatementHandler 的区别是 SQL 语句是否包含变量。是否通过外部进行参数传入。SimpleStatementHandler 用于执行没有任何参数传入的 SQL,PreparedStatementHandler 需要对外部传入的变量和参数进行提前参数绑定和赋值。

  1. CallableStatementHandler

  java.sql.CallableStatement对象的创建处理器,管理 Statement 对象并调用数据库中的存储过程。

  • RoutingStatementHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码 public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

switch (ms.getStatementType()) {
case STATEMENT:
delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case PREPARED:
delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
case CALLABLE:
delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
break;
default:
throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
}

}

  与CachExecutor相似,RoutingStatementHandler 并没有对 Statement 对象进行使用,只是根据StatementType 来创建一个代理,代理的就是对应Handler的三种实现类。在MyBatis工作时,使用的StatementHandler 接口对象实际上就是 RoutingStatementHandler对象。

2.2 StatementHandler的选择和创建

以查询为例,前面说到Executor在执行时会先查询缓存在走数据库,我们顺着**queryFromDatabase()方法的doQuery()**方法可以发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码 @Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
//创建StatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

2.2.1 configuration.newStatementHandler

1
2
3
4
5
6
7
复制代码 public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
//
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
//插件机制
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}

这里会创建一个RoutingStatementHandler对象,我们刚才说过了,它会根据StatementType来创建对应的Statement对象。StatementType是MappedStatement的一个属性,他在bulid的时候默认为StatementType.PREPARED。
PreparedStatementHandler在构建的时候会调用其父类BaseStatementHandlerde的构造函数。ParameterHandler和ResultSetHandler也是在这里创建的,我们来看看。

2.2.2 BaseStatementHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码  protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
this.configuration = mappedStatement.getConfiguration();
this.executor = executor;
this.mappedStatement = mappedStatement;
this.rowBounds = rowBounds;

this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
this.objectFactory = configuration.getObjectFactory();

if (boundSql == null) { // issue #435, get the key before calculating the statement
generateKeys(parameterObject);
boundSql = mappedStatement.getBoundSql(parameterObject);
}

this.boundSql = boundSql;
//构建parameterHandler
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
//构建resultSetHandler
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

ParameterHandler

3.1 类图

相比于其他的组件就简单很多了,ParameterHandler 译为参数处理器,负责为 PreparedStatement 的 sql 语句参数动态赋值,这个接口很简单只有两个方法:

  • getParameterObject:用于读取参数
  • setParameters: 用于对 PreparedStatement 的参数赋值

3.2 ParameterHandler的创建

1
2
3
4
5
复制代码public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}

ResultSetHandler

4.1 类图

ResultSetHandler 也很简单,它只有一个实现类DefaultResultSetHandler,主要负责处理两件事

  • 处理 Statement 执行后产生的结果集,生成结果列表。
  • 处理存储过程执行后的输出参数

4.2 ResultSetHandler的创建

1
2
3
4
5
6
复制代码public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
ResultHandler resultHandler, BoundSql boundSql) {
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}

本文仅对Mybatis执行sql的组件做一个入门级的介绍,后面我们会对执行sql的过程作出详细的讲解。

本文转载自: 掘金

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

过来人告诉你,去工作前最好还是学学Git

发表于 2020-06-18

前言

只有光头才能变强。

文本已收录至我的GitHub精选文章,欢迎Star:github.com/ZhongFuChen…

之前遇到过很多同学私信问我:「三歪,我马上要实习了,我要在实习前学些什么做准备啊?」

三歪在实习之前也同样问过自己当时的部门老大。

如果再给我一次机会,我会先去花点时间去学学Git。

Git我相信大家对它应该不陌生吧?但凡用过GitHub的同学应该多多少少都会了解一下Git

不知道当时大家学Git的时候是看哪个教程的,我看的是廖雪峰老师的Git系列的。

(别看到廖雪峰就以为是广告了啊,哈哈哈哈,这篇纯原创分享)

分享一下三歪的经历

刚实习的时候,一直都忙着看各种东西。有一天,我学长说:我看你也学了一些基础了,我们来看看公司的代码吧,看看我们生产环境是怎么做的。

于是我学长丢了一个Git链接给三歪

1
复制代码https://github.com/ZhongFuCheng3y/3y.git

那三歪做了什么?三歪去IDEA下把这个Git给Clone下来:

我用Clone完了以后,我学长又补了一句:这个项目不是用master分支的哦,你切换一下分支。

三歪:啥?切换分支?咋整?我忘了。

我学长看了下我,貌似不咋会切换分支,就说:“我来吧”。

于是在命令行终端一顿操作后,对三歪说:“好了”

三歪:“我对Git不是很熟悉,之前一直都是在IDEA上操作的。你们一般用命令行多还是图形界面的多呀?”

我学长:“这没什么,反正工具这东西,学学就行,不是什么大问题。也没必要说很仔细去学它,就工具嘛”

三歪:“嗯”

时间飞逝,又过了一段时间…

三歪被分配了一个需求,于是就需要新建分支去做这个需求了。所有的标准应用线上走的是master分支,公司通过一个发布系统来控制发布版本、以及整套上下线的流程。

于是我要先在发布系统里边新建Git分支:

完了以后,我就在IDEA界面上选择那个被我新建完的分支

但发现我死活找不到…于是我就问我学长:我在发布系统里边新建了分支,为什么在IDEA上找不到啊?

学长:“怎么会呢,我看看”。

找了一会,他问我:“你fetch 过了吗?”

三歪:“啥?”

于是他拿着我的电脑,打开了终端,又以是命令行的方式敲了一顿,问我:“这是不是你新建的分支?“

三歪点了点头,于是我学长说:”好了,你再看看“。

后来发现,新建完远程分支,如果在IDEA上要能感知到,可以在pull界面上刷新一下,那就能找到了。

也不是说命令行一定会就比界面牛逼,其实IDEA的Git功能也做得挺好的。现在我都是混合使用,一些操作用命令行,一些操作用IDEA快捷键。

我commit和push的时候就喜欢用快捷键。command+k和command +shift+k我就感觉比敲命令要快不少。

这些都是个人习惯的问题,也无对错之分,怎么方便怎么来。

其实也不是所有的系统都会走发布系统的(有标准应用,非标准应用)。如果要自己写一个启动的脚本,一般我们会做些什么?无非就是用Git拉最新的代码,然后用maven打个包,然后启动。

理解Git

如果你看过上一篇《三歪给女朋友讲解什么是Git》应该能大概了解什么是Git了。

其实我觉得学Git主要理解工作区 -> 暂存区->仓库 这几个概念。

我们使用Git其实绝大部分的操作都是在本地上完成的,比如说add 和commit。

只有我们push的时候,才会把本地完成好的内容推到远程仓库上

通过上一篇文章我们知道在每个人的本地都有完整的历史版本,所以我们可以在本地就能穿梭到不同的版本,然后将修改之后的代码再重新提交到远程仓库上。

所谓的工作区实际上就是我们真正的的本地目录。

我们在本地添加文件后,需要add到暂存区,文件一旦被add到了暂存区,意味着Git能追踪到这个文件。

当我们修改到一定程度之后,我们会执行一次提交commit,在提交的时候我们会”备注“自己这次的提交修改了什么内容。

一次commit在Git就是一个版本,Git是版本控制的软件,我们可以随意穿梭到任何的版本中,修改代码。

暂存区是这么一个概念呢?

暂存区就像购物车,没到付款的时候你都不确定购物车里的东西全部都是要的。每拿一件商品就付一次款,那麻烦可大了。

从宏观上看,Git其实有本地和远程的概念,只是本地又分了工作区、暂存区、本地仓库。再次强调:我们操作几乎都是在本地完成,每个人的本地都会有所有历史版本信息。

我们一般会新建分支去支持每一次的修改。

其实分支这个概念也挺好理解的:我们需要并行开发,同时我们又不关心对方改的是什么内容,改的是什么文件。因此我们需要在自己的专属环境下去修改内容,只要把最终修改完后的内容合并到一个主分支就OK了。

假设三歪做完了,经过校验通过后,把自己的代码merge(合并)到origin/master分支后,然后就发布上线啦。

随后,鸡蛋也做完了,自己的分支校验完了以后,他此时也想把自己的代码合并到origin/master。不料,他改的代码跟三歪改的代码有冲突了(Git不知道选择谁的的代码),那鸡蛋只能手动merge了。

综合来看,我们使用Git大多数的场景就是各自分支开发,然后各自在本地commit(提交),最后汇总到master分支。

所以,我们学Git大多数就学怎么实现分支的增删改、切换以及版本的穿梭。

学习Git的小tips:

Unix/Linux 命令中,- 后一般跟短命令选项(通常是单字母,也有一些命令是例外的),-- 后一般跟长命令选项。如果只有一个单独的--,后面不紧跟任何选项,则表示命令选项结束,后续的都作为命令的参数而不是选项。

例如:git checkout -- filename filename作为git checkout 的参数,而不是选项。

日常Git使用场景

一、如果这个项目的代码我们在本地还没有,我们先去GitLab里边找对应的Git地址,然后Clone到本地:

1
复制代码git clone https://github.com/ZhongFuCheng3y/3y.git

二、接到了新的需求,我们要新建一个分支,然后基于这个分支去开发:

1
复制代码git checkout -b feature/sanwaiAddLog

在开发的时候,我们肯定会有两个操作:

  • 在原来的基础上添加新的文件
  • 在原有的文件上修改

三、不管怎么样,等我们做到一定程度了,我们都会提交代码。如果我们添加了新的文件,我们需要先add,然后再commit

1
2
复制代码git add .
git commit -m "try to commit files to GitHub, i am java3y"

四、假设我们一切顺利,在没人打扰的情况下已经写好了代码了,然后我们会把自己的分支push到远程仓库

1
复制代码git push

五、假设我们写到一半,其他小伙伴已经把他的代码merge到主分支了,我们也需要把他最新的 代码给pull拉取下来(可以 git fetch + git merge 替代)。

1
复制代码git pull

如果没有冲突,那git就会把他的代码给merge到我当前的分支上。如果有冲突,Git会提醒我去手动解决一下冲突。

六、假设我们写到一半了,现在工作区的代码都已经commit了。此时同事说要不帮忙一起排查一个问题,同事一般用的是自己分支,于是就得问他:你用的哪个分支啊?于是得把他的分支给拉下来,看看他的代码哪儿有问题

1
2
复制代码git fecth -- 手动拉取远程仓库更新的信息
git checkout 分支名 -- 切换到他的分支

现在切换到他的分支,相当于你的环境跟他的环境是一模一样的,于是就可以愉快地一起看Bug了。

七、假设我们写到一半了,现在工作区的代码还没commit。现在有同事说要排查问题或者一个新的Bug被发现了,要紧急切换到其他的分支。现在我又不想commit(我就写了一半,编译还报着错误,没理由让我commit吧)。

这时,我会把工作区的代码先stash到暂存区给保存起来,然后就可以愉快地切换其他的分支了。

1
复制代码git stash

等我解决完另一个bug或者帮别人看完问题了,我再把刚刚保存在暂存区的代码给捞出来,继续干活

1
复制代码git stash pop

八、我一直在修Bug,现在的分支已经被我搞得人摸鬼样了,我非常难受,甚至不知道自己在这个过程中改了多少东西了。

思路已经完全被打乱了,我想回到一个稳定的commit重新出发,重来吧(通过下面的命令,把工作区的代码都改成对应commit的代码了)。

1
复制代码git reset --hard  版本号

那我怎么找到版本号呢?Git也是有日志的:

1
复制代码git log --pretty=oneline

常用的Git命令

查看Git工作区、暂存区的变更情况(可以知道哪些没有commit、哪些没有被Git追踪):git status

拉取远程最新的变更到本地:git fetch

切换分支:git checkout 分支名

将代码还原到某个版本(包括工作目录):git reset --hard 版本号

查看Git的提交(commit)记录:git log

将代码还原到某个版本后,后悔了,想重新回去,但在提交记录已经找不到了。git reset --hard 把reset 之后的 commit都给抹杀掉了。找到最近的执行Git命令:git reflog

还原到某个版本了,现在我为了稳健,不想再原来的分支上修改了,再新建一个分支吧(-b 参数把当前分支切换到了要创建的分支上):git checkout -b 分支名

我们把上一次还是”相对稳健“的分支合并到我新建的分支上:git merge 分支

突然想看看现在有多少个分支:git branch -a

新增几个文件了,随手git add一下吧

改得差不多了,随手git commit -m一下吧,最好还是写好备注,不然以后等改多了,你都不知道你改了什么啦。

改完了,提交到远程吧:git push

想把远程分支最新的代码给拉下来,然后合并到本地上。我们可以用git fetch和git merge来实现,也可以通过git pull来实现。一般我用的都是git fetch+git merge,这样会更加可控一些

有的时候,本地分支在master分支,然后忘了切其他的分支去修改,直接在master改了,然后也push到远程了。等你发现的时候,你会真的想骂自己。

咋办?最简单的办法其实我们还是可以git reset --hard到对应的版本,然后将其修改或者复原,再强制提交到master分支:git push -u origin/master -f

三歪瞎扯

在这篇文章中,我列出的Git常用的命令其实并不多吧。

像很多博客讲的diff、tag、config之类的命令我都没有讲,我这边现实开发时这些命令也没怎么用过…

如果觉得我说漏的,可以在评论区补充,一起学习。

其实现在IDEA也很强大,很多时候都可以配合IDEA给我们提供的Git去做很多事。有的场景敲命令会比较方便,有的时候就直接图形化界面就比较方便。

就diff这个功能而言, 肯定还是图形界面好用一些吧(至少我是这样认为的

IDEA配合一些快捷键,使用Git也能爽得飞起。Git始终也只是一个工具,如果你有兴趣可以了解它的实现(我觉得大部分人可能不知道它是怎么实现的);

如果没兴趣看它的实现,了解它是怎么使用的,也足够应付日常的开发场景了。

总的来说,现在的互联网公司大多数还是用Git的,Git本身使用上其实不难,只要理解了Git是干嘛的,它有个本地仓库的概念,它可以来回穿梭各种版本,然后将本地的信息提交到远程,跟着教程把常用的命令敲敲也差不多了。

如果实在是不懂,也别慌(我都给你们打了个样了);主动认怂,虚心求教,同事们都不会嫌弃你的。

如果实习之前不知道要准备什么去公司,要是对Git不了解,我觉得Git可以有占一席之位。

更多Git命令和参考资料:

  • github.com/xjh22222228…
  • juejin.cn/post/684490…
  • www.liaoxuefeng.com/wiki/896043…

各类知识点总结

下面的文章都有对应的原创精美PDF,在持续更新中,可以来找我催更~

  • 92页的Mybatis
  • 129页的多线程
  • 141页的Servlet
  • 158页的JSP
  • 76页的集合
  • 64页的JDBC
  • 105页的数据结构和算法
  • 142页的Spring
  • 58页的过滤器和监听器
  • 30页的HTTP
  • 42页的SpringMVC
  • Hibernate
  • AJAX
  • Redis
  • ……

涵盖Java后端所有知识点的开源项目(已有8K+ star):

  • GitHub
  • Gitee访问更快

我是三歪,一个想要变强的男人,感谢大家的点赞收藏和转发,下期见。给三歪点个赞,对三歪真的非常重要!

本文转载自: 掘金

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

Spring Boot经典入门教程 前言 一认识Sprin

发表于 2020-06-18

前言

Springboot是现在学习Java开发必备的一个技术点。讲真,这是一个非常简单的东西,只要花一点时间都可以非常愉快的把他用起来。但是现在教程一般都是两种,一种是使用idea直接创建就开始用,导致感觉懂了,但是又有很多细节流失。另一种是先讲大篇原理,半天不入门,让很多初学者摸不到头脑。 所以我想从使用层面入手,但是不丢失细节的方式来带大家入门。

一.认识SpringBoot

  • 为简化Spring项目配置而生
  • 使用maven的方式对Spring应用开发进行进一步封装和简化
  • 为了简化spring应用搭建,开发,部署,监控的开发工具
  • 官网:spring.io/projects/sp…

二.Maven的父子项目认识

本节与SpringBoot无关,已经了解Maven父子关系的同学可以忽略本章

咱们刚才说了,SpringBoot是使用maven(注:也可以使用Gradle)的方式对Spring应用开发进行进一步的封装和简化。所以咱们在学习SpringBoot前需要学习Maven,而在练习前咱们会创建多个练习demo,因此,在这里需要先进行Maven父子模块讲解(已经了解Maven父子模块可以忽略本章)

  • idea只能创建一个项目,所以咱们会以模块的方式来进行项目的创建
  • 咱们会先创建一个父项目,然后在里面创建多个子模块

2.1 创建一个普通Maven项目

  • 开发工具使用idea

2.1.1 创建普通的maven项目(父项目)

  • 取名springboot-parent

2.1.2 创建子模块项目

  • 取名springboot-hello-01

2.2 父子模块分析

主要是分析两个pom.xml中的内容

2.2.1 父模块pom.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
复制代码<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--组id-->
<groupId>cn.itsource</groupId>
<!--模块名称-->
<artifactId>springboot-parent</artifactId>
<!--
packaging
jar === 当前项目打成jar包
war === 当前项目打成war包
pom === 当前项目不写java代码,权代表用于管理jar包
maven-plugin === 当前项目用于开发插件使用(暂时不用管)
-->
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>

<!--父项目中管理的所有子项目模块-->
<modules>
<!--管理的子项目模块,注意名称和子模块名称保持一致-->
<module>springboot-hello-01</module>
</modules>


</project>

2.2.2 子模块pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!--
当前子模块的pom.xml中没有声音自己的版本与主id
通过parent 引入父模块中的内容(这里是继承关系)
-->
<parent>
<artifactId>springboot-parent</artifactId>
<groupId>cn.itsource</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!--子模块的名称-->
<artifactId>springboot-hello-01</artifactId>
</project>

三.Hello,SpringBoot

3.1 继承springboot的父依赖

  • springboot为咱们准备好了相关依赖jar包(下面代码直接拷备使用即可)
  • pom.xml是单继承的结构,所以我们在父pom.xml中继承父依赖
  • 父依赖中已经声明了很多现在可用的jar包(大家可看源码分析)
  • dependencyManagement:只声明 不引用
1
2
3
4
5
复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>

3.2 子pom.xml中添加依赖

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.3 创建controller

1
2
3
4
5
6
7
8
9
10
复制代码@Controller
@RequestMapping("/hello")
public class HelloController {

@RequestMapping("/01")
@ResponseBody
public String hello01(){
return "hello springboot";
}
}

3.4 创建启动类

特别注意:启动类必需在外层

1
2
3
4
5
6
7
8
9
10
复制代码//申明我当前是一个SpringBoot的应用
@SpringBootApplication
public class ApplicationConfig {

public static void main(String[] args) {
// 注:这里传入的字段码对象,必需是声明了@SpringBootApplication的类
//启动SpringBoot程序
SpringApplication.run(ApplicationConfig.class);
}
}

3.5 注意事项(疑问)

  1. 为什么要继承spring-boot-starter-parent
    • 父项目准备了很多应用的插件与jar包,子项目可以直接引用即可(方便开发)
  2. 当前项目引入 spring-boot-starter-web是什么意思?
    • 在引入后就会导入spring运行web项目的所有jar包(如spring,日志,mvc包等等)
    • springboot有组合包的概念,专门用于简化maven的导包
    • springboot提供包的格式: spring-boot-starter-xxx
  3. 居然一个主方法启动了tomcat
    • spring-boot-starter-web内嵌了一个tomcat插件
  4. 为什么主方法运行后,应用程序就启动了
    • 初始化运行程序中的所有bean对象(只有扫描它所有的包及其子包的java对象)
    • 自动装配springmvc的相关代码与配置(有默认的配置,我们以后可以修改)
    • 初始化spring容器
    • 把当前应用打成一个jar包放到tomcat中运行

四.SpringBoot三种运行方式

4.1 直接在工具中运行main方法

最简单,咱们平时开发时也就这样直接运行

4.2 插件运行

  1. 引入插件
1
2
3
4
5
6
7
8
复制代码<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
  1. 运行项目

4.3 打包运行

注:打包运行必需要引入插件 咱们以后开发项目放到服务器中运行,就可以使用这种方式

  • 当前位置打开cmd,并且输入java -jar springboot-hello-01-1.0-SNAPSHOT.jar

五.热部署方案

  • 在pom.xml中加上热部署插件(代码如下)
  • 修改代码后按 ctrl+f9 进行编译
1
2
3
4
5
6
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</dependency>

六.SpringBoot配置文件

6.1 springboot配置文件认识

  • 默认配置文件会在resources中
  • 配置文件有两种(我们任选一种即可)
    • application.properties
    • application.yml

6.2 application.properties

properties的配置咱们以前都已经学过,这里简单看一下端口修改的配置即可

1
2
复制代码server.port=80
server.servlet.path=/haha

6.3 application.yml的配置

yml是一种更加优雅的配置方式(有层次结构),之后咱们都直接使用yml的方式来做配置

6.3.1 基本用法

  • 以空格的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的
  • k:(空格)v:表示一对键值对(空格必须有)
  • 属性和值也是大小写敏感
1
2
3
4
复制代码server:
port: 8088
servlet:
path: /haha

6.3.2 字符串的字面量

  • 字符串不需要加引号
  • 双引号不会对串中转义字符进行转义
    • name: "zhangsan \n lisi":输出;zhangsan 换行 lisi
  • 单引号转义特殊字符
    • name: "zhangsan \n lisi":输出;zhangsan \n lisi

注:以后配置文件中可能还会写数组,集合等,后面的课程涉及到会单独讲解,大家也可以自行在网上进行查找

6.4 多环境支持方案

咱们以后可能会遇到一个项目多个环境(开发,测试,上线等),为不同的环境会写一些不同的配置,那么就需要咱们做相应的环境之间的切换

6.4.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
复制代码# 确定哪一个模块为活动模块
spring:
profiles:
active: pro

---
#开发模块
server:
port: 8088
spring:
profiles: dev

---
#测试模块
server:
port: 8089
spring:
profiles: test

---
#在线模块
server:
port: 8099
spring:
profiles: pro

6.4.2 多profile文件模式

  • 创建多个yml文件,注意名称都是 applicaton-xxx.yml命名(如下截图)

七.SpringBoot的测试功能

  1. 在咱们父模块下新建一个子模块 springboot-test
  2. 引入springboot的测试starter
  3. SpringBoot的启动类
  4. 准备一个bean让Spring管理
  5. 完成测试(看是否可以注入使用这个bean)

7.1 最近创建好的结构

7.2 引入测试依赖(starter)

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>

7.3 准备启动类

1
2
3
4
5
6
复制代码@SpringBootApplication //声明这是一个springboot应用
public class ApplicationConfig {
public static void main(String[] args) {
SpringApplication.run(ApplicationConfig.class,args);
}
}

7.4 准备一个MyBean让Spring管理

1
2
3
4
复制代码//创建一个bean
@Component
public class MyBean {
}

7.5 功能测试

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@RunWith(SpringRunner.class)
//代表这是一个SpringBoot的测试
// classes对应的类必需是经过SpringBootApplication修饰的类
@SpringBootTest(classes=ApplicationConfig.class)
public class MyBeanTest {
@Autowired
private MyBean myBean;

@Test
public void test01(){
System.out.println(myBean);
}
}

七.RestController注解方式

  • 在大量返回json的Controller中使用(以后用得比较多)
  • RestController是一个组合注解 它等于 (@Controller+@ResponseBody)

咱们创建一个新的模块进行测试,下面为核心代码(基础代码此处省略)

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
复制代码@RestController
@RequestMapping("/json")
public class JsonController {


//返回普通数据
@RequestMapping("/01")
public String json01(){
return "hello json";
}
//返回对象
@RequestMapping("/02")
public Employee json02(){
return new Employee(1L,"小春风");
}
//返回集合
@RequestMapping("/03")
public List<Employee> json03(){
return Arrays.asList(
new Employee(1L,"令狐兄"),
new Employee(2L,"不群兄"),
new Employee(3L,"我行兄")
);
}

//返回map
@RequestMapping("/04")
public Map json04(){
Map map = new HashMap();
map.put("name","小飞侠");
map.put("age",24);
map.put("sex",false);
return map;
}

}

八.Thymeleaf

  • Thymeleaf是一个模板技术
    • 其它的模板技术:freemarker,velocity,jsp等
    • jsp现在用得很少,因为它必需依赖servlet容器才能运行,并且编译的效率低下
  • springboot推荐使用Thymeleaf
  • 详细语法:fanlychie.github.io/post/thymel…

8.1 引入thymeleaf的支持包

1
2
3
4
5
6
7
8
9
10
11
复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入thymeleaf的支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

8.2 完成thymeleaf的配置

  • 该步骤可以省略,默认前缀classpath:/templates/,后缀.html
1
2
3
4
复制代码spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html

8.3 controller完成跳转传参

1
2
3
4
5
6
7
8
复制代码@Controller
public class HelloController {
@RequestMapping("/hello")
public String index(Model model){
model.addAttribute("msg","hello,Springboot");
return "index";
}
}

8.4 页面展示

注:加上xmlns:th="http://www.thymeleaf.org"则会支持thymeleaf的提示

1
2
3
4
5
6
7
8
9
10
11
复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--使用语法th:text 可以设置里面的文本内容 -->
<div th:text="${msg}">你好啊!兄弟!!!</div>
</body>
</html>

九.框架集成

注: 练习前先准备相应的数据库与表数据

9.1 导包

导入:数据库驱动包,springboot与jdbc集成包,mybatis与springboot集成包,springboot的web支持包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码    <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库驱动包-->
<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>
<!--mybatis提供的与springboot集成包 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

9.2 准备代码结构

9.3 yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码spring:
datasource:
username: root
password: root
url: jdbc:mysql:///mytest
driver-class-name: com.mysql.jdbc.Driver
##mybatis的配置
mybatis:
# 扫描相应的映射文件
mapper-locations: classpath:cn/itsource/mapper/*.xml
# 该包下的对象取别名
type-aliases-package: cn.itsource.domain
##日志级别的打印(需要看日志的可以直接拷备使用:特别注意它的层级)
logging:
level:
cn:
itsource: trace
root: error

9.4 进行mapper接口的扫描

1
2
3
4
5
6
7
8
9
复制代码@SpringBootApplication
//进行相应的映射接口扫描
@MapperScan("cn.itsource.mapper")
public class ApplicationConfig {

public static void main(String[] args) {
SpringApplication.run(ApplicationConfig.class);
}
}

9.4 mapper层功能

  1. mapper的接口功能
1
2
3
4
复制代码public interface UserMapper {
List<User> findAll();
void save(User user);
}
  1. mapper的xml文件(注:写在resource中对应的位置)
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<?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.itsource.mapper.UserMapper">

<select id="findAll" resultType="User">
select * from user
</select>

<insert id="save" parameterType="User">
insert into user (username) values (#{username})
</insert>
</mapper>
  1. 完成功能测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationConfig.class)
public class UserMapperTest {

@Autowired
private UserMapper userMapper;

@Test
public void mytest(){
userMapper.findAll().forEach(user -> {
System.out.println(user);
});
}
}

9.5 service层功能

注:Springboot已经集成事务,咱们可以直接使用

1.IUserService代码

1
2
3
4
复制代码public interface IUserService {
List<User> findAll();
void save(User user);
}

2.UserServiceImpl功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码@Service
@Transactional(readOnly = true, propagation = Propagation.SUPPORTS)
public class UserServiceImpl implements IUserService {

@Autowired
private UserMapper mapper;

@Override
public List<User> findAll() {
return mapper.findAll();
}

@Override
@Transactional
public void save(User user) {
mapper.save(user);
int i = 1/0;
}
}

3.测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码@RunWith(SpringRunner.class)
@SpringBootTest(classes = ApplicationConfig.class)
public class UserServiceTest {
@Autowired
private IUserService userService;
@Test
public void save(){
User user = new User();
user.setUsername("虎子xx");
userService.save(user);
}
@Test
public void findAll(){
userService.findAll().forEach(user -> {
System.out.println(user);
});
}
}

9.6 controller层功能

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@RestController
@RequestMapping("/user")
public class UserController {

@Autowired
private IUserService userService;

@RequestMapping("/findAll")
public List<User> findAll(){
return userService.findAll();
}
}

本文转载自: 掘金

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

SpringBoot+Vue前后端分离,请求api跨域问题

发表于 2020-06-17

前言

最近过年在家无聊,刚好有大把时间学习Vue,顺便做了一个增删查改+关键字匹配+分页的小dome,可是使用Vue请求后端提供的Api的时候确发现一个大问题,前后端分离了,但是请求的时候也就必定会有跨域这种问题,那如何解决呢?

前端解决方案

思路:由于Vue现在大多数项目但是用脚手架快速搭建的,所以我们可以直接在项目中创建一个vue.config.js的配置文件,然后在里面配置proxy代理来解决,话不多说,直接上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码module.exports = {
devServer: {
proxy: {
'/api':{
target: 'http://127.0.0.1:8181', //API服务器的地址
ws: true, //代理websockets
changeOrigin: true, // 是否跨域,虚拟的站点需要更管origin
pathRewrite: {
'^/api': ''
}
}
}
}
};

这样配置了之后,Vue用axios或者ajax调用后台的api的时候,只需要在请求的路径中/api/xx/xx这种格式去发送请求
这种方式有两个优点
1:解决了跨域问题,而且每次请求的时候只需要写调用的接口,前缀根本不需要再次去写
2:由于是提供了代理,利于隐藏真实的Api服务器地址,确保服务端的安全

后端解决方案

思路: 相信现在Java大多数都是Spring全家桶的天下了吧,而SpringBoot呢最近几年也是大火,基本上大多数后端人员都用过吧,所以我们可以在SpringBoot项目中创建一个config配置包,在里面创建一个webconfig配置类,通过实现WebMvcConfigurer接口的addCorsMappings方法来解决跨域问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码package com.vue.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders("*");
}
}

这种方式也可以解决,但是最好是前后端一致都提供跨域的解决方案

本文转载自: 掘金

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

1…802803804…956

开发者博客

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