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

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


  • 首页

  • 归档

  • 搜索

SpringBoot 配置提示功能 目的 版本 文件 实战

发表于 2019-10-31

提示

目的

配置自动提示的辅助功能可以让配置写起来更快,准确率大大提高。

springboot jar 包含提供所有支持的配置属性细节的元数据文件。文件的目的是为了让 IDE 开发者在用户使用 application.properties 或 application.yml 文件时提供上下文帮助和代码补全。

大多数元数据文件是在编译时通过处理用 @ConfigurationProperties 注释的所有项自动生成的。也可以手动编写部分元数据。

版本

参考 SpringBoot 2.2.0.RELEASE 文档

文件

jar包中的 META-INF/spring-configuration-metadata.json (自动生成)或 META-INF/additional-spring-configuration-metadata.json (手动添加)

实战

1
2
3
4
5
6
复制代码<!-- 引入相关依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
复制代码@Configuration
@ConfigurationProperties(prefix = "file.upload")
public class FileUploadConfig {
/** Maximum number of bytes per file */
private String maxSize = "1024M";

/** 不允许的文件后缀 */
private String rejectSuffix;
//注意:使用的时候必须要有getter/setter,否则不会自动生成该属性对应的提示
//此处因为篇幅原因省略 getter/setter
}
1
2
3
4
5
6
7
8
复制代码@Configuration
@ConfigurationProperties("map.test")
public class MapTestConfig {
/** 测试Map类型数据的提示 */
private Map<String, Object> data;
//注意:使用的时候必须要有getter/setter,否则不会自动生成该属性对应的提示
//此处因为篇幅原因省略 getter/setter
}

中文注释会乱码,以上故意用中文注释的地方,会在下面文件中指定对应的描述,看是否会覆盖。

additional-spring-configuration-metadata.json

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
复制代码{
"properties": [
{
"name": "file.upload.reject-suffix",
"type": "java.lang.String",
"defaultValue": "exe,jar",
"description": "The file suffix is not allowed.",
"sourceType": "com.lw.metadata.config.FileUploadConfig"
},
{
"name": "map.test.data",
"type": "java.util.Map",
"description": "Tips for testing Map type data.",
"sourceType": "com.lw.metadata.config.MapTestConfig"
}
],
"hints": [
{
"name": "map.test.data.keys",
"values": [
{
"value": "name",
"description": "The name of the person."
},
{
"value": "sex",
"description": "The sex of the person."
}
]
}
]
}

maven compile 之后,生成的 additional-spring-configuration-metadata.json 与源码中的一样,生成的 spring-configuration-metadata.json 如下:

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
复制代码{
"groups": [
{
"name": "file.upload",
"type": "com.lw.metadata.config.FileUploadConfig",
"sourceType": "com.lw.metadata.config.FileUploadConfig"
},
{
"name": "map.test",
"type": "com.lw.metadata.config.MapTestConfig",
"sourceType": "com.lw.metadata.config.MapTestConfig"
}
],
"properties": [
{
"name": "file.upload.max-size",
"type": "java.lang.String",
"description": "Maximum number of bytes per file",
"sourceType": "com.lw.metadata.config.FileUploadConfig",
"defaultValue": "1024M"
},
{
"name": "file.upload.reject-suffix",
"type": "java.lang.String",
"description": "The file suffix is not allowed.",
"sourceType": "com.lw.metadata.config.FileUploadConfig",
"defaultValue": "exe,jar"
},
{
"name": "map.test.data",
"type": "java.util.Map<java.lang.String,java.lang.Object>",
"description": "Tips for testing Map type data.",
"sourceType": "com.lw.metadata.config.MapTestConfig"
}
],
"hints": [
{
"name": "map.test.data.keys",
"values": [
{
"value": "name",
"description": "The name of the person."
},
{
"value": "sex",
"description": "The sex of the person."
}
]
}
]
}

效果

SpringBoot配置提示效果

由此可以看到以下现象:

  • 代码中的默认值会自动生成到提示文件中,如:FileUploadConfig#maxSize
  • 代码中的注释会自动生成到提示文件中,如:FileUploadConfig#maxSize
  • additional-spring-configuration-metadata.json 文件中存在的提示会覆盖自动生成的对应属性,若自动生成的没有此属性则自动增加。

手动写提示文件

示例

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
复制代码{
"groups": [
{
"name": "server",
"type": "org.springframework.boot.autoconfigure.web.ServerProperties",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
},
{
"name": "spring.jpa.hibernate",
"type": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate",
"sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties",
"sourceMethod": "getHibernate()"
}
],
"properties": [
{
"name": "server.port",
"type": "java.lang.Integer",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
},
{
"name": "server.address",
"type": "java.net.InetAddress",
"sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
},
{
"name": "spring.jpa.hibernate.ddl-auto",
"type": "java.lang.String",
"description": "DDL mode. This is actually a shortcut for the \"hibernate.hbm2ddl.auto\" property.",
"sourceType": "org.springframework.boot.autoconfigure.orm.jpa.JpaProperties$Hibernate"
}
],
"hints": [
{
"name": "spring.jpa.hibernate.ddl-auto",
"values": [
{
"value": "none",
"description": "Disable DDL handling."
},
{
"value": "validate",
"description": "Validate the schema, make no changes to the database."
},
{
"value": "update",
"description": "Update the schema if necessary."
},
{
"value": "create",
"description": "Create the schema and destroy previous data."
},
{
"value": "create-drop",
"description": "Create and then destroy the schema at the end of the session."
}
]
}
]
}

groups

分组,将配置类分组。可以按照文件来分组,即:将同一个配置文件的所有属性放在同一个组

属性 类型 是否必须 用途
name String Y 分组的完整名称
type String N 分组数据类型的类名(如:使用@ConfigurationProperties注释的完整类名、使用@Bean注释的方法返回类型)
description String N 分组的简短描述。
sourceType String N 提供分组来源的类名。
sourceMethod String N 提供分组的方法,包含括号和参数类型。

properties

提示主体,必须

属性 类型 是否必须 用途
name String Y 属性的完整名称。名称采用小写句点分隔格式,如:server.address
type String N 属性数据类型的完整签名(如:java.lang.String)或完整的泛型类型(如:java.util.Map)。此属性提示用户输入值得类型。原生类型在此处使用其包装类型(如:boolean使用java.lang.Boolean)。
description String N 分组的简短描述。
sourceType String N 提供分组来源的类名。
defaultValue Object N 默认值。当属性为指定时使用。
deprecation Deprecation N 指定属性是否已弃用。

deprecation属性如下:

属性 类型 是否必须 用途
level String N 弃用级别,可以是 warning(默认值) 或 error。warning:属性应该仍然可以使用;error:属性不保证可以使用
reason String N 属性弃用的简短原因。
replacement String N 替换此弃用属性的新属性全名。可为空

注意:Spring Boot 1.3 版本之前,是使用 boolean 类型的 deprecated。

以下示例来源于官方文档,展示了如何处理这种场景:

`java

@ConfigurationProperties(“app.acme”)

public class AcmeProperties {

private String name;

public String getName() { … }

public void setName(String name) { … }

@DeprecatedConfigurationProperty(replacement = “app.acme.name”)

@Deprecated

public String getTarget() {

return getName();

}

@Deprecated

public void setTarget(String target) {

setName(target);

}

}

`

一旦 getTarget 和 setTarget 方法从公共 API 中删除,元数据中的自动弃用提示也会消失。 如果要保留提示,则添加具有 error 弃用级别的手动元数据可以确保用户仍然了解该属性。在提供替代品时,这样做特别有用。

hints

辅助提示,非必须

属性 类型 是否必须 用途
name String Y 提示关联的属性的完整名称。名称是小写句点分隔格式(如:spring.mvc.servlet.path),如果属性关联map类型(如:system.contexts),提示可以关联map的键(system.contexts.keys)或者值(system.contexts.values)。
values ValueHint[] N 有效值集合。(下表详述)
providers ValueProvider[] N 提供者集合。(下表详述)

values 属性如下:

属性 类型 是否必须 用途
value Object Y 提示引用元素的有效值。如果属性是数组,value和description也可以是数组。
description String N value 对应的简短描述

ValueHint

对于Map类型的支持如下:

1
2
3
4
5
6
复制代码@ConfigurationProperties("sample")
public class SampleProperties {

private Map<String,Integer> contexts;
// getters and setters
}
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{"hints": [
{
"name": "sample.contexts.keys",
"values": [
{
"value": "sample1"
},
{
"value": "sample2"
}
]
}
]}

提示是对Map内每一对 key-value 的提示。

.keys 和 .values 前缀必须分别关联 Map 的 keys 和 values。

providers 属性如下:

属性 类型 是否必须 用途
name String N 用于为提示所引用的元素提供其他内容帮助的 provider 的名称。
parameters JSON object N provider 所支持的任何其他参数(有关详细信息,请查看 provider 的文档)。

ValueProvider

*一般用不到,建议跳过*

下表总结了支持的 providers 列表:

属性 描述
any 允许提供任何附加值。
class-reference 自动完成项目中可用的类。通常由目标参数指定的基类约束。
handle-as 处理属性,就像它是由必须的 target 参数定义的类型定义的一样。
logger-name 自动完成有效的记录器名称和记录器组。通常,当前项目中可用的包和类名可以自动完成,也可以定义组。
spring-bean-reference 自动完成当前项目中可用的bean名称。通常由 target 参数指定的基类约束。
spring-profile-name 自动完成项目中可用的 spring 配置文件名称。

any

符合属性类型的所有值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码{"hints": [
{
"name": "system.state",
"values": [
{
"value": "on"
},
{
"value": "off"
}
],
"providers": [
{
"name": "any"
}
]
}
]}

class-reference

提供以下参数:

参数 类型 默认值 描述
target String(Class) 无 分配给值的类的全限定类名。通常用于筛选非候选类。
concrete boolean true 指定是否仅将具体类视为有效候选。
1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{"hints": [
{
"name": "server.servlet.jsp.class-name",
"providers": [
{
"name": "class-reference",
"parameters": {
"target": "javax.servlet.http.HttpServlet"
}
}
]
}
]}

handle-as

允许您将属性的类型替换为更高级的类型。

这通常在属性具有 java.lang.String 类型时发生,因为您不希望配置类依赖于不在类路径上的类。

参数 类型 默认值 描述
target String(Class) 无 Y 为属性考虑的类型的完全限定名。

可用的值如下:

  • 任何 java.lang.Enum: 列出属性的可能值。
  • java.nio.charset.Charset: 支持自动完成字符集/编码值(如 utf-8)
  • java.util.Locale:自动完成区域设置(如:en_US)
  • org.springframework.util.MimeType:支持自动完成 content-type 值(如: text/plain )
  • org.springframework.core.io.Resource: 支持自动完成spring的资源抽象以引用文件系统或类路径上的文件 (如:classpath:/sample.properties)

注意:如果要提供多个值,用 Collection 或 数组类型

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{"hints": [
{
"name": "spring.liquibase.change-log",
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "org.springframework.core.io.Resource"
}
}
]
}
]}

logger-name

logger-name provider 自动完成有效的记录器名称和记录器组。 通常,当前项目中可用的包和类名可以自动完成。 如果组已启用(默认),并且配置中标识了自定义记录器组,则应提供该组的自动完成。

支持以下参数:

参数 类型 默认值 描述
group boolean true 指定是否应考虑已知组。

由于记录器名称可以是任意名称,此 provider 应允许任何值,但可以突出显示项目的类路径中不可用的有效包和类名。

以下是 logging.level 属性。keys 是 logger 名,values 关联标准的 log levels 或 自定义的 level,

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
复制代码{"hints": [
{
"name": "logging.level.keys",
"values": [
{
"value": "root",
"description": "Root logger used to assign the default logging level."
},
{
"value": "sql",
"description": "SQL logging group including Hibernate SQL logger."
},
{
"value": "web",
"description": "Web logging group including codecs."
}
],
"providers": [
{
"name": "logger-name"
}
]
},
{
"name": "logging.level.values",
"values": [
{
"value": "trace"
},
{
"value": "debug"
},
{
"value": "info"
},
{
"value": "warn"
},
{
"value": "error"
},
{
"value": "fatal"
},
{
"value": "off"
}

],
"providers": [
{
"name": "any"
}
]
}
]}

spring-bean-reference

