0%


一、线程的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程1");
}
});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行线程2");
}
});
thread2.run();
}

创建以上两个线程,执行结果:

1
2
执行线程1
执行线程2

线程1与线程2都执行了 run(),但线程1是通过 start() 执行的,线程2是直接调用 run() 执行的。

二、start() 与 run() 的区别

为了更精准地找到两种调用之间的区别,需要打印线程相关的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程1; 线程名称:" + currentThread.getName());
}
});
thread1.start();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程2; 线程名称:" + currentThread.getName());
}
});
thread2.run();
}

执行结果:

1
2
执行线程1; 线程名称:Thread-0
执行线程2; 线程名称:main

从执行结果来看,start() 调用,是通过开启一个新线程来执行 run() 的;而直接调用 run(),是主线程直接执行方法内容的。

此外,我们还可以做另一种尝试:多次执行 run() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程1; 线程名称:" + currentThread.getName());
}
});
thread1.run();
thread1.run();

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Thread currentThread = Thread.currentThread();
System.out.println("执行线程2; 线程名称:" + currentThread.getName());
}
});

thread2.start();
thread2.start();
}

执行结果:

1
2
3
4
5
6
执行线程1; 线程名称:main
执行线程1; 线程名称:main
执行线程2; 线程名称:Thread-1
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.example.springbatchdemo.job.Test.main(Test.java:29)

从执行结果可知,可以多次调用 run(),而 start() 只能被执行一次,这是为什么呢?我们看下 start 方法说明:

1
2
3
4
5
6
7
8
9
10
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

调用 start(),首先需要判断当前线程的状态是否为 0 (NEW),如果不是,则抛异常。当第一次调用 start() 时,线程状态从 NEW (新建)变为 RUNNABLE(就绪),当第二次调用 start() 时,线程状态肯定不是 NEW。

三、总结

Thread 的 start() 与 run() 都可以完成任务执行,主要区别在:

  • 作用机制不同:直接调用 run(),相当于在当前线程中执行普通方法;调用 start(),是开启一个新的线程来执行 run();
  • 执行速度不同:调用 run() 会立即执行任务,调用 start() 是将线程的状态改为就绪状态,等待空闲 CPU,不会立即执行;
  • 调用次数不同:run() 可以被重复调用,而 start() 只能被调用一次。


一、 NexT 集成第三方搜索服务

根据官方文档, NexT 主题框架集成的搜索方式有四种:

  • SwiftType
  • 微搜索
  • Local Search
  • Algolia

其中,Local Search 最为简单方便,本文将进一步介绍其配置与使用。

  • 安装插件 hexo-generator-searchdb

    1
    npm install hexo-generator-searchdb --save
  • 编辑站点配置文件,新增以下配置:

    1
    2
    3
    4
    5
    search:
    path: search.xml
    field: post
    format: html
    limit: 10000
  • 编辑主题配置文件,启用本地搜索功能:

    1
    2
    3
    4
    # Local Search
    # Dependencies: https://github.com/theme-next/hexo-generator-searchdb
    local_search:
    enable: true

部署博客,访问首页,可以看到新增一个搜索控件:

使用搜索功能,查看所有跟 Spring 相关的博客:

可以看到,Local Search 不仅支持标题检索,还支持内容检索,十分好用。

食材准备

鱼,玉米油,佐料(大葱、姜、蒜头、生抽、老抽、醋、料酒、蚝油、豉油、花椒、八角、桂树皮、黄豆酱)

GO4pTK.jpg

1. 煎鱼

GO4kSH.jpg

为了不糊锅,建议事先在鱼的身上糊上一层面粉。入锅之前,需要提起鱼来,把多余的面粉抖掉,防止热油变苦。煎鱼的目的有两个:高油温可以锁住水分,使鱼肉外焦里嫩;鱼本身的腥味主要来自于表皮和腹腔,高油温可以迅速使鱼皮和腹腔角质化,起到去腥的作用。

GO4MtS.jpg

待鱼炸至两面金黄后捞出,放凉备用。

GO4t00.jpg

2. 烧鱼

GO4g76.jpg

热油锅将佐料翻炒出味,兑水至 2/3 满锅。

GO4ojA.jpg

大火烧开,约 10 分钟后转至文火慢炖。约 1 小时后,大火收汁,出锅。

GO5VCF.jpg

3. 心得