此 provider 自动完成在当前项目的配置中定义的bean。 支持以下参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码{"hints": [
{
"name": "spring.jmx.server",
"providers": [
{
"name": "spring-bean-reference",
"parameters": {
"target": "javax.management.MBeanServer"
}
}
]
}
]}

spring-profile-name

此 provider 自动完成在当前项目的配置中定义的spring配置文件。

以下示例表示:spring.profiles.active属性可启用的配置文件名称。

1
2
3
4
5
6
7
8
9
10
复制代码{"hints": [
{
"name": "spring.profiles.active",
"providers": [
{
"name": "spring-profile-name"
}
]
}
]}

可重复的元数据项

具有相同“property”和“group”名称的对象可以在元数据文件中多次出现。 例如,可以将两个单独的类绑定到同一前缀,每个类都有可能重叠的属性名。 虽然多次出现在元数据中的相同名称不应是常见的,但元数据的使用者应注意确保他们支持该名称。

自动生成提示文件

通过使用 spring-boot-configuration-processor jar,您可以从用 @ConfigurationProperties 注释的类中轻松生成自己的配置元数据文件。 jar包含一个java注释处理器,在编译项目时调用它。 用此处理器,需要引入 spring-boot-configuration-processor 依赖。

`xml

org.springframework.boot

spring-boot-configuration-processor

true

`

处理器获取用@configurationproperties注释的类和方法。 配置类中字段值的 javadoc 用于填充 description 属性。

注意:仅仅只应将简单文本与@configurationproperties字段javadoc一起使用,因为在将它们添加到json之前不会对它们进行处理。

如果类有一个“至少一个参数”的构造函数,则为每个构造函数参数创建一个属性。 否则,通过标准getter和setter来发现属性,这些getter和setter对集合类型进行了特殊处理(即使只有getter存在,也会检测到)。

注解处理器还支持使用@data、@getter和@setter 的 lombok 注解。

注解处理器无法自动检测 Enum 和 Collections 的默认值。在集合或枚举属性具有非空默认值的情况下,应提供手动元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码@ConfigurationProperties(prefix="acme.messaging")
public class MessagingProperties {

private List<String> addresses = new ArrayList<>(Arrays.asList("a", "b")) ;

private ContainerType = ContainerType.SIMPLE;

// ... getter and setters

public enum ContainerType {
SIMPLE,
DIRECT
}
}

为了提示上述属性的默认值,应该手动添加如下元数据:

1
2
3
4
5
6
7
8
9
10
复制代码{"properties": [
{
"name": "acme.messaging.addresses",
"defaultValue": ["a", "b"]
},
{
"name": "acme.messaging.container-type",
"defaultValue": "simple"
}
]}

注意: 如果在项目中使用 AspectJ,则需要确保注解处理器只运行一次。 使用 Maven 时, 可以显式地配置 maven-apt-plugin插件,并仅在那里向注解处理器添加依赖项。 还可以让 AspectJ 插件运行于所有的处理且在 maven-compiler-plugin 的 configuration 中禁用注解处理,如下:

`java

org.apache.maven.plugins

maven-compiler-plugin

none

`

绑定属性

注解处理器自动将内部类视为嵌套属性。

1
2
3
4
5
6
7
8
9
10
11
复制代码@ConfigurationProperties(prefix="server")
public class ServerProperties {
private String name;
private Host host;
// ... getter and setters
public static class Host {
private String ip;
private int port;
// ... getter and setters
}
}

上述示例生成 server.name、server.host.ip 和 server.host.port 属性的元数据信息。 可以在字段上使用@NestedconfigurationProperty 注解来指示应将常规(非内部)类视为嵌套类。

注意: 这对集合和映射没有影响,因为这些类型是自动标识的,并且为每个类型生成一个元数据属性。

添加额外的元数据

Spring Boot 的配置文件处理非常灵活,通常情况下,可能存在不绑定到 @ConfigurationProperties bean的属性。 您还可能需要调整现有key的某些属性,为了支持这种情况并让您提供自定义的“提示”,注解处理器会自动将 META-INF/additional-spring-configuration-metadata.json 中的提示项合并到主要元数据文件(***spring-configuration-metadata.json***)中。

如果引用已自动检测到的属性,则将覆盖描述、默认值和弃用信息(如果指定)。 如果当前模块中没有标识手动属性中的声明,则将其作为新属性添加。

additional-spring-configuration-metadata.json 文件的格式与 spring-configuration-metadata.json 文件一样。 附加属性文件是可选的。如果没有任何其他属性,就不要添加文件。

参考资料

springboot 配置提示官方文档公众号:逸飞兮(专注于 Java 领域知识的深入学习,从源码到原理,系统有序的学习)

逸飞兮

本文转载自: 掘金

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

微服务中如何使用RestTemplate优雅调用API(拦截

发表于 2019-10-30

关注我,可以获取最新知识、经典面试题以及微服务技术分享

  在微服务中,rest服务互相调用是很普遍的,我们该如何优雅地调用,其实在Spring框架使用RestTemplate类可以优雅地进行rest服务互相调用,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接,操作使用简便,还可以自定义RestTemplate所需的模式。其中:

  • RestTemplate默认使用HttpMessageConverter实例将HTTP消息转换成POJO或者从POJO转换成HTTP消息。默认情况下会注册主mime类型的转换器,但也可以通过setMessageConverters注册自定义转换器。
  • RestTemplate使用了默认的DefaultResponseErrorHandler,对40X Bad Request或50X internal异常error等错误信息捕捉。
  • RestTemplate还可以使用拦截器interceptor,进行对请求链接跟踪,以及统一head的设置。

其中,RestTemplate还定义了很多的REST资源交互的方法,其中的大多数都对应于HTTP的方法,如下:

方法 解析
delete() 在特定的URL上对资源执行HTTP DELETE操作
exchange() 在URL上执行特定的HTTP方法,返回包含对象的ResponseEntity
execute() 在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象
getForEntity() 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象
getForObject() 发送一个HTTP GET请求,返回的请求体将映射为一个对象
postForEntity() POST 数据到一个URL,返回包含一个对象的ResponseEntity
postForObject() POST 数据到一个URL,返回根据响应体匹配形成的对象
headForHeaders() 发送HTTP HEAD请求,返回包含特定资源URL的HTTP头
optionsForAllow() 发送HTTP OPTIONS请求,返回对特定URL的Allow头信息
postForLocation() POST 数据到一个URL,返回新创建资源的URL
put() PUT 资源到特定的URL
  1. RestTemplate源码

1.1 默认调用链路

restTemplate进行API调用时,默认调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码###########1.使用createRequest创建请求########
resttemplate->execute()->doExecute()
HttpAccessor->createRequest()
//获取拦截器Interceptor,InterceptingClientHttpRequestFactory,SimpleClientHttpRequestFactory
InterceptingHttpAccessor->getRequestFactory()
//获取默认的SimpleBufferingClientHttpRequest
SimpleClientHttpRequestFactory->createRequest()

#######2.获取响应response进行处理###########
AbstractClientHttpRequest->execute()->executeInternal()
AbstractBufferingClientHttpRequest->executeInternal()

###########3.异常处理#####################
resttemplate->handleResponse()

##########4.响应消息体封装为java对象#######
HttpMessageConverterExtractor->extractData()

1.2 restTemplate->doExecute()

在默认调用链中,restTemplate 进行API调用都会调用 doExecute 方法,此方法主要可以进行如下步骤:

1)使用createRequest创建请求,获取响应

2)判断响应是否异常,处理异常

3)将响应消息体封装为java对象

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
复制代码@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {

Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
//使用createRequest创建请求
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
//获取响应response进行处理
response = request.execute();
//异常处理
handleResponse(url, method, response);
//响应消息体封装为java对象
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}finally {
if (response != null) {
response.close();
}
}
}

1.3 InterceptingHttpAccessor->getRequestFactory()

在默认调用链中,InterceptingHttpAccessor的getRequestFactory()方法中,如果没有设置interceptor拦截器,就返回默认的SimpleClientHttpRequestFactory,反之,返回InterceptingClientHttpRequestFactory的requestFactory,可以通过resttemplate.setInterceptors设置自定义拦截器interceptor。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码//Return the request factory that this accessor uses for obtaining client request handles.
public ClientHttpRequestFactory getRequestFactory() {
//获取拦截器interceptor(自定义的)
List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
if (!CollectionUtils.isEmpty(interceptors)) {
ClientHttpRequestFactory factory = this.interceptingRequestFactory;
if (factory == null) {
factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
this.interceptingRequestFactory = factory;
}
return factory;
}
else {
return super.getRequestFactory();
}
}

然后再调用SimpleClientHttpRequestFactory的createRequest创建连接:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Override
public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
HttpURLConnection connection = openConnection(uri.toURL(), this.proxy);
prepareConnection(connection, httpMethod.name());

if (this.bufferRequestBody) {
return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming);
}
else {
return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming);
}
}

1.4 resttemplate->handleResponse()

在默认调用链中,resttemplate的handleResponse,响应处理,包括异常处理,而且异常处理可以通过调用setErrorHandler方法设置自定义的ErrorHandler,实现对请求响应异常的判别和处理。自定义的ErrorHandler需实现ResponseErrorHandler接口,同时Spring boot也提供了默认实现DefaultResponseErrorHandler,因此也可以通过继承该类来实现自己的ErrorHandler。

DefaultResponseErrorHandler默认对40X Bad Request或50X internal异常error等错误信息捕捉。如果想捕捉服务本身抛出的异常信息,需要通过自行实现RestTemplate的ErrorHandler。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码ResponseErrorHandler errorHandler = getErrorHandler();
//判断响应是否有异常
boolean hasError = errorHandler.hasError(response);
if (logger.isDebugEnabled()) {
try {
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
logger.debug("Response " + (status != null ? status : code));
}catch (IOException ex) {
// ignore
}
}
//有异常进行异常处理
if (hasError) {
errorHandler.handleError(url, method, response);
}
}

1.5 HttpMessageConverterExtractor->extractData()

在默认调用链中, HttpMessageConverterExtractor的extractData中进行响应消息体封装为java对象,就需要使用message转换器,可以通过追加的方式增加自定义的messageConverter:先获取现有的messageConverter,再将自定义的messageConverter添加进去。

根据restTemplate的setMessageConverters的源码可得,使用追加的方式可防止原有的messageConverter丢失,源码:

1
2
3
4
5
6
7
8
9
10
11
复制代码public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
//检验
validateConverters(messageConverters);
// Take getMessageConverters() List as-is when passed in here
if (this.messageConverters != messageConverters) {
//先清除原有的messageConverter
this.messageConverters.clear();
//后加载重新定义的messageConverter
this.messageConverters.addAll(messageConverters);
}
}

HttpMessageConverterExtractor的extractData源码:

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
复制代码MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
return null;
}
//获取到response的ContentType类型
MediaType contentType = getContentType(responseWrapper);

try {
//依次循环messageConverter进行判断是否符合转换条件,进行转换java对象
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
//会根据设置的返回类型responseType和contentType参数进行匹配,选择合适的MessageConverter
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericMessageConverter =
(GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
if (logger.isDebugEnabled()) {
ResolvableType resolvableType = ResolvableType.forType(this.responseType);
logger.debug("Reading to [" + resolvableType + "]");
}
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
}
if (this.responseClass != null) {
if (messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
String className = this.responseClass.getName();
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
}
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
}
}
}
}
.....
}

1.6 contentType与messageConverter之间的关系

在HttpMessageConverterExtractor的extractData方法中看出,会根据contentType与responseClass选择messageConverter是否可读、消息转换。关系如下:

类名 支持的JavaType 支持的MediaType
ByteArrayHttpMessageConverter byte[] application/octet-stream, */*
StringHttpMessageConverter String text/plain, */*
ResourceHttpMessageConverter Resource */*
SourceHttpMessageConverter Source application/xml, text/xml, application/*+xml
AllEncompassingFormHttpMessageConverter Map<K, List<?>> application/x-www-form-urlencoded, multipart/form-data
MappingJackson2HttpMessageConverter Object application/json, application/*+json
Jaxb2RootElementHttpMessageConverter Object application/xml, text/xml, application/*+xml
JavaSerializationConverter Serializable x-java-serialization;charset=UTF-8
FastJsonHttpMessageConverter Object */*
  1. springboot集成RestTemplate

  根据上述源码的分析学习,可以轻松,简单地在项目进行对RestTemplate进行优雅地使用,比如增加自定义的异常处理、MessageConverter以及拦截器interceptor。本文使用示例demo,详情请查看接下来的内容。

2.1. 导入依赖:(RestTemplate集成在Web Start中)

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>

2.2. RestTemplat配置:

  • 使用ClientHttpRequestFactory属性配置RestTemplat参数,比如ConnectTimeout,ReadTimeout;
  • 增加自定义的interceptor拦截器和异常处理;
  • 追加message转换器;
  • 配置自定义的异常处理.
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
复制代码 @Configuration
public class RestTemplateConfig {

@Value("${resttemplate.connection.timeout}")
private int restTemplateConnectionTimeout;
@Value("${resttemplate.read.timeout}")
private int restTemplateReadTimeout;

@Bean
//@LoadBalanced
public RestTemplate restTemplate( ClientHttpRequestFactory simleClientHttpRequestFactory) {
RestTemplate restTemplate = new RestTemplate();
//配置自定义的message转换器
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
messageConverters.add(new CustomMappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(messageConverters);
//配置自定义的interceptor拦截器
List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(new HeadClientHttpRequestInterceptor());
interceptors.add(new TrackLogClientHttpRequestInterceptor());
restTemplate.setInterceptors(interceptors);
//配置自定义的异常处理
restTemplate.setErrorHandler(new CustomResponseErrorHandler());
restTemplate.setRequestFactory(simleClientHttpRequestFactory);

return restTemplate;
}

@Bean
public ClientHttpRequestFactory simleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory reqFactory= new SimpleClientHttpRequestFactory();
reqFactory.setConnectTimeout(restTemplateConnectionTimeout);
reqFactory.setReadTimeout(restTemplateReadTimeout);
return reqFactory;
}
}

2.3. 组件(自定义异常处理、interceptor拦截器、message转化器)

自定义interceptor拦截器,实现ClientHttpRequestInterceptor接口

  • 自定义TrackLogClientHttpRequestInterceptor,记录resttemplate的request和response信息,可进行追踪分析;
  • 自定义HeadClientHttpRequestInterceptor,设置请求头的参数。API发送各种请求,很多请求都需要用到相似或者相同的Http Header。如果在每次请求之前都把Header填入HttpEntity/RequestEntity,这样的代码会显得十分冗余,可以在拦截器统一设置。

TrackLogClientHttpRequestInterceptor:

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
复制代码/**
* @Auther: ccww
* @Date: 2019/10/25 22:48,记录resttemplate访问信息
* @Description: 记录resttemplate访问信息
*/
@Slf4j
public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
trackRequest(request,body);
ClientHttpResponse httpResponse = execution.execute(request, body);
trackResponse(httpResponse);
return httpResponse;
}

private void trackResponse(ClientHttpResponse httpResponse)throws IOException {
log.info("============================response begin==========================================");
log.info("Status code : {}", httpResponse.getStatusCode());
log.info("Status text : {}", httpResponse.getStatusText());
log.info("Headers : {}", httpResponse.getHeaders());
log.info("=======================response end=================================================");
}

private void trackRequest(HttpRequest request, byte[] body)throws UnsupportedEncodingException {
log.info("======= request begin ========");
log.info("uri : {}", request.getURI());
log.info("method : {}", request.getMethod());
log.info("headers : {}", request.getHeaders());
log.info("request body : {}", new String(body, "UTF-8"));
log.info("======= request end ========");
}
}

HeadClientHttpRequestInterceptor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码@Slf4j
public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
log.info("#####head handle########");
HttpHeaders headers = httpRequest.getHeaders();
headers.add("Accept", "application/json");
headers.add("Accept-Encoding", "gzip");
headers.add("Content-Encoding", "UTF-8");
headers.add("Content-Type", "application/json; charset=UTF-8");
ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
HttpHeaders headersResponse = response.getHeaders();
headersResponse.add("Accept", "application/json");
return response;
}
}

自定义异常处理,可继承DefaultResponseErrorHandler或者实现ResponseErrorHandler接口:

  • 实现自定义ErrorHandler的思路是根据响应消息体进行相应的异常处理策略,对于其他异常情况由父类DefaultResponseErrorHandler来进行处理。
  • 自定义CustomResponseErrorHandler进行30x异常处理

CustomResponseErrorHandler:

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
复制代码/**
* @Auther: Ccww
* @Date: 2019/10/28 17:00
* @Description: 30X的异常处理
*/
@Slf4j
public class CustomResponseErrorHandler extends DefaultResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = response.getStatusCode();
if(statusCode.is3xxRedirection()){
return true;
}
return super.hasError(response);
}

@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = response.getStatusCode();
if(statusCode.is3xxRedirection()){
log.info("########30X错误,需要重定向!##########");
return;
}
super.handleError(response);
}

}

自定义message转化器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码/**
* @Auther: Ccww
* @Date: 2019/10/29 21:15
* @Description: 将Content-Type:"text/html"转换为Map类型格式
*/
public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public CustomMappingJackson2HttpMessageConverter() {
List<MediaType> mediaTypes = new ArrayList<MediaType>();
mediaTypes.add(MediaType.TEXT_PLAIN);
mediaTypes.add(MediaType.TEXT_HTML); //加入text/html类型的支持
setSupportedMediaTypes(mediaTypes);// tag6
}

}

各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!

欢迎关注公众号【Ccww技术博客】,原创技术文章第一时间推出

本文转载自: 掘金

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

面试官:"准备用HashMap存1w条数据,构造时传1000

发表于 2019-10-30

1
2
3
java复制代码// 预计存入 1w 条数据,初始化赋值 10000,避免 resize。  
HashMap<String,String> map = new HashMap<>(10000)
// for (int i = 0; i < 10000; i++)

Java 集合的扩容

HashMap 算是我们最常用的集合之一,虽然对于 Android 开发者,Google 官方推荐了更省内存的 SparseArray 和 ArrayMap,但是 HashMap 依然是最常用的。

我们通过 HashMap 来存储 Key-Value 这种键值对形式的数据,其内部通过哈希表,让存取效率最好时可以达到 O(1),而又因为可能存在的 Hash 冲突,引入了链表和红黑树的结构,让效率最差也差不过 O(logn)。

整体来说,HashMap 作为一款工业级的哈希表结构,效率还是有保障的。

编程语言提供的集合类,虽然底层还是基于数组、链表这种最基本的数据结构,但是和我们直接使用数组不同,集合在容量不足时,会触发动态扩容来保证有足够的空间存储数据。

动态扩容,涉及到数据的拷贝,是一种「较重」的操作。那如果能够提前确定集合将要存储的数据量范围,就可以通过构造方法,指定集合的初始容量,来保证接下来的操作中,不至于触发动态扩容。

这就引入了本文开篇的问题,如果使用 HashMap,当初始化是构造函数指定 1w 时,后续我们立即存入 1w 条数据,是否符合与其不会触发扩容呢?

在分析这个问题前,那我们先来看看,HashMap 初始化时,指定初始容量值都做了什么?

PS:本文所涉及代码,均以 JDK 1.8 中 HashMap 的源码举例。

HashMap 的初始化

在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity),这个方法最终会调用到 HashMap 另一个构造方法,其中的参数 loadFactor 就是默认值 0.75f。

1
2
3
4
5
6
7
8
9
10
11
java复制代码public HashMap(int initialCapacity, float loadFactor) {  
  if (initialCapacity < 0)
    throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
  if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
  if (loadFactor <= 0 || Float.isNaN(loadFactor))
    throw new IllegalArgumentException("Illegal load factor: " + loadFactor);

  this.loadFactor = loadFactor;
  this.threshold = tableSizeFor(initialCapacity);
}

其中的成员变量 threshold 就是用来存储,触发 HashMap 扩容的阈值,也就是说,当 HashMap 存储的数据量达到 threshold 时,就会触发扩容。

从构造方法的逻辑可以看出,HashMap 并不是直接使用外部传递进来的 initialCapacity,而是经过了 tableSizeFor() 方法的处理,再赋值到 threshole 上。

1
2
3
4
5
6
7
8
9
java复制代码static final int tableSizeFor(int cap) {  
  int n = cap - 1;
  n |= n >>> 1;
  n |= n >>> 2;
  n |= n >>> 4;
  n |= n >>> 8;
  n |= n >>> 16;
  return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

在 tableSizeFor() 方法中,通过逐步位运算,就可以让返回值,保持在 2 的 N 次幂。以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。

那么当我们从外部传递进来 1w 时,实际上经过 tableSizeFor() 方法处理之后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。

这种场景下,用来存放 1w 条数据,绰绰有余了,并不会触发我们猜想的扩容。

HashMap 的 table 初始化

当我们把初始容量,调整到 1000 时,情况又不一样了,具体情况具体分析。

再回到 HashMap 的构造方法,threshold 为扩容的阈值,在构造方法中由 tableSizeFor() 方法调整后直接赋值,所以在构造 HashMap 时,如果传递 1000,threshold 调整后的值确实是 1024,但 HashMap 并不直接使用它。

仔细想想就会知道,初始化时决定了 threshold 值,但其装载因子(loadFactor)并没有参与运算,那在后面具体逻辑的时候,HashMap 是如何处理的呢?

在 HashMap 中,所有的数据,都是通过成员变量 table 数组来存储的,在 JDK 1.7 和 1.8 中虽然 table 的类型有所不同,但是数组这种基本结构并没有变化。那么 table、threshold、loadFactor 三者之间的关系,就是:

table.size == threshold * loadFactor

那这个 table 是在什么时候初始化的呢?这就要说会到我们一直在回避的问题,HashMap 的扩容。

在 HashMap 中,动态扩容的逻辑在 resize() 方法中。这个方法不仅仅承担了 table 的扩容,它还承担了 table 的初始化。

当我们首次调用 HashMap 的 put() 方法存数据时,如果发现 table 为 null,则会调用 resize() 去初始化 table,具体逻辑在 putVal() 方法中。

1
2
3
4
5
6
java复制代码final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {  
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length; // 调用 resize()
    // ...
}

在 resize() 方法中,调整了最终 threshold 值,以及完成了 table 的初始化。

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
java复制代码final Node<K,V>[] resize() {  
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; 
    }
    else if (oldThr > 0) 
        newCap = oldThr; // ①
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
          // ②
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // ③
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab; // ④
      // ....
}

注意看代码中的注释标记。

因为 resize() 还糅合了动态扩容的逻辑,所以我将初始化 table 的逻辑用注释标记出来了。其中 xxxCap 和 xxxThr 分别对应了 table 的容量和动态扩容的阈值,所以存在旧和新两组数据。

当我们指定了初始容量,且 table 未被初始化时,oldThr 就不为 0,则会走到代码 ① 的逻辑。在其中将 newCap 赋值为 oldThr,也就是新创建的 table 会是我们构造的 HashMap 时指定的容量值。

之后会进入代码 ② 的逻辑,其中就通过装载因子(loadFactor)调整了新的阈值(newThr),当然这里也做了一些限制需要让 newThr 在一个合法的范围内。

在代码 ③ 中,将使用 loadFactor 调整后的阈值,重新保存到 threshold 中。并通过 newCap 创建新的数组,将其指定到 table 上,完成 table 的初始化(代码 ④)。

到这里也就清楚了,虽然我们在初始化时,传递进来的 initialCapacity 虽然经过 tableSizeFor() 方法调整后,直接赋值给 threshold,但是它实际是 table 的尺寸,并且最终会通过 loadFactor 重新调整 threshold。

那么回到之前的问题就有答案了,虽然 HashMap 初始容量指定为 1000,会被 tableSizeFor() 调整为 1024,但是它只是表示 table 数组为 1024,扩容的重要依据扩容阈值会在 resize() 中调整为 768(1024 * 0.75)。

它是不足以承载 1000 条数据的,最终在存够 1k 条数据之前,还会触发一次动态扩容。

通常在初始化 HashMap 时,初始容量都是根据业务来的,而不会是一个固定值,为此我们需要有一个特殊处理的方式,就是将预期的初始容量,再除以 HashMap 的装载因子,默认时就是除以 0.75。

例如想要用 HashMap 存放 1k 条数据,应该设置 1000 / 0.75,实际传递进去的值是 1333,然后会被 tableSizeFor() 方法调整到 2048,足够存储数据而不会触发扩容。