红烧鱼是我最常做的荤菜,我也试过很多做法,有的味道很好,有的就做砸了。综合实操经验来看,我觉得做鱼的关键,并不是那盘佐料,也不是热油锅炸至两面金黄,而是时间———煮鱼的时间。我试过将一条鲜鱼直接放入一锅水中,然后只加入油盐酱醋,慢煮 2 小时后,别是一番风味———鱼的原味。所以,以上很多步骤可能只是花拳绣腿,关键还是在 2/3 满锅的水和 1 小时的文火慢炖。大家都可以尝试下,感受下那 1 小时内到底发生了什么。



又是个美好的周末,照例要吃点好的。今天做了虎皮蛋和红烧鱼。老是写技术博客也没啥意思,不如放点食谱上来吧,毕竟饮食男女嘛。这里先说虎皮蛋的制法。

制作流程

食材准备

新鲜鸡蛋,玉米油,佐料(大葱、小葱、姜、蒜头、生抽、老抽、蚝油、花椒、八角)

GOoYnA.jpg

1. 煮蛋

将鸡蛋在开水中煮 20 分钟(一定要煮熟,否则做不出沙沙的蛋黄),然后捞出放入流动冷水下冲洗3分钟,方便剥壳。

GOoHBR.jpg

2. 剥蛋

尽量剥得完整,防止油炸的时候出现裂痕。沥干鸡蛋表皮的水分,防止入油锅时发生爆炸。

GOoON6.jpg

3. 炸蛋

玉米油的味道最轻,对鸡蛋原味的影响最小,推荐。油入锅后,大火升温;将木筷子插入热油中,当出现气泡后,可将鸡蛋下入油中。调至文火,慢炸 20 分钟,直至鸡蛋表面布满虎皮。捞出后放入凉水中浸泡,然后沥干水分。

GOTZ8S.jpg

GOT35V.jpg

4. 卤蛋

将佐料放入油中炸至金黄,再加入生抽老抽蚝油,兑水熬卤汁。卤水翻开后,放入虎皮蛋,调至文火慢炖 30 分钟。大火收汁,捞出放入碗中。留存的卤汁需要没过鸡蛋。

GOTar9.jpg

5. 腌蛋

GOTy8O.jpg

卤制好的蛋,不要着急吃。虎皮蛋的精髓不在外面的虎皮,而在里面的蛋黄。一颗合格的虎皮蛋,除去 Q 弹的虎皮,还有饱满多汁、沙瓤的蛋黄。要达到这种效果,必须将做好的虎皮蛋静置 4 至 6 小时。天气暖和的时候,需放入冰箱,防止变质。


一、背景介绍

Spring 容器是 Spring 框架的核心。容器负责配置对象,注入对象,管理对象,并完成整个生命周期。Spring 容器依赖控制反转(Inversion of Control),也即依赖注入(Dependency Injection)来配置并注入对象。这样的对象被成为 Bean。具体过程可抽象为:Spring 读取程序中的 Bean 配置信息,并据此在容器中生成一份 Bean 配置注册表,然后再根据注册表实例化 Bean ,装配好待用。

Spring 提供了两种容器:BeanFactoryApplicationContextBeanFactorySpring Ioc 功能提供了底层的实现基础,但为了实现框架化,Spring 新增了ApplicationContext 接口。 ApplicationContext 继承于 BeanFactory,并涵盖其所有功能。官方对两个容器做出比较:

特征 BeanFactory ApplicationContext
Bean 实例化 / 连接
集成生命周期管理
BeanPostProcessor 自动注册
BeanFactoryPostProcessor 自动注册
利用 MessageSource 进行国际化
嵌入式 ApplicationEvent 发布机制

本文分析 ApplicationContext 容器。Spring 源码参考版本 5.2.5.RELEASE

二、ApplicationContext 容器涉及的类关系图

ApplicationContext 有很多实现类,这里我们以 Java EE web (采用 Spring 框架)应用的启动过程为例。为了更直观地描述初始化过程,我们有必要认识 XmlWebApplicationContext 的关联类图:

三、ApplicationContext 容器初始化