当想用 HashMap 存放 1w 条数据时,依然设置 10000 / 0.75,实际传递进去的值是 13333,会被调整到 16384,和我们直接传递 10000 效果是一样的。

小结时刻

到这里,就了解清楚了 HashMap 的初始容量,应该如何科学的计算,本质上你传递进去的值可能并无法直接存储这么多数据,会有一个动态调整的过程。其中就需要将我们预期的值进行放大,比较科学的就是依据装载因子进行放大。

最后我们再总结一下:

  1. HashMap 构造方法传递的 initialCapacity,虽然在处理后被存入了 loadFactor 中,但它实际表示 table 的容量。
  2. 构造方法传递的 initialCapacity,最终会被 tableSizeFor() 方法动态调整为 2 的 N 次幂,以方便在扩容的时候,计算数据在 newTable 中的位置。
  3. 如果设置了 table 的初始容量,会在初始化 table 时,将扩容阈值 threshold 重新调整为 table.size * loadFactor。
  4. HashMap 是否扩容,由 threshold 决定,而 threshold 又由初始容量和 loadFactor 决定。
  5. 如果我们预先知道 HashMap 数据量范围,可以预设 HashMap 的容量值来提升效率,但是需要注意要考虑装载因子的影响,才能保证不会触发预期之外的动态扩容。

HashMap 作为 Java 最常用的集合之一,市面上优秀的文章很多,但是很少有人从初始容量的角度来分析其中的逻辑,而初始容量又是集合中比较实际的优化点。其实不少人也搞不清楚,在设置 HashMap 初始容量时,是否应该考虑装载因子,才有了此文。

如果本文对你有所帮助,留言、转发、收藏是最大的支持,谢谢!


公众号后台回复成长『成长』,将会得到我准备的学习资料。

本文转载自: 掘金

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

最近学到的ABTest知识

发表于 2019-10-30

前言

只有光头才能变强。

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

如果之前看过我文章的同学就知道我在工作中搞的是推送系统,之前写过一篇 带你了解什么是Push消息推送,里面也提到了我们或许可以做ABTest,最终提高推送消息的点击率。

那什么是ABTest呢?这篇文章带你们入门一下。

一、ABTest的介绍

比如我写了一篇关于ABTest的文章,我希望这篇文章的阅读量能上2500,但是我没想好标题叫什么比较合适。一条推文的标题非常能影响到阅读量,于是我想了几个的标题:

  • 最近我学到的AbTest知识
  • AbTest入门

而我不知道哪个标题效果会更好一些,于是我做了这么一个尝试:

  1. 《最近我学到的AbTest知识》这个标题推送给10%的用户
  2. 《AbTest入门》这个标题推送给10%的用户
  3. 过一段时间后,我看一下效果,哪个标题的阅读量更高,我就将效果高的标题推送给剩余80%的用户

ABTest过程

要注意的是:在推送的文章的时候,除了标题不同,其他因素都需要相同(不能被别的因素给干扰),这样看数据的时候才有说服力。

1.1为什么要做ABTest?

做ABTest的原因其实很简单,我们在做业务的时候会有各种各样的想法,比如说:

  • “我觉得在文案上加入emoji表情,这个推送的消息的点击率肯定高”
  • “我觉得这个按钮/图片换成别的颜色,转化率肯定会提高”
  • “我觉得首页就应该设计成这样,还有图墙应该是这样这样..“
  • …..

但是,并不是所有的想法都是正确的,很可能因为你的想法把首页的样式改掉,用户不喜欢,就影响到了GMV等等等….

一个好的产品都是迭代出来的,而我们很可能不清楚这次的迭代最终是好是坏(至少我们是觉得迭代对用户是好的,是有帮助的,对公司的转化也是好的),但是我们的用户未必就买账。

于是,为了降低试错成本,我们就做ABTest。一个功能做出来,我们只放小流量看下效果,如果效果比原来的功能差,那很可能我们这个想法没有达到预期。如果小流量效果比预期要好,再逐步加大流量,直至全量。

二、怎么做ABTest?

从上面的案例,其实我们大概知道,ABTest最主要做的就是一个分流的事

  • 将10%流量分给用户群体A
  • 将10%流量分给用户群体A

分流

我们需要保证的是:一个用户再次请求进来,用户看到的结果是一样的

比如说,我访问了Java3y,他的简介是:“一个坚持原创的Java技术公众号“。而一个小时后,我再访问了他一次,他的简介是:“一个干货满满的技术号“。而一个小时过后,我又访问了他一次,他的简介是:“一个坚持原创的Java技术公众号“。

简介

这是不合理的,理应上用户在一段时间内,看到的内容是相同的,不然就给用户带来一种错乱感。

OK,于是一般可以这样做:

  • 对用户ID(设备ID/CookieId/userId/openId)取hash值,每次Hash的结果都是相同的。
  • 直接取用户ID的某一位

现在看起来,ABTest好像就是一个分流的东西,只是取了个高大尚的名字叫做ABTest。

2.1 ABTest更多的内容

假如我做了一个UI层面上的ABTest,占用全站的流量80%,现在我还想做搜索结果的ABTest怎么办?只能用剩下的20%了?那我的流量不够用啊(我可能要做各种实验的呢)。UI层面上的ABTest和搜索结果的ABTest能不能同时进行啊?

答案是可以的。因为UI层面和搜索结果(算法优化)的业务关联性是很低的。如果要做“同一份流量同时做UI层面上和搜索结果的ABTest”,那要保证“在UI层面做的ABTest不能影响到搜索结果的ABTest”

  • 业界应用最多的,是可重叠分层分桶方法
  • 层与层之间的流量互不干扰,这就是很多文章所讲的正交(流量在每一层都会被重新打散)

来源:https://www.infoq.cn/article/BuP18dsaPyAg-hflDxPf

我们就可以这样干:通过 Hash(userId, LayerId) % 1000 类似的办法来实现

  • 每一层的实验不管有多少个,对其他层的影响都是均匀的

我的理解:

为了实现UI/算法/广告 这些业务上没什么关联的,能够使用同一份流量做ABTest测试,所以分了层。流量经过每一层都需要将流量重新打散(正交)—-每层实验后,不会影响到下一层的实验

如果业务关联强的应该放在同一层,同一层多个实验是互斥的(比如 一个按钮颜色改为绿色作为一个实验,一个按钮的样式改成大拇指作为一个实验。这两个实验的流量是要互斥的(不然你咋知道用户是因为你的按钮颜色还是样式而点击)

示意图

最后

一个完整的ABTest系统,不单单只做分流,还会给用户(我们程序员)提供一个方便可配置的后台系统,做完实验提供数据报表展示等等等~

微信公众号不支持外链,在后台回复“AB”得到更多的ABTest资料

参考资料:

  • oldj.net/blog/tag/a-…
  • www.infoq.cn/article/BuP…
  • www.jianshu.com/p/de8d9f0b1…
  • liyaoli.com/2018-04-29/…
  • zhuanlan.zhihu.com/p/25319221
  • zhuanlan.zhihu.com/p/52424409
  • qiankunli.github.io/2018/06/27/…

乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,关注即可获取!

转发到朋友圈是对我最大的支持!

觉得我的文章写得不错,点赞!

近期推荐:最低价购买云服务器+搭建教程

本文转载自: 掘金

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

如何优雅地将Token参数转换成userId

发表于 2019-10-29

在实际项目中,我们往往会发放一个token凭证给前端,前端在每次请求的时候通过请求参数或者请求头将token传给后端进行验证。后端在获得token,验证通过之后会将token转成实际需要的参数,比如userId。

在SrpingBoot项目中,由于在请求参数中并没有userId这个参数,所以我们无法通过方法形参获取到userId,因此我们需要通过HttpServletWrapper来将token转换成userId参数。

1、继承HttpServletWrapper类,并重写getParameterValues方法

注:此处并没有使用真正的token,而是用一个map模拟token存储,
token为123和456为有效token,分别对应着userId = 1和userId = 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
复制代码/**
* Token请求包装类,将token字段转换成userId字段
* @author yan
* @date 2019年10月17日
*/
public class TokenHttpServletWrapper extends HttpServletRequestWrapper{
private Logger logger = LoggerFactory.getLogger(getClass());

private Map<String,Integer> tokenMap; //模拟token

public TokenHttpServletWrapper(HttpServletRequest request) {
super(request);
tokenMap = new HashMap<>() {
{
put("123", 1);
put("456", 2);
}
};
}


@Override
public String[] getParameterValues(String name) {
//如果请求参数不是userId,则跳过
if(!"userId".equals(name)) {
return super.getParameterValues(name);
}
//检验token,转换成相应的userId
String token = super.getParameter("token");
if(token == null) {
return null;
}
logger.debug("token:" + token);
Integer userId = tokenMap.get(token);
logger.debug("userId:" + userId);
return userId == null ? null : new String[] {String.valueOf(userId)};
}
}

2、定义过滤器,判断是否有token参数或者token参数是否有效

AbstractFilter是自定义过滤器抽象类,主要用于添加排除路径功能,其他用法与过滤器一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码public class TokenFilter extends AbstractFilter{

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = request.getParameter("token");
if(!"123".equals(token) && !"456".equals(token)) {
CodeResult codeResult = new CodeResult(CodeEnum.UNAUTHORIZED, null);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(BeanUtil.beanToJson(codeResult));
return;
}
chain.doFilter(new TokenHttpServletWrapper(request), response);
}

}

3、配置过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Configuration
public class TokenFilterConfig {
@Bean
public FilterRegistrationBean<Filter> securityFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
Filter filter = new TokenFilter();
registration.setFilter(filter);
registration.addUrlPatterns("/*");
registration.setName("tokenFilter");
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registration;
}
}

4、在Controller方法中使用userId作为形参

5、请求访问该接口

  1. 当没有token参数时或者token参数无效时,返回未授权信息

  1. 当token参数有效时,获取到相应的userId

本文转载自: 掘金

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

《我们一起进大厂》系列- Redis基础

发表于 2019-10-28

你知道的越多,你不知道的越多

点赞再看,养成习惯

GitHub上已经开源github.com/JavaFamily,有一线大厂面试点脑图,欢迎Star和完善

面试开始

一个大腹便便,穿着格子衬衣的中年男子,拿着一个满是划痕的mac向你走来,看着快秃顶的头发,心想着肯定是尼玛顶级架构师吧!但是我们腹有诗书气自华,虚都不虚。

小伙子您好,看你简历上写了你项目里面用到了Redis,你们为啥用Redis?

心里忍不住暗骂,这叫啥问题,大家不都是用的这个嘛,但是你不能说出来。

认真回答道:帅气迷人的面试官您好,因为传统的关系型数据库如Mysql已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件,目前市面上比较常用的缓存中间件有 Redis 和 Memcached 不过中和考虑了他们的优缺点,最后选择了Redis。

至于更细节的对比朋友们记得查阅Redis 和 Memcached 的区别,比如两者的优缺点对比和各自的场景,后续我有时间也会写出来。

那小伙子,我再问你,Redis有哪些数据结构呀?

String、Hash、List、Set、SortedSet。

这里我相信99%的读者都能回答上来Redis的5个基本数据类型。如果回答不出来的小伙伴我们就要加油补课哟,大家知道五种类型最适合的场景更好。

但是,如果你是Redis中高级用户,而且你要在这次面试中突出你和其他候选人的不同,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。

如果你还想加分,那你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,这个时候面试官得眼睛就开始发亮了,心想这个小伙子有点东西啊。

**注:本人在面试回答到Redis相关的问题的时候,经常提到BloomFilter(布隆过滤器)这玩意的使用场景是真的多,而且用起来是真的香,原理也好理解,看一下文章就可以在面试官面前侃侃而谈了,不香么?下方传送门 ↓**

避免缓存击穿的利器之BloomFilter

如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。严重的话会出现缓存雪崩,我们一般需要在时间上加一个随机值,使得过期时间分散一些。

电商首页经常会使用定时任务刷新缓存,可能大量的数据失效时间都十分集中,如果失效时间一样,又刚好在失效的时间点大量用户涌入,就有可能造成缓存雪崩

那你使用过Redis分布式锁么,它是什么回事?

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!

对方这时会显露笑容,心里开始默念:嗯,这小子还不错,开始有点意思了。假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答Redis关键的一个特性:Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

不过,增量式迭代命令也不是没有缺点的: 举个例子, 使用 SMEMBERS 命令可以返回集合键当前包含的所有元素, 但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。

使用过Redis做异步队列么,你是怎么用的?

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?

list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方接着追问能不能生产一次消费多次呢?

使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列。

如果对方继续追问 pub/su b有什么缺点?

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如RocketMQ等。

如果对方究极TM追问Redis如何实现延时队列?

这一套连招下来,我估计现在你很想把面试官一棒打死(面试官自己都想打死自己了怎么问了这么多自己都不知道的),如果你手上有一根棒球棍的话,但是你很克制。平复一下激动的内心,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

到这里,面试官暗地里已经对你竖起了大拇指。并且已经默默给了你A+,但是他不知道的是此刻你却竖起了中指,在椅子背后。

Redis是怎么持久化的?服务主从数据怎么交互的?

RDB做镜像全量持久化,AOF做增量持久化。因为RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。

这里很好理解,把RDB理解为一整个表全量的数据,AOF理解为每次操作的日志就好了,服务器重启的时候先把表的数据全部搞进去,但是他可能不完整,你再回放一下日志,数据不就完整了嘛。不过Redis本身的机制是 AOF持久化开启且存在AOF文件时,优先加载AOF文件;AOF关闭或者AOF文件不存在时,加载RDB文件;加载AOF/RDB文件城后,Redis启动成功; AOF/RDB文件存在错误时,Redis启动失败并打印错误信息

对方追问那如果突然机器掉电会怎样?

取决于AOF日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

对方追问RDB的原理是什么?

你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

注:回答这个问题的时候,如果你还能说出AOF和RDB的优缺点,我觉得我是面试官在这个问题上我会给你点赞,两者其实区别还是很大的,而且涉及到Redis集群的数据同步问题等等。想了解的伙伴也可以留言,我会专门写一篇来介绍的。

Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。

是否使用过Redis集群,集群的高可用怎么保证,集群的原理是什么?

Redis Sentinal 着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster 着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

面试结束

小伙子你可以的,什么时候有时间来上班啊,要不明天就来吧?

你强装镇定,这么急啊我还需要租房,要不下礼拜一吧。

好的 心想这小子这么NB是不是很多Offer在手上,不行我得叫hr给他加钱。

能撑到最后,你自己都忍不住自己给自己点个赞了!

(暗示点赞,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我喜欢)。

总结

在技术面试的时候,不管是Redis还是什么问题,如果你能举出实际的例子,或者是直接说自己开发过程的问题和收获会给面试官的印象分会加很多,回答逻辑性也要强一点,不要东一点西一点,容易把自己都绕晕的。

还有一点就是我问你为啥用Redis你不要一上来就直接回答问题了,你可以这样回答:

帅气的面试官您好,首先我们的项目DB遇到了瓶颈,特别是秒杀和热点数据这样的场景DB基本上就扛不住了,那就需要缓存中间件的加入了,目前市面上有的缓存中间件有 Redis 和 Memcached ,他们的优缺点……,综合这些然后再结合我们项目特点,最后我们在技术选型的时候选了谁。

如果你这样有条不紊,有理有据的回答了我的问题而且还说出这么多我问题外的知识点,我会觉得你不只是一个会写代码的人,你逻辑清晰,你对技术选型,对中间件对项目都有自己的理解和思考,说白了就是你的offer有戏了。

鸣谢

老钱:掌阅服务端技术专家,互联网分布式高并发技术十年老兵,现任字节跳动技术专家。

以Redis系列为开始是他给的建议,并且允许我以他其中一篇的风格和大部分内容为自己开始写博客的风格。

点关注,不迷路

好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。

我后面会每周都更新几篇一线互联网大厂面试和常用技术栈相关的文章,非常感谢人才们能看到这里,如果这个文章写得还不错,觉得「敖丙」我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!

白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

敖丙 | 文 【原创】

如果本篇博客有任何错误,请批评指教,不胜感激 !


文章每周持续更新,可以微信搜索「 三太子敖丙 」第一时间阅读和催更(比博客早一到两篇哟),本文 GitHub github.com/JavaFamily 已经收录,有一线大厂面试点思维导图,也整理了很多我的文档,欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。

本文转载自: 掘金

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

websoket原理和实战 websoket原理和实战

发表于 2019-10-27

websoket原理和实战

概述

项目地址:github.com/longxiaonan…

项目包结构

两个包:
com.javasea.web.websocket.springb.websocket
使用实现WebSocketConfigurer接口的方式实现

com.javasea.web.websocket.springb.websocket2
通过注解@ServerEndpoint方式实现

场景引用

场景:页面需要实时显示被分配的任务,页面需要实时显示在线人数。

思考:像这样的消息功能怎么实现? 如果网页不刷新,服务端有新消息如何推送到浏览器?

解决方案,采用轮询的方式。即:通过js不断的请求服务器,查看是否有新数据,如果有,就获取到新数据。
这种解决方法是否存在问题呢?

当然是有的,如果服务端一直没有新的数据,那么js也是需要一直的轮询查询数据,这就是一种资源的浪费。
那么,有没有更好的解决方案? 有!那就是采用WebSocket技术来解决。

什么是WebSocket?

WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。 WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。 它是一种在单个TCP连接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为标准。

什么叫做全双工和半双工?

比如对讲机,说话的时候就听不到对方说话,那么就是半双工。

我们打电话的时候说话的同时也能听到对方说话,就是全双工。

http与websocket的区别

http协议是短连接,因为请求之后,都会关闭连接,下次重新请求数据,需要再次打开链接。

WebSocket协议是一种长链接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通讯。

浏览器支持情况

查看:caniuse.com/#search=web…

服务器支持情况:Tomcat 7.0.47+以上才支持。

快速入门

创建项目

配置pom.xml

  • 集成javaee
1
2
3
4
5
6
复制代码 <dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
  • 配置tomcat插件
1
2
3
4
5
6
7
8
9
复制代码 <plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8082</port>
<path>/</path>
</configuration>
</plugin>

之后启动服务只需要在maven中直接运行即可。

pom的详细配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
复制代码<?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>
<groupId>com.iee</groupId>
<artifactId>javasea-web-websoecket-quickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>7.0</version>
<!--<scope>provided</scope>-->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--maven编译插件, 指定jdk为1.8 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- 使用jdk进行编译 -->
<fork>true</fork>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 配置Tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8082</port>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
</project>

websocket的相关注解说明

  • @ServerEndpoint("/websocket/{uid}")
    申明这是一个websocket服务
    需要指定访问该服务的地址,在地址中可以指定参数,需要通过{}进行占位
  • @OnOpen
    用法:public void onOpen(Session session, @PathParam(“uid”) String uid) throws
    IOException{}
    该方法将在建立连接后执行,会传入session对象,就是客户端与服务端建立的长连接通道
    通过@PathParam获取url申明中的参数
  • @OnClose
    用法:public void onClose() {}
    该方法是在连接关闭后执行
  • @OnMessage
    用法:public void onMessage(String message, Session session) throws IOException {}

客户端消息到来时调用,包含会话Session,根据消息的形式,如果是文本消息,传入String类型参数或者Reader,如果是二进制消息,传入byte[]类型参数或者InputStream。

message:发来的消息数据
session:会话对象(也是通道)
发送消息到客户端
用法:session.getBasicRemote().sendText(“你好”);
通过session进行发送。

实现websocket服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码@ServerEndpoint("/websocket/{uid}")
public class MyWebSocket {
  @OnOpen
  public void onOpen(Session session, @PathParam("uid") String uid) throws
IOException {
    // 连接成功
    session.getBasicRemote().sendText(uid + ",你好,欢迎连接WebSocket!");
 }
  @OnClose
  public void onClose() {
    System.out.println(this + "关闭连接");
 }
  @OnMessage
  public void onMessage(String message, Session session) throws IOException {
    System.out.println("接收到消息:" + message);
    session.getBasicRemote().sendText("消息已收到.");
 }
  @OnError
  public void onError(Session session, Throwable error) {
    System.out.println("发生错误");
    error.printStackTrace();
 }
}

maven中启动tomcat:

1
复制代码mv tomcat7:run

也可以用上文中在IDE中直接启动。

测试

一共有三种测试方式,直接js脚本方式、chrome插件方式或者通过在线工具进行测试:

  • 直接js脚本方式,直接用如下代码进行测试:
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
复制代码var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
socket = new WebSocket("ws://localhost:8080/websocket2/22");
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
//此时可以尝试刷新页面
}
//离开页面时,关闭socket
//jquery1.8中已经被废弃,3.0中已经移除
// $(window).unload(function(){
// socket.close();
//});
}

浏览器随便打开一个网页,然后粘贴到console下,回车即可

  • chrome插件方式,需要安装chrome插件,Simple WebSocket Client:
    chrome.google.com/webstore/de…

  • 在线工具进行测试(推荐):www.websocket-test.com/

我一直测试失败,还没找到原因。下文整合springboot的测试成功。

编写js客户端

在webapp下编写两个html文件

  • websocket.html内容如下
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
复制代码<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
const socket = new WebSocket("ws://localhost:8082/websocket/1");
// 连接建立时触发
socket.onopen = (ws) => {
console.log("建立连接!", ws);
};
// 客户端接收服务端数据时触发
socket.onmessage = (ws) => {
console.log("接收到消息 >> ", ws.data);
};
// 连接关闭时触发
socket.onclose = (ws) => {
console.log("连接已断开!", ws);
};
// 通信发生错误时触发
socket.onerror = (ws) => {
console.log("发送错误!", ws);
};
// 2秒后向服务端发送消息
setTimeout(() => {
// 使用连接发送数据
socket.send("发送一条消息试试");
}, 2000);
// 5秒后断开连接
setTimeout(() => {
// 关闭连接
socket.close();
}, 5000);
</script>
</body>
</html>
  • websocket2.html内容如下
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
复制代码<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script type="text/javascript">
function WebSocketTest()
{
if ("WebSocket" in window)
{
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://localhost:8082/websocket/1");
ws.onopen = function()
{
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
ws.onmessage = function (evt)
{
var received_msg = evt.data;
alert("数据已接收...");
};
ws.onclose = function()
{
// 关闭 websocket
alert("连接已关闭...");
};
}
else
{
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</head>
<body>
<div id="sse">
<a href="javascript:WebSocketTest()">运行 WebSocket</a>
</div>
</body>
</html>

在浏览器请求http://localhost:8082/websocket2.html

http://localhost:8082/websocket.html也可以进行测试,区别在于websocket.html是在打开页面的时候里面自动去连接websocket服务,websocket2.html是还需要点击一下才去连接。

emmm,失败的,还没找到原因,下文整合springboot,测试是成功的。

整合springboot

使用springboot内置tomcat时,就不需要引入javaee-api了,spring-boot已经包含了。

springboot的高级组件会自动引用基础的组件,像spring-boot-starter-websocket就引入了spring-boot-starter-web和spring-boot-starter,所以不要重复引入

springboot已经做了深度的集成和优化,注意是否添加了不需要的依赖、配置或声明。由于很多讲解组件使用的文章是和spring集成的,会有一些配置。在使用springboot时,由于springboot已经有了自己的配置,再这些配置有可能导致各种各样的异常。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码<?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">
<parent>
<artifactId>javasea-web-websocket</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>javasea-web-websocket-springb</artifactId>

<dependencies>
<!-- https://mvnrepository.com/artifact/javax/javaee-api -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins> <!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<!-- 使用jdk进行编译 -->
<fork>true</fork>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>

springboot有两种方式实现websocket

  • 通过注解@ServerEndpoint方式实现

webSocket核心是@ServerEndpoint这个注解。这个注解是Javaee标准里的注解,tomcat7以上已经对其进行了实现,如果是用传统方法使用tomcat发布的项目,只要在pom文件中引入javaee标准即可使用。

快速入门中的例子就是通过@ServerEndpoint来实现的WebSocket服务,在整合springboot的时候需要额外配置Config类,创建一个ServerEndpointExporter();

1
2
3
4
5
6
7
复制代码@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
  • 使用实现WebSocketConfigurer接口的方式实现

下文就是这种方式的实现

编写WebSocketHandler

在Spring中,处理消息的具体业务逻辑需要实现WebSocketHandler接口。

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
复制代码package com.javasea.web.websocket.springb.websocket;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;

/**
* @Description 在Spring中,处理消息的具体业务逻辑需要实现WebSocketHandler接口。
* @Author longxiaonan@163.com
* @Date 16:50 2019/10/27 0027
**/
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
System.out.println("获取到消息 >> " + message.getPayload());
session.sendMessage(new TextMessage("消息已收到"));
if (message.getPayload().equals("10")) {
for (int i = 0; i < 10; i++) {
//回写消息到client
session.sendMessage(new TextMessage("消息 -> " + i));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
session.sendMessage(new TextMessage("欢迎连接到ws服务"));
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("断开连接!");
}
}

编写配置类来实现WebSocket服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码package com.javasea.web.websocket.springb.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws").setAllowedOrigins("*");
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}

编写启动类

1
2
3
4
5
6
7
8
9
10
11
复制代码package com.javasea.web.websocket.springb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class APPlication {
public static void main(String[] args) {
SpringApplication.run(APPlication.class, args);
}
}

测试

在线进行测试,url:ws://localhost:8080/ws

用上文的html页面也可以测试的,修改地址为ws://localhost:8080/ws然后在文件夹下直接用浏览器打开即可。

添加拦截器

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
复制代码package com.javasea.web.websocket.springb.websocket;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.Map;

@Component
public class MyHandshakeInterceptor implements HandshakeInterceptor {
/*** 握手之前,若返回false,则不建立链接 */
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { //将用户id放入socket处理器的会话(WebSocketSession)中
attributes.put("uid", 1001);
System.out.println("开始握手。。。。。。。");
return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("握手成功啦。。。。。。");
}
}

将拦截器添加到websocket服务中:

就是在上文的config中添加addInterceptors(this.myHandshakeInterceptor);。

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
复制代码package com.javasea.web.websocket.springb.websocket;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Autowired
private MyHandshakeInterceptor myHandshakeInterceptor;

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/ws").setAllowedOrigins("*").addInterceptors(this.myHandshakeInterceptor);
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}

测试拦截器

在MyHandler类afterConnectionEstablished方法下输出获取到的uid。

1
2
3
4
5
复制代码@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("uid =>" + session.getAttributes().get("uid"));
session.sendMessage(new TextMessage("欢迎连接到ws服务"));
}