Java EE Web 项目启动,会触发 ContextLoaderListener 监听器,完成 WebApplicationContext -根容器的初始化。具体实现方法如下:

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
/**
* web.xml 中 <context>contextLoaderListener</context> 触发根容器初始化
*
* @param servletContext 整个应用的上下文; 一个应用只有一个 ServletContext
* @return 根容器
*/
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 根容器挂载在 ServletContext 下,有且仅有一个根容器
if (servletContext
.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)
!= null) {
throw new IllegalStateException("...");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
if (this.context == null) {
// 默认生成的根容器为 XmlWebApplicationContext
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac
= (ConfigurableWebApplicationContext) this.context;
// 根容器还没有被刷新
if (!cwac.isActive()) {
// 添加父容器
if (cwac.getParent() == null) {
// 对于 web 应用, 父容器默认为空
ApplicationContext parent = loadParentContext(servletContext);
cwac.setParent(parent);
}
// 配置并刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 将根容器挂载在 ServletContext 下,指定属性 name 值,方便在应用内部随时使用
servletContext
.setAttribute(WebApplicationContext
.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 获取当前线程的上下文加载器
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
} else if (ccl != null) {
// 若上下文加载器不是 ContextLoader 加载器,则存至 currentContextPerThread 静态域
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in "
+ elapsedTime + " ms");
}
return this.context;
} catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
// 若发生异常,则将异常挂载到 ServletContext 上下文中,此后不再初始化
servletContext
.setAttribute(WebApplicationContext
.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}

/**
* 创建根容器 WebApplicationContext
*
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
* @return WebApplicationContext 根容器
*/
WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 判断根容器的实现类
Class<?> contextClass = determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("...");
}
// 实例化根容器
return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

/**
* 判断根容器的实现类
*
* @param servletContext 应用上下文; 一个应用只有一个应用上下文
* @return 根容器的实现类; 默认为 XmlWebApplicationContext
*/
Class<?> determineContextClass(ServletContext servletContext) {
// 读取web.xml中的配置 <context-param>contextClass</context-param>
String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
if (contextClassName != null) {
try {
return ClassUtils.forName(contextClassName,
ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load custom context class [" + contextClassName + "]", ex);
}
} else {
// 若未配置, 则创建默认的根容器 XmlWebApplicationContext
contextClassName =
defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName,
ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException ex) {
throw new ApplicationContextException(
"Failed to load default context class [" + contextClassName + "]", ex);
}
}
}

从代码实现可以得出:

  • 一个 Web 应用,根容器只能创建一次;

  • 根容器的实现类可配置,默认为 XmlWebApplicationContext

  • 需要为根容器指定父容器,旨在实现父容器-子容器的继承架构。目前持保留实现,默认为 null

  • 根容器 WebApplicationContext 是挂载在应用上下文 ServletContext 下的,并指定属性名 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

下面介绍根容器的配置与刷新:

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
/**
* WebApplicationContext 根容器的配置与刷新
*
* @param wac WebApplicationContext 根容器的可配置继承
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
*/
void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac,
ServletContext sc) {
// 1. 为根容器配置 ID,用于后面加载Spring-MVC的配置文件
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
} else {
// 生成默认的 ID
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 2. 为根容器配置应用上下文
wac.setServletContext(sc);
// 3. 为根容器添加配置参数地址
// 读取web.xml中的配置 <context-param>configLocation</context-param>
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 4. 初始化环境属性
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// 5. 自定义上下文
customizeContext(sc, wac);
// 6. 刷新根容器
wac.refresh();
}

/**
* 自定义上下文
*
* @param sc ServletContext 应用上下文; 一个应用只有一个应用上下文
* @param wac WebApplicationContext 根容器的可配置继承
*/
void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>>
initializerClasses =
determineContextInitializerClasses(sc);
for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>>
initializerClass : initializerClasses) {
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass,
ApplicationContextInitializer.class);
if (initializerContextClass != null
&& !initializerContextClass.isInstance(wac)) {
throw new ApplicationContextException(String.format("..."));
}
// 创建 Initializer 实例,并添加到 contextInitializers
this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
AnnotationAwareOrderComparator.sort(this.contextInitializers);
for (ApplicationContextInitializer<ConfigurableApplicationContext>
initializer : this.contextInitializers) {
// 依次执行初始化程序
initializer.initialize(wac);
}
}
}

从代码实现可以得出:

  • 通过 <context-parm>configLocation</context-param> 为根容器添加配置信息;
  • 因为只要刷新上下文,就要调用环境的 initPropertySources 方法,所以需要提前初始化环境属性,以保证根容器刷新之前的一些操作,如后置处理或初始化过程,都可以直接获取到 Servlet 属性;
  • 在根容器添加配置后,刷新前,执行自定义操作。根据 ServletContextcontextInitializerClassesglobalInitializerClasses 配置加载所有的上下文初始化程序,并依次执行;
  • 自定义根容器后,执行根容器的刷新。放在下一个专栏分解。