连接websocket服务 ws://localhost:8080/ws,console输出:

1
2
复制代码握手成功啦。。。。。。
uid =>1001

说明测试成功。

项目地址

github地址:github.com/longxiaonan…

参考

juejin.cn/post/684490…

blog.csdn.net/hry2015/art…

本文转载自: 掘金

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

Java程序员必备:查看日志常用的linux命令

发表于 2019-10-27

前言

趁周末,复习一下鸟哥的linux私房菜,看到文件内容查阅部分,做个笔记,哈哈,希望对你有帮助哦。

cat

cat : 由第一行开始显示文件所有内容

参数说明

1
2
3
4
5
6
复制代码cat [-AbEnTv]
参数:
-A : 相当于-vET 的整合参数,可列出一些特殊字符,而不是空白而已
-b :列出行号,仅针对非空白行做行号显示,空白行不标行号
-E :将结尾的断行字符$显示出来
-n : 打印行号,连同空白行也会有行号,与-b的参数不同

范例demo

范例一:

查看cattest.txt的内容

1
2
3
4
5
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ whx]# cat cattest.txt 
test cat command
jaywei

#####

范例二:

查看cattest.txt的内容,并且显示行号

1
2
3
4
5
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ whx]# cat -n cattest.txt 
1 test cat command
2 jaywei
3
4 #####

适用场景

  • cat是Concatenate的缩写,主要功能是将一个文件的内容连续显示在屏幕上面。
  • 一般文件内容行数较少时,如40行之内,适合用cat。
  • 如果是一般的DOS文件时,就需要特别留意一些奇怪的符号,例如断行与[Tab]等,要显示出来,就得加入-a之类的参数了。

tac

tac : 从最后一行开始显示,可以看出tac是cat的倒写形式

范例demo

1
2
3
4
5
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ whx]# tac  cattest.txt 
#####

jaywei
test cat command

适用场景

  • tac 的功能跟cat相反,cat是由“第一行到最后一行连续显示在屏幕上”,而tac则是“由最后一行到第一行反向在屏幕上显示出来”。

head

head :显示文件开头的内容,以行为单位,默认文件开头的前10行

参数说明

1
2
3
4
5
复制代码head [OPTION]... FILE...
-n<行数> 显示的行数
-q 隐藏文件名
-v 显示文件名
-c<字节> 显示字节数

范例demo

显示 sentinel.conf 文件前12行

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# head -n 12  sentinel.conf 
# Example sentinel.conf

# *** IMPORTANT ***
#
# By default Sentinel will not be reachable from interfaces different than
# localhost, either use the 'bind' directive to bind to a list of network
# interfaces, or disable protected mode with "protected-mode no" by
# adding it to this configuration file.
#
# Before doing that MAKE SURE the instance is protected from the outside
# world via firewalling or other means.
#

tail

查看文件的内容,也是以行为单位,默认10行,从尾往前看。监听Java动态日志时,一般跟-f参数配合使用。

参数说明

1
2
3
4
5
6
复制代码tail [参数] [文件]  
-f 循环读取
-q 不显示处理信息
-v 显示详细的处理信息
-c<数目> 显示的字节数
-n<行数> 显示文件的尾部 n 行内容

范例demo

范例一

显示sentinel.conf文件的最后12行

1
2
3
4
5
6
7
8
9
10
11
12
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# tail -n 12  sentinel.conf 
# <role> is either "leader" or "observer"
#
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected slave
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

范例二

持续检测sentinel.conf的内容

1
2
3
4
5
6
7
8
9
10
11
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# tail -f sentinel.conf
# The arguments from-ip, from-port, to-ip, to-port are used to communicate
# the old address of the master and the new address of the elected slave
# (now a master).
#
# This script should be resistant to multiple invocations.
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
<==要等到输入[ctrl]-c 之后才离开tail 这个命令的检测

范例三

持续检测sentinel.conf的内容,并匹配redis关键字。匹配关键字,一般用grep ,tail 一般也会跟grep 搭档使用。

1
2
3
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# tail -f sentinel.conf | grep redis
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh
<==要等到输入[ctrl]-c 之后才离开tail 这个命令的检测

适用场景

  • tial -f 被用来动态监听Java日志,开发联调经常使用到,它一般跟grep 一起搭档使用。

more

more :一页一页地显示文件内容

参数说明

1
2
3
4
5
6
7
8
9
10
复制代码more [-dlfpcsu] [-num] [+/pattern] [+linenum] [fileNames..]
参数:
-num :一次显示的行数
-p :不以卷动的方式显示每一页,而是先清除萤幕后再显示内容
-c : 跟 -p 相似,不同的是先显示内容再清除其他旧资料
-s : 当遇到有连续两行以上的空白行,就代换为一行的空白行
+/pattern : 在每个文档显示前搜寻该字串(pattern),然后从该字串之后开始显示
-u :不显示下引号 (根据环境变数 TERM 指定的 terminal 而有所不同)
+num : 从第 num 行开始显示
fileNames :欲显示内容的文档,可为复数个数

常用操作命令

1
2
3
4
5
6
7
8
9
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# more sentinel.conf 
# Example sentinel.conf
...(中间省略) ...
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Slaves are auto-discovered, so you don't need to specify slaves in
--More--(29%)

仔细看上面的范例,如果more后面接的文件内容行数大于屏幕输出的行数时,就会出现类似上面的图示。重点在最后一行,最后一行会显示出目前显示的百分比,而且还可以在最后一行输入一些有用的命令。在more这个程序的运行过程中,你可以使用一些常用的操作命令:

  • 空格键 :代表往下翻一页
  • Enter : 代表往下滚动一行
  • /字符串 :代表在这个显示的内容当中,向下查询“字符串” 这个关键字
  • :f :立刻显示出文件名以及目前显示的行数
  • q :代表立刻离开more,不再显示该文件内容
  • b或[Ctrl]-b :代表往回翻页,不过这操作只对文件有用,对管道无用。

最常用的是:按q离开,按空格键往下翻页,按b往回翻页,以及/字符串搜索功能,请看以下demo

范例demo

范例一

1
2
3
4
5
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# more -10 sentinel.conf
# Example sentinel.conf
...(此处省略)...
# Before doing that MAKE SURE the instance is protected from the outside
--More--(4%)

分页查看sentinel.conf文件,一页展示10行。按下空格键,可以往下翻页,

1
2
3
4
5
6
7
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# more -10 sentinel.conf
# Example sentinel.conf
...(此处省略)...
# protected-mode no
# port <sentinel-port>
# The port that this sentinel instance will run on
--More--(7%)

按下b,可以回退到上一页

1
2
3
4
5
复制代码# *** IMPORTANT ***
...(此处省略)...
# Before doing that MAKE SURE the instance is protected from the outside
# world via firewalling or other means.
--More--(5%)

按下q,可以立刻离开more

1
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]#

范例二

如果想在sentinel.conf文件中,搜寻sentinel关键字,可以这样做

1
2
3
4
5
复制代码[root@iZ2zehkwp9rwg4azsvnjbuZ redis-4.0.7]# more -10  sentinel.conf
# Example sentinel.conf
...(此处省略)...
# Before doing that MAKE SURE the instance is protected from the outside
/sentinel 输入/之后,光标就会自动跑到最下面一行等待输入

如同上面的说明,输入了/之后,光标就会跑到最下面一行,并且等待你的输入,你输入了字符串并按下[Enter]之后,more就会开始向下查询该字符串,而重复查询同一个字符串,可以直接按下n即可。最后不想看了,就按下q离开more。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码# Before doing that MAKE SURE the instance is protected from the outside
/sentinel
...skipping
# protected-mode no

# port <sentinel-port>
# The port that this sentinel instance will run on
port 26379

# sentinel announce-ip <ip>
# sentinel announce-port <port>
#
/
...skipping
# Example:
#
# sentinel announce-ip 1.2.3.4

# dir <working-directory>
# Every long running process should have a well-defined working directory.
# For Redis Sentinel to chdir to /tmp at startup is the simplest thing
# for the process to don't interfere with administrative tasks such as
# unmounting filesystems.
--More--(23%)

适用场景

  • more使用日志比较大的文件查看,可以一页一页查看,不会让前面的数据看不到。

less

less 与 more 类似,但less的用法比起more又更加有弹性。

  • 若使用了less时,就可以使用下、下等按键的功能来往前往后翻看文件。
  • 除此之外,在less里头可以拥有更多的查询功能。不止可以向下查询,也可以向上查询。

常用操作命令

  • 空格键:往下翻动一页
  • [pagedown]: 向下翻动一页
  • [pageup]: 向上翻动一页
  • Enter : 代表往下滚动一行
  • y :向前滚动一行
  • /字符串:向下搜索”字符串”的功能
  • ?字符串:向上搜索”字符串”的功能
  • n:重复前一个搜索(与 / 或 ? 有关)
  • N:反向重复前一个搜索(与 / 或 ? 有关)
  • q: 离开less这个程序
  • b 向后翻一页

范例demo

范例一

在sentinel.conf文件中,搜寻sentinel关键字,如下

1
复制代码less sentinel.conf

输入反斜杠/,输入关键字sentinel,回车

重复前一个搜索,可以按n,反向重复前一个搜索,按N

范例二

Linux 动态查看日志文件,一般用tail -f ,但是我们也可以用less+ F 实现。

1
复制代码less + file-name + 命令 F =  tail -f + file-name

我们经常用tail -f +grep 关键字,动态查找报错的日志,也可以用less实现。先输入shirft+g,到达文件结尾

然后输入?,输入搜索关键字,如sentinel,回车,然后按n键往上搜索,效果是不是也不错?尤其日志文件动态刷新太快的时候,奸笑脸。

适用场景

  • less适合日志比较大的文件查看,可以一页一页查看,并且比more更灵活,也可以动态查看日志,我一般用它查看Java日志。

小结

本文总结了查看文件日志的几个linux命令,cat、tac、head、tail、more、less,其中less真的很适合日常开发日志查看,非常推荐less。

参考与感谢

  • 《鸟哥的linux私房菜》
  • Linux 命令大全 |菜鸟教程

个人公众号

  • 如果你是个爱学习的好孩子,可以关注我公众号,一起学习讨论。
  • 如果你觉得本文有哪些不正确的地方,可以评论,也可以关注我公众号,私聊我,大家一起学习进步哈。

本文转载自: 掘金

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