1. 问题描述

上篇介绍了通过Blob 类文件对象实现文件下载,但具体操作下去,会发现我留下了一个坑—-浏览器跨域请求问题CORS。如下图提示:

toVMsf.png

为实现应用解耦,前后端分离已然成为主流设计。生产环境中,前端请求需要经过代理和负载(如: NigixLVS)处理,才能传递到后端。此种模式下,前后端的交互实现都需要跨域。

同源安全策略是浏览器的一种安全限制,默认阻止跨域获取资源,防止跨站攻击。但 CORS 为跨域请求提供了可能。CORS 将跨域权限交给 Web 服务端,即通过配置就可以实现跨域地资源访问。

2. 跨域请求

跨域场景 示例
域名不同 spring.io zhihu.com
域名相同,端口不通 http://127.0.0.1:8080; http://127.0.0.1:8081
二级目录不同 document.spring.io; reference.spring.io

综上: 当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。

3. 解决方案

Access-Control-Allow-Origin 是响应头中一个重要的属性,它规定浏览器可以获取哪些域的资源。既然 CORS 将跨域访问权限交给了服务器,那么只需要服务器设置 Access-Control-Allow-Origin属性即可。以 Java 为后端语言的服务中,可以做如下设置:

1
2
// 为实现服务的可扩展,采用通配符域名,即允许访问所有域的资源
response.setHeader("Access-Control-Allow-Origin", "*");

继续实现下去,我们可能又会遇到如下问题:

tol4mR.png

查阅资料发现,对于跨域请求,请求头的可获取性配置在属性 Access-Control-Expose-Headers 中,W3C 规定客户端获取的响应头字段仅限于simple response header ,包括:

  • Cache-Control
  • Content-Language
  • Content-Type
  • Expires
  • Last-Modified
  • Pragma

而文件名在后端的设置方式如下:

1
response.setHeader("fileName", encodeFileName);

因此还后端将请求头 fileName 暴露给前端,配置如下:

1
2
response.setHeader("fileName", encodeFileName);
response.setHeader("Access-Control-Expose-Headers", "fileName");

以上。


一、背景介绍

Axios 是一个基于 PromiseHTTP 库,可以用在浏览器和 node.js 中。与传统的 Ajax 相比,Axios 的优势主要体现在: 支持 Promise API 、支持并发、支持请求与响应拦截、自动转换 JSON 数据、支持防御 XSRF、符合 MVVM 设计理念。在目前大火的前端框架 Vue 中也内置了 Axios 框架

正是因为 Axios 的数据处理为 json 格式,所以只能获取文件流,但不能给与正确处理。具体表现为:

  • response.statusrepsponse.headers 与期望相同,但 response.data 为一团乱码
  • 浏览器没有自动下载文件

二、引入 Blob 容器实现文件下载

既然 Axios 无法正确处理文件流,便需要采用其他技术来达到预期效果。Blob 对象表示一个不可变、原始数据的类文件对象;从字面意思来看,Blob 也是一个可以存储二进制文件的容器。通过这项技术,可以完美地弥补 Axios 的不足。具体实现如下:

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
// 下载文件请求
export function download (param) {
return axios({
url: '/web/bill/download',
method: 'post',
data: param,
responseType: 'blob'
})
}

// 下载文件入口方法
download(this.param).then(response => {
// 执行文件下载
this.exeDownloadFile(response)
})

// 执行文件下载
exeDownloadFile (response) {
// 创建 Blob 对象
let blob = new Blob([response.data], {type: response.headers['content-type']})
// 获取文件名
let fileName = response.headers['content-disposition'].match(/filename=(.*)/)[1]
// 创建指向 Blob 对象地址的URL
let href = window.URL.createObjectURL(blob)
// 创建用于跳转至下载链接的 a 标签
let downloadElement = document.createElement('a')
// 属性配置
downloadElement.style.display = 'none'
downloadElement.href = href
downloadElement.download = fileName
// 将 a 标签挂载至当前页面
document.body.appendChild(downloadElement)
// 触发下载事件
downloadElement.click()
// 移除已挂载的 a 标签
document.body.removeChild(downloadElement)
// 释放 Blob URL
window.URL.revokeObjectURL(href)
}