Spring Boot 2X(十一):全局异常处理

发表于 2019-10-26

前言

在 Java Web 系统开发中,不管是 Controller 层、Service 层还是 Dao 层,都有可能抛出异常。如果在每个方法中加上各种 try catch 的异常处理代码,那样会使代码非常繁琐。在Spring MVC 中,我们可以将所有类型的异常处理从各个单独的方法中解耦出来,进行异常信息的统一处理和维护。

在 Spring MVC 中全局异常捕获处理的解决方案通常有两种方式:

1.使用 @ControllerAdvice + @ExceptionHandler 注解进行全局的 Controller 层异常处理。

2.实现 org.springframework.webb.servlet.HandlerExceptionResolver 接口中的 resolveException 方法。

使用 @ControllerAdvice + @ExceptionHandler 注解

1.定义统一异常处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码@ControllerAdvice
public class GlobalExceptionHandler {

private Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) {
log.error("ExceptionHandler ===>" + e.getMessage());
e.printStackTrace();
// 这里可根据不同异常引起的类做不同处理方式
String exceptionName = ClassUtils.getShortName(e.getClass());
log.error("ExceptionHandler ===>" + exceptionName);
ModelAndView mav = new ModelAndView();
mav.addObject("stackTrace", e.getStackTrace());
mav.addObject("errorMessage", e.getMessage());
mav.addObject("url", req.getRequestURL());
mav.setViewName("forward:/error/500");
return mav;
}
}

其中 @ExceptionHandler(value = Exception.class) 中的捕获异常 value 可以自定义,如下:

类型 描述
NullPointerException 当应用程序试图访问空对象时,则抛出该异常
SQLException 提供关于数据库访问错误或其他错误信息的异常
IndexOutOfBoundsException 指示某排序索引(例如对数组、字符串或向量的排序)超出范围时抛出
NumberFormatException 当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常
FileNotFoundException 当试图打开指定路径名表示的文件失败时,抛出此异常
IOException 当发生某种I/O异常时,抛出此异常。此类是失败或中断的I/O操作生成的异常的通用类
ClassCastException 当试图将对象强制转换为不是实例的子类时,抛出该异常
ArrayStoreException 试图将错误类型的对象存储到一个对象数组时抛出的异常
IllegalArgumentException 抛出的异常表明向方法传递了一个不合法或不正确的参数
ArithmeticException 当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例
NegativeArraySizeException 如果应用程序试图创建大小为负的数组,则抛出该异常
NoSuchMethodException 无法找到某一特定方法时,抛出该异常
SecurityException 由安全管理器抛出的异常,指示存在安全侵犯
UnsupportedOperationException 当不支持请求的操作时,抛出该异常
RuntimeException 是那些可能在Java虚拟机正常运行期间抛出的异常的超类

当捕获到响应的异常类型时,会进入 defaultErrorHandler() 方法中的逻辑:把异常信息放入 model,跳转至 /error/500 请求URL。

2.异常信息展现

视图控制器配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

/**
* 视图控制器配置
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("/index");//设置默认跳转视图为 /index
registry.addViewController("/error/500").setViewName("/error/500");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
super.addViewControllers(registry);

}

}

视图模板

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Exception</h1>
<h3 th:text="${url}"></h3>
<h3 th:text="${errorMessage}"></h3>
<p th:each="line : ${stackTrace}" th:text="${line}"> </p>
</body>
</html>

3.测试异常类

1
2
3
4
5
6
7
8
9
复制代码@Controller
public class TestController {

@GetMapping("/index")
public String hello() {
int x = 1 / 0;
return "hello";
}
}

4.运行测试

浏览器访问:http://127.0.0.1:8080/index

@ControllerAdvice 还能结合 @ModelAttribute 、@InitBinder 注解一起使用,实现全局数据绑定和全局数据预处理等功能。

实现 HandlerExceptionResolver 接口

1.定义统一异常处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码@Component
public class GlobalHandlerExceptionResolver implements HandlerExceptionResolver {

private Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
Exception e = new Exception();
//处理 UndeclaredThrowableException
if (ex instanceof UndeclaredThrowableException) {
e = (Exception) ((UndeclaredThrowableException) ex).getUndeclaredThrowable();
} else {
e = ex;
}
e.printStackTrace();
//这里可以根据不同异常引起的类做不同处理方式
String exceptionName = ClassUtils.getShortName(e.getClass());
if(exceptionName.equals("ArrayIndexOutOfBoundsException")) {
log.error("GlobalHandlerExceptionResolver resolveException ===>" + exceptionName);
ModelAndView mav = new ModelAndView();
mav.addObject("stackTrace", e.getStackTrace());
mav.addObject("exceptionName", exceptionName);
mav.addObject("errorMessage", e.getMessage());
mav.addObject("url", request.getRequestURL());
mav.setViewName("forward:/error/500");
return mav;
}
return null;
}

}

UndeclaredThrowableException 异常通常是在 RPC 接口调用场景或者使用 JDK 动态代理的场景时发生。如果不预先处理转换,测试捕获到的异常则为 UndeclaredThrowableException,而不是真实的异常对象。

2.异常信息展现 同上

3.测试异常类

1
2
3
4
5
6
7
8
9
10
11
复制代码@Controller
public class TestController {

@GetMapping("/test")
public String test() {
String[] ss = new String[] { "1", "2" };
System.out.print(ss[2]);
return "hello";
}

}

4.测试运行

测试前先把 @ControllerAdvice 注释了。
浏览器访问:http://127.0.0.1:8080/test

示例代码

github

码云

非特殊说明,本文版权归 朝雾轻寒 所有,转载请注明出处.

原文标题:Spring Boot 2.X(十一):全局异常处理

原文地址:https://www.zwqh.top/article/info/20

如果文章对您有帮助,请扫码关注下我的公众号,文章持续更新中…

本文转载自: 掘金

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

Java基础系列-跨越 Java8

发表于 2019-10-26

虽然 Java8 已经发布了很长的时间,而且 Java8 中有很多特性可以提升代码的效率和安全,但是大多数 Java 程序员还是没有跨过 Java8 这个坎,Benjamin 在 2014 年写下的这篇 Java8 的入门教程我觉得非常不错,或许可以帮助你跨过 Java8 这个坎。


这份教程会指导你一步一步学习 Java8 的新特性。按照先后顺序,这篇文章中包括以下的内容:接口的 default 方法,lambda 表达式,方法引用,可复用注解,还有一些 API 的更新,streams,函数式接口,map 的扩展和新的 Date Api。

本文没有大段的文字,只有带注释的代码片段,希望你能喜欢!

接口的 default 方法

Java8 允许在接口中实现具体的方法,只需要在方法前加上 default 关键字就行。这一特性也称之为虚拟扩展方法。这里是第一个例子:

1
2
3
4
5
6
复制代码interface Formual {    
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}

在上面的例子中,Formual 接口定义了一个 default 方法 sqrt,接口的实现类只要需要实现 calculate 方法,sqrt 方法开箱即用。

1
2
3
4
5
6
7
8
9
复制代码Formula formula = new Formula() {
@Override
public double calculate(int a) {
return sqrt(a * 100);
}
};

formula.calculate(100); // 100.0
formula.sqrt(16); // 4.0

上面的代码匿名实现了 Formual 接口。代码相当的冗长,用了 6 行代码才实现了 sqrt(a * 100) 的功能。在下一节中可以通过 Java8 的特性优雅的完成这个功能。

Lambda 表达式

先看一下之前版本的 Java 中如何实现对一个字符串 List 进行排序的功能:

1
2
3
4
5
6
7
8
复制代码List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});

静态方法 Collection.sort 接收一个字符串 List 和一个字符串的 Comparator 用于比较传入的字符串 List。通常的做法就是实现一个匿名的 Comparator 然后传入到 sort 方法中。

相比于使用匿名方法的冗长实现,Java8 可以通过 lambda 表达式用很短的代码来实现:

1
2
3
复制代码Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});

这个代码已经比之前的匿名方法短很多了,但是这个代码还可以更短一点:

1
复制代码Collections.sort(names, (String a, String b) -> b.compareTo(a));

注:使用 Collections.sort(names, (a,b)->b.compareTo(a)); 也可以

用一行代码就实现了方法,省略掉了 {} 和 return 关键字。但是其实还可以更短一点:

1
复制代码Collections.sort(names, (a, b) -> b.compareTo(a));

Java 编译器可以根据上下文判断出参数的类型,所以你也可以省略参数的类型。下面来探究一下 lambda 表达式更进阶的用法。

函数式接口

lambda 表达式和如何与 Java 的类型系统相匹配?每个 lambda 表达式都会被接口给定类型,所以每个函数式接口都至少声明一个 abstract 方法。每一个 lambda 表达式的参数类型都必须匹配这个抽象方法的参数。由于 default 关键字标识的方法不是抽象方法,可以在接口中添加任意多个 default 方法。

注:每一个 lambda 都是函数式的接口,所以使用了 @FunctionInterface 的 interface 都只能有一个抽象方法

可以将任意只包含一个抽象方法的接口当作 lambda 表达式。为了确保接口满足要求,需要在接口上添加 @FunctionalInterface 注解,如果加上注解接口中不止一个虚拟方法,编译器就会报错。如下的例子:

1
2
3
4
复制代码@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
1
2
3
复制代码Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted); // 123

但是省略 @FunctionalInterface 这个注解后,代码也可以正常工作。

方法引用

以上的示例代码可以通过静态方法引用进一步简化:

1
2
3
复制代码Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted); // 123

Java8 允许你使用 :: 来调用静态方法和构造函数的引用。上面的代码展示了如何引用一个静态方法。也可以通过同样的方法来引用对象方法:

1
2
3
4
5
复制代码class Something {
String startsWith(String s) {
return String.valueOf(s.charAt(0));
}
}
1
2
3
4
复制代码Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"

注:System.out::println 引用的 println 不是静态方法,因为 System.out 是一个对象

下面让来看看 :: 是如何在构造函数上起作用的。首先定义一个有着不同构造方法的类 Person:

1
2
3
4
5
6
7
8
9
10
11
复制代码class Person {
String firstName;
String lastName;

Person() {}

Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}

接下来定义一个 Person 工厂接口来创建新的 Person 对象:

1
2
3
复制代码interface PersonFactory<P extends Person> {
P create(String firstName, String lastName);
}

不需要手动实现一个工厂,而是通过构造函数的引用来完成新建 Person 对象:

1
2
复制代码PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

通过 Person::new 来获取到了 Person 类的构造方法引用。然后 Java 编译器会根据 PersonFactory::create 的参数来自动选择合适的构造函数。

注:lambda 、方法引用、构造函数引用都是由 @FunctionalInterface 的实例生成的,只有一个抽象方法的接口默认是一个 @FunctionalInterface,加了 @FunctionalInterface 注解的接口只能有一个抽象方法。

Lambda 的访问范围

相比于匿名实现的对象,lambda 表达式访问外部变量非常简单。lambda 表达式可以访问本地外部的 final 变量、成员变量和静态变量。

访问本地变量

lambda 表达式可以访问外部本地的 final 变量:

1
2
3
4
复制代码final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3

与匿名方式不同的是,num 变量可以不定义成 final,下面的这些代码也是可以工作的:

1
2
3
4
5
复制代码int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);

stringConverter.convert(2); // 3

然而 num 变量在编译的过程中会被隐式的编译成 final,下面的代码会出现编译错误:

1
2
3
4
复制代码int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;

在 lambda 表达式中也不能改变 num 的值。

访问成员变量和静态变量

与访问本地变量相反,在 lambda 表达式中对成员变量和静态变量可以进行读和写。这种访问变量的方式在匿名变量中也实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码class Lambda4 {
static int outerStaticNum;
int outerNum;

void testScopes() {
Converter<Integer, String> stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};

Converter<Integer, String> stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
}
}

注:外部的变量无法在 lambda 内部完成赋值操作,如果需要从 lambda 中获取到值,可以通过在外部定义一个 final 的数组,将需要带出的值放在数组里面带出来。

访问默认接口方法

还记得前面的 Formula 例子吗?Formula 接口定义了一个默认方法 sqrt 可以在每一个 Formula 的实例(包括匿名实现的对象)中访问。但是默这种方式在 lambda 表达式中不起作用。

默认方法不能通过 lambda 表达式访问,下面的代码无法编译通过:

1
复制代码Formula formula = (a) -> sqrt( a * 100);

内置的函数式接口

Java8 包含很多的内置函数式接口。有一些被广泛应用的接口如 Comparator 、Runnable。这些已经存在的接口都通过 @FunctionalInterface 进行了扩展,从而支持 lambda 表达式。

但是 Java8 中也有一些全新的函数式接口可以让你代码写的更轻松。其中一些来自于 Google Guava 库。即使你对这个库已经很熟悉了,但是还是应该密切注意这些接口是如何被一些有用的方法扩展的。

Predicates

Predicate 是一个参数的布尔函数。这个接口提供了很多的默认函数来组合成复杂的逻辑运算(与、非)。

1
2
3
4
5
6
7
8
9
10
复制代码Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo"); // true
predicate.negate().test("foo"); // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
Functions

Function 接收一个参数产生一个结果。默认方法可以用于多个方法组成的方法链。

1
2
3
4
复制代码Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123"); // "123"
Suppliers

Supplier 根据给定的类属性产生一个对象,Supplier 不支持传入参数。

1
2
复制代码Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
Consumers

Consumer 对输入的参数进行一系列预定义的流程进行处理。

1
2
复制代码Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
Comparators

Comparator 是在老版本的 Java 中就经常被使用的接口, Java8 在这个接口中加入了很多的默认方法。

1
2
3
4
5
6
7
复制代码Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2); // > 0
comparator.reversed().compare(p1, p2); // < 0
Optionals

Optional 不是一个函数式接口,而是一个消灭 NullPointerException 的好方法。这是下一节会对其原理进行重点讲解,下面来看看 Optional 是如何工作的。

Optional 是包含了一个值的容器,这个值可以为 null,也可以不为 null。考虑到方法可能会返回非 null 的值,也可能什么都不会返回。在 Java8 中,你可以让它不返回 null,或是返回一个 Optional 对象。

1
2
3
4
5
6
7
复制代码Optional<String> optional = Optional.of("bam");

optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"

注:这些内置的函数式接口都加上了 @FuncationalInterface 注解,算是一个语法糖,为不同类型的函数式方法提供了便捷方式,不用重头定义,在后面的 Stream 编程的各个阶段所需要的函数式接口都不同,这些内置的接口也为 Stream 编程做好了准备。

Streams

一个 java.util.Stream 代表着一系列可以执行一个或者多个操作的元素。Stream 操作可以是中间操作,也可以是终端操作。终端操作返回的是类型确定的结果。中间操作返回的是 Stream 对象本身,可以继续在同一行代码里面继续调用其他的方法链。

Stream 对象可以由 java.util.Collection 的对象创建而来,比各类 list 和 set (map 暂时不支持),Stream 可以支持串联和并行操作。

首先来看一下串联操作,通过 List 对象创建一个 Stream 对象:

1
2
3
4
5
6
7
8
9
复制代码List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Java8 中的 Collections 已经被扩展了,可以通过 Collection.stream() 或者 Collection.parallelStream() 来创建 Stream 对象,下面的内容将介绍最常用的 Stream 操作。

Filter

Filter 接受一个 Predicate 来过滤 Stream 中的所有元素。这个操作是一个中间操作,对过滤的结果可以调用另一个 Stream 操作(比如: forEach)。ForEach 接收一个 Consumer 参数,执行到过滤后的每一个 Stream 元素上。ForEach 是一个终端操作,所以不能在这个操作后调用其他的 Stream 操作。

1
2
3
4
5
复制代码stringCollection
.stream()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa2", "aaa1"

注:每一个 stream 在执行 forEach 等终端操作之后就不能再继续接 filter 等中间操作。

Sorted

Sorted 是一个中间操作,会返回排好序的 Stream。如果不传入自定义的 Comparator,那么这些元素将会按照自然顺序进行排序。

1
2
3
4
5
6
复制代码stringCollection
.stream()
.sorted()
.filter((s) -> s.startsWith("a"))
.forEach(System.out::println);
// "aaa1", "aaa2"

需要注意的是 Sorted 只会对流里面的元素进行排序,而不会去改变原来集合里元素的顺序,在执行 Sorted 操作后,stringCollection 中元素的顺序并没有改变:

1
2
复制代码System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1
Map

Map 是一个中间操作,会根据给定的函数把 Stream 中的每一个元素变成另一个对象。下面的例子展示了将每一个字符串转成大写的字符串。你同样也可以使用 Map 将每一个元素转成其他的类型。这个 Stream 的类型取决与你传入到 Map 的中的方法返回的类型。

1
2
3
4
5
6
7
复制代码stringCollection
.stream()
.map(String::toUpperCase)
.sorted((a, b) -> b.compareTo(a))
.forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"
Match

各种各样的 Match 操作可以用于判断一个给定的 Predicate 是否与 Stream 中的元素相匹配。Match 操作是一个终端操作,会返回一个布尔值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码boolean anyStartsWithA =
stringCollection
.stream()
.anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA); // true

boolean allStartsWithA =
stringCollection
.stream()
.allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA); // false

boolean noneStartsWithZ =
stringCollection
.stream()
.noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ); // true
Count

Count 是一个终端操作,会返回一个 long 值来表示 Stream 中元素的个数。

1
2
3
4
5
6
7
复制代码long startsWithB =
stringCollection
.stream()
.filter((s) -> s.startsWith("b"))
.count();

System.out.println(startsWithB); // 3
Reduce

Reduce 是一个终端操作,会根据给定的方法来操作 Stream 中所有的元素,并且返回一个Optional 类型的值。

1
2
3
4
5
6
7
复制代码Optional<String> reduced =
stringCollection
.stream()
.sorted()
.reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

注:ifPresent 方法接受一个 Consumer 类型的对象,System.out::println 是一个方法引用,而且 println 是一个接收一个参数且不返回值得函数,刚好符合 Consumer 的定义。

并行 Streams

在上文中提到过 Stream 可以是串联的也可以是并行的。 Stream 的串行操作是在单线程上进行的,并行操作是在多线程上并发进行的。

下面的例子展示了使用并行 Stream 来提高程序性能性能。

首先初始化一个有很多元素的 list,其中每个元素都是唯一的:

1
2
3
4
5
复制代码int max = 1000000;
List<String> values = new ArrayList<>(max);for (int i = 0; i < max; i++) {
UUID uuid = UUID.randomUUID();
values.add(uuid.toString());
}

接下来分别测试一下串联和并行 Stream 操作这个 list 所花的时间。

串联排序:

1
2
3
4
5
6
7
8
9
复制代码long t0 = System.nanoTime();

long count = values.stream().sorted().count();
System.out.println(count);
long t1 = System.nanoTime();
long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));

// sequential sort took: 899 ms

并行排序:

1
2
3
4
5
6
7
8
9
10
11
复制代码long t0 = System.nanoTime();

long count = values.parallelStream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));

// parallel sort took: 472 ms

如结果所示,运行这些几乎一样的代码,并行排序大约快了 50%,你仅仅需要将 stream() 改成 parallelStream()。

Map

前面已经提到 Map 不支持 Stream ,但是 Map 已经支持很多新的、有用的方法来完成通常的任务。

1
2
3
4
5
复制代码Map<Integer, String> map = new HashMap<>();
for (int i = 0; i < 10; i++) {
map.putIfAbsent(i, "val" + i);
}
map.forEach((id, val) -> System.out.println(val));

从上面的代码可以看出,putIfAbsent 可以不用做 null 的检查,forEach 接受一个 Consumer 来遍历 map 中的每一个元素。

下面的代码展示了如何使 map 的内置方法进行计算:

1
2
3
4
5
6
7
8
9
10
11
复制代码map.computeIfPresent(3, (num, val) -> val + num);
map.get(3); // val33

map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9); // false

map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23); // true

map.computeIfAbsent(3, num -> "bam");
map.get(3); // val33

下面来学习如何删除一个键所对应的值,只有在输入的值与 Map 中的值相等时,才能删除:

1
2
3
4
5
复制代码map.remove(3, "val3");
map.get(3); // val33

map.remove(3, "val33");
map.get(3); // null

下面这个方法也很有用:

1
复制代码map.getOrDefault(42, "not found");  // not found

合并 Map 中的值也相当的简单:

1
2
3
4
5
复制代码map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9); // val9

map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9); // val9concat

如果当前的键对应的值不存在,那么就会将输入的值直接放入 Map 中,否则就会调用 Merge 函数来改变现有的值。

Date API

Java8 在 java.time 包下有全新的日期和时间的 API。这些新的日期 API完全比得上 Joda-Time,但是却不完全一样。下面的包括了这些新 API 最重要的部分。

Clock

Clock 类可以用来访问当前的日期和时间。Clock 可以获取当前的时区,可以替代 System.currentTimeMillis() 来获取当前的毫秒数。当前时间线上的时刻可以使用 Instant 类来表示,Instant 也可以创建原先的 java.util.Date 对象。

1
2
3
4
复制代码Clock clock = Clock.systemDefaultZone();long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
Timezones

时区是通过 zoneId 来表示的,zoneId 可以通过静态工厂方法访问到。时区类还定义了一个偏移量,用来在当前时刻或某时间与目标时区时间之间进行转换。

1
2
3
4
5
6
7
8
9
复制代码System.out.println(ZoneId.getAvailableZoneIds());// prints all available timezone ids

ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());

// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime

LocalTime 表示一个没有时区的时间,比如 10pm 或者 17:30:15。下面的例子为之前定义的时区创建了两个本地时间。然后比较两个时间并且计算两个时间之间在小时和分钟上的差异。

1
2
3
4
5
6
7
8
9
10
复制代码LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);

System.out.println(now1.isBefore(now2)); // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);

System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239

本地时间可以通过很多工厂方法来创建实例,包括转换字符串来得到实例:

1
2
3
4
5
6
7
8
9
10
复制代码LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59

DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime); // 13:37
LocalDate

LocalDate 表示一个明确的日期,比如 2017-03-11。它是不可变的,与 LocalTime 完全一致。下面的例子展示了如何在一个日期上增加或者减少天数,月份或者年。需要注意的是每次计算后返回的都是一个新的实例。

1
2
3
4
5
6
7
复制代码LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY

从字符串转变 LocalDate 就像 LocalTime 一样简单。

1
2
3
4
5
6
7
复制代码DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas); // 2014-12-24
LocalDateTime

LocalDateTime 代表一个具体的日期时间,它结合了上面例子中的日期和时间。LocalDateTime 是不可变的,用法和 LocalDate 和 LocalTime 一样。可以使用方法获取 LocalDateTime 实例中某些属性。

1
2
3
4
5
6
7
8
9
10
复制代码LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439

想获取一个时区中其他的信息可以从 Instant 对象中转化来。Instant 实例可以很方便的转成 java.util.Date 对象。

1
2
3
4
5
6
复制代码Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014

格式化 LocalDateTime 对象与格式化 LocalDate 和 LocalTime 对象是一样的,可以使用自定义的格式而不用提前定义好格式.

1
2
3
4
5
6
7
复制代码DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern("MMM dd, yyyy - HH:mm");

LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string); // Nov 03, 2014 - 07:13

与 java.text.NumberFormat 不同,新的 DateTimeFormatter 是不可变而且是线程安全的。

更多的格式化的语法看 这里。

注解

Java8 中的注解是可复用的,下面有几个例子来演示这个特性。

首先,定义一个注释的包装器,包装了一个数组的的注解:

1
2
3
4
5
6
7
8
9
10
复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(value={ElementType.TYPE})
@interface Hints {
Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
String value();
}

Java8 允许通过 @Repeatable 在相同的类型上使用多个注解。

旧用法: 使用容器进行注解

1
2
复制代码@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

新用法: 使用可复用的注解

1
2
复制代码@Hint("hint1")@Hint("hint2")
class Person {}

使用新用法时 Java 编译器隐式的使用了 @Hints 注解。这对于通过反射来读取注解非常重要。

1
2
3
4
5
6
7
8
9
复制代码
Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint); // null

Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length); // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length); // 2

尽管没有在 Person 类上声明 @Hints 注解,但是它却可以通过 getAnnotation(Hints.class) 获取到。然而,更方便的方法则是通过 getAnnotationByType 直接获取所有使用了 @Hint 的注解。

另外,在 Java8 中使用注解可以扩展到两个新的 Target

1
2
复制代码@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

(完)

原文

关注微信公众号,聊点其他的

本文转载自: 掘金

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

1…851852853…956

开发者博客

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