虽然以上可以实现文件下载,但美中不足的是,当后台返回异常时,通过 Blob 对象依然可以下载到一个 undefined 文件。因此,在执行下载之前,需要对下载请求是否存在异常做出判断。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 下载文件入口方法
download(this.param).then(response => {
let fr = new FileReader()
fr.readAsText(response.data)
fr.onload = function () {
// 新增-异常校验; 若 Blob 对象可 JSON 格式化,则说明文件下载异常
try {
let jsonRet = JSON.parse(this.result)
} catch (e) {
// 执行文件下载
this.exeDownloadFile(response)
}
}
})

一、背景介绍

HTTP 为请求-响应式协议,指客户端先向服务器发送请求,然后服务器接收请求后,再向客户端发送响应信息。服务器向客户端发送的响应信息,包含三个部分: 状态行、消息报头和消息正文。状态行包括 HTTP 版本和状态码。消息报头包括浏览器、服务器或消息正文的相关信息。消息正文为返回的实体数据。

在以 Java 语言为服务的响应中,消息报头是存储在 Header 属性中的。主要实现方法有两种:HttpServletResponse.setHeader(name, MIME)HttpServletResponse.addHeader(name, MIME)。下面详细介绍两种方法的底层实现原理及其区别。参考 Tomcat 源码版本8.5.31。


二、底层实现

三、setHeaderaddHeader 的区别

根据上面的分析可以看出,响应头的属性可以分为两种: 特殊属性与普通属性。其中,Content-typeContent-length 为特殊属性,MimeHeaderFields 中为普通属性。响应头的属性都在 coyote/Response 类中。

从底层实现原理看,setHeader 是通过新建或覆盖来实现属性配置的,而 addHeader 只会新增属性到队列中。此处需要说明,属性若已存在,setHeader 的进行覆盖后,还会将其他同名属性移除队列。源码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 属性组
private MimeHeaderField[] headers = new MimeHeaderField[8];

/**
* Allow "set" operations, which removes all current values
* for this header.
* @param name The header name
* @return the message bytes container for the value
*/
public MessageBytes setValue(String name) {
for(int i = 0; i < this.count; ++i) {
if (this.headers[i].getName().equalsIgnoreCase(name)) {
for(int j = i + 1; j < this.count; ++j) {
if (this.headers[j].getName().equalsIgnoreCase(name)) {
this.removeHeader(j--);
}
}
return this.headers[i].getValue();
}
}
MimeHeaderField mh = this.createHeader();
mh.getName().setString(name);
return mh.getValue();
}

继续分析,addHeader 为属性设置多个值,那么我们可以获取哪个值呢?通过属性名查询属性的方法有 getHeader(name)getHeaders(name)。实现方法如下:

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
/**
* getHeader(name)
* @param name 属性名
* @return 属性
*/
public MessageBytes getValue(String name) {
for (int i = 0; i < count; i++) {
if (headers[i].getName().equalsIgnoreCase(name)) {
return headers[i].getValue();
}
}
return null;
}

/**
* getHeaders(name)
* @param name 属性名
* @return 该属性名对应的所有属性值
*/
public Collection<String> getHeaders(String name) {
Enumeration<String> enumeration =
getCoyoteResponse().getMimeHeaders().values(name);
List<String> result = new ArrayList<>();
while (enumeration.hasMoreElements()) {
result.add(enumeration.nextElement());
}
return result;
}

从方法实现可以看出,getHeader 会返回匹配到的第一个属性值,而 getHeaders 则返回相同属性名的所有属性值。我们可以通过程序示例证实:

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
response.setHeader("set", "one");
response.setHeader("set", "two");
response.addHeader("add", "a");
response.addHeader("add", "b");
response.addHeader("add", "c");
response.addHeader("add", "d");

public static void main(String[] args) {

String setName = "set";
String addName = "add";

log.info("setHeader -> getHeader 方式查询到: {}", response.getHeader(setName));
log.info("setHeader -> getHeaders 方式查询到: {}", response.getHeaders(setName));

log.info("addHeader -> getHeader 方式查询到: {}", response.getHeader(addName));
log.info("addHeader -> getHeaders 方式查询到: {}", response.getHeaders(addName));
}

----- 输出
setHeader -> getHeader 方式查询到: two
setHeader -> getHeaders 方式查询到: two

addHeader -> getHeader 方式查询到: a
addHeader -> getHeaders 方式查询到: [a, b, c, d]