从零手写实现 nginx-25-directive map 条件判断指令
前言
大家好,我是老马。很高兴遇到你。
我们为 java 开发者实现了 java 版本的 nginx
如果你想知道 servlet 如何处理的,可以参考我的另一个项目:
手写从零实现简易版 tomcat minicat
手写 nginx 系列
如果你对 nginx 原理感兴趣,可以阅读:
从零手写实现 nginx-01-为什么不能有 java 版本的 nginx?
从零手写实现 nginx-03-nginx 基于 Netty 实现
从零手写实现 nginx-04-基于 netty http 出入参优化处理
从零手写实现 nginx-05-MIME类型(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展类型)
从零手写实现 nginx-12-keep-alive 连接复用
从零手写实现 nginx-13-nginx.conf 配置文件介绍
从零手写实现 nginx-14-nginx.conf 和 hocon 格式有关系吗?
从零手写实现 nginx-15-nginx.conf 如何通过 java 解析处理?
从零手写实现 nginx-16-nginx 支持配置多个 server
从零手写实现 nginx-18-nginx 请求头+响应头操作
从零手写实现 nginx-20-nginx 占位符 placeholder
从零手写实现 nginx-21-nginx modules 模块信息概览
从零手写实现 nginx-22-nginx modules 分模块加载优化
从零手写实现 nginx-23-nginx cookie 的操作处理
前言
大家好,我是老马。
这一节我们将配置的加载,拆分为不同的模块加载处理,便于后续拓展。
if
详细介绍一下 nginx 的 map 指令
Nginx 的 map
指令是一个强大的工具,用于根据变量的值来设置另一个变量的值。
它可以用于很多场景,比如基于请求的某些特征来动态设置变量,从而影响后续的处理逻辑。
以下是关于 map
指令的详细介绍:
语法和基本用法
map
指令的基本语法如下:
map $variable_to_test $variable_to_set {
default value;
key value;
...
}
$variable_to_test
:要测试的变量。$variable_to_set
:要设置的变量。default
:如果没有找到匹配的键,则使用默认值。key value
:键值对,根据$variable_to_test
的值来设置$variable_to_set
。
示例
假设我们想根据请求的主机名设置一个变量,进而用这个变量来决定后续的行为。
可以这样使用 map
指令:
http {
map $http_host $backend_server {
default backend1.example.com;
"www.example.com" backend2.example.com;
"api.example.com" backend3.example.com;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://$backend_server;
}
}
}
在这个例子中:
- 根据
$http_host
的值(请求头中的主机名),将$backend_server
变量设置为不同的后端服务器。 - 如果主机名是
www.example.com
,则$backend_server
设置为backend2.example.com
。 - 如果主机名是
api.example.com
,则$backend_server
设置为backend3.example.com
。 - 如果主机名不匹配任何键,则使用默认值
backend1.example.com
。
复杂匹配
map
指令支持更复杂的匹配模式,包括正则表达式。
示例如下:
http {
map $request_uri $file_extension {
"~*\.jpg$" image;
"~*\.png$" image;
"~*\.css$" stylesheet;
"~*\.js$" javascript;
default other;
}
server {
listen 80;
server_name example.com;
location / {
set $content_type $file_extension;
# 在此可以根据 $content_type 变量进行不同的处理
}
}
}
在这个例子中:
- 根据请求的 URI,将
$file_extension
变量设置为不同的值。 - 如果 URI 以
.jpg
或.png
结尾,则设置为image
。 - 如果 URI 以
.css
结尾,则设置为stylesheet
。 - 如果 URI 以
.js
结尾,则设置为javascript
。 - 如果 URI 不匹配任何模式,则使用默认值
other
。
注意事项
map
指令必须放在http
块中,不能直接放在server
或location
块中。- 在
map
指令中使用的变量必须在之前已经定义或已经存在。 map
指令中键的匹配是按顺序进行的,匹配到第一个符合条件的键时就会停止匹配。
实际应用
map
指令可以用于很多实际应用场景,比如:
- 根据客户端 IP 设置访问限制或调整访问策略。
- 根据 User-Agent 头设置不同的响应头。
- 动态调整缓存策略。
- 根据请求路径或参数动态选择后端服务器。
通过 map
指令,Nginx 的配置变得更加灵活和强大,可以根据实际需要进行复杂的条件判断和变量设置。
为什么 nginx 中需要 map 指令
在 Nginx 配置中,map
指令用于根据某个变量的值来动态设置另一个变量的值。这在许多情况下都非常有用,尤其是在需要根据请求的不同条件(如 URL、IP 地址、请求头等)来执行不同的配置或行为时。以下是一些具体的使用场景和map
指令的详细解释:
使用场景
-
动态配置
- 通过
map
指令,可以根据请求的特定条件(例如,客户端 IP 地址、请求路径、请求头等)来设置不同的 Nginx 配置项。 - 例如,可以根据访问路径设置不同的后端服务器、不同的缓存策略或不同的访问控制策略。
- 通过
-
简化配置
map
指令可以简化复杂的条件判断逻辑,避免在配置文件中编写大量的if
指令。- 通过集中管理映射规则,可以使配置文件更清晰、更易于维护。
-
负载均衡
- 可以根据请求的属性(如 User-Agent 或 Cookie)将请求分配到不同的后端服务器,实现更灵活的负载均衡策略。
map
指令的语法和用法
map
指令的基本语法如下:
map $variable_to_map $result_variable {
default value; # 设置默认值
condition1 value1; # 条件1 对应的值
condition2 value2; # 条件2 对应的值
...
}
$variable_to_map
:要根据其值进行映射的变量。$result_variable
:映射结果存储到的变量。default value
:如果没有匹配的条件,使用的默认值。condition value
:条件和值的对,满足条件时将值赋给$result_variable
。
示例
假设我们需要根据不同的主机名来设置不同的后端服务器:
http {
map $host $backend {
default web1.example.com;
host1.example.com web2.example.com;
host2.example.com web3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://$backend;
}
}
}
在这个示例中:
- 根据请求的主机名(
$host
),将$backend
变量设置为不同的后端服务器。 - 默认情况下,
$backend
会被设置为web1.example.com
。 - 如果请求的主机名是
host1.example.com
,$backend
会被设置为web2.example.com
。 - 如果请求的主机名是
host2.example.com
,$backend
会被设置为web3.example.com
。
结论
map
指令在 Nginx 中是一个强大的工具,可以根据请求的条件动态设置变量,从而实现更灵活和可维护的配置。
通过合理使用map
指令,可以简化配置文件,增强 Nginx 的功能,使其能够更好地适应各种复杂的应用场景。
java 实现
配置的解析
我们以一个比较全的配置为例
http {
# 定义一个 map 指令,根据请求的主机名设置后端服务器
map $host $backend {
default web1.example.com;
host1.example.com web2.example.com;
host2.example.com web3.example.com;
}
# 定义另一个 map 指令,根据用户代理设置变量
map $http_user_agent $mobile {
default 0;
"~*iphone|android" 1;
}
# others
}
配置加载
直接放在 http 的全局配置中,解析如下:
/**
* @since 0.22.0
* @author 老马啸西风
*/
public class NginxUserMapConfigLoadFile implements INginxUserMapConfigLoad {
//conf
@Override
public NginxUserMapConfig load() {
Map<String, String> mapping = new HashMap<>();
NginxUserMapConfig config = new NginxUserMapConfig();
List<String> values = mapBlock.getValues();
if(values.size() != 2) {
throw new Nginx4jException("map 指令的 values 必须为 2,形如 map $key1 $key2");
}
config.setPlaceholderMatchKey(values.get(0));
config.setPlaceholderTargetKey(values.get(1));
Collection<NgxEntry> entryList = mapBlock.getEntries();
if(CollectionUtil.isEmpty(entryList)) {
throw new Nginx4jException("map 指令的映射关系不可为空,可以配置 default xxx");
}
for(NgxEntry entry : entryList) {
if(entry instanceof NgxParam) {
NgxParam ngxParam = (NgxParam) entry;
String name = ngxParam.getName();
String value = ngxParam.getValue();
// 对比
if("default".equals(name)) {
config.setDefaultVal(value);
} else {
mapping.put(name, value);
}
}
}
config.setMapping(mapping);
return config;
}
}
map 指令的实现
目前实现简单的,在 dispatch 前触发 map 指令。
/**
* @since 0.22.0
* @author 老马啸西风
*/
public class NginxMapDirectiveDefault implements NginxMapDirective {
private static final Log logger = LogFactory.getLog(NginxMapDirectiveDefault.class);
@Override
public void map(NginxRequestDispatchContext context) {
Map<String, Object> placeholderMap = context.getPlaceholderMap();
List<NginxUserMapConfig> mapConfigList = context.getNginxConfig().getNginxUserConfig().getMapConfigs();
if(CollectionUtil.isEmpty(mapConfigList)) {
// 忽略
logger.info("mapConfigList 为空,忽略处理 map 指令");
return;
}
for(NginxUserMapConfig mapConfig : mapConfigList) {
processMap(mapConfig, placeholderMap);
}
}
protected void processMap(NginxUserMapConfig mapConfig,
Map<String, Object> placeholderMap) {
//1. key
String matchKey = mapConfig.getPlaceholderMatchKey();
String matchValue = (String) placeholderMap.get(matchKey);
String targetKey = mapConfig.getPlaceholderTargetKey();
// 遍历
for(Map.Entry<String, String> mapEntry : mapConfig.getMapping().entrySet()) {
if(matchValue == null) {
logger.info("matchValue is null, ignore match");
break;
}
String key = mapEntry.getKey();
String value = mapEntry.getValue();
if(key.equals(matchValue)) {
// fast-return
placeholderMap.put(targetKey, value);
logger.info("命中相等 {}={}, {}={}", matchKey, matchValue, targetKey, value);
return;
} else if(matchValue.matches(key)) {
placeholderMap.put(targetKey, value);
logger.info("命中正则 {}={}, {}={}", matchKey, matchValue, targetKey, value);
return;
}
}
// 默认值
placeholderMap.put(targetKey, mapConfig.getDefaultVal());
logger.info("命中默认值 {}={}", targetKey, mapConfig.getDefaultVal());
}
}
测试验证
直接本地启用访问 http://192.168.1.13:8080/
日志:
信息: 命中默认值 $backend=web1.example.com
信息: 命中默认值 $mobile=0
小结
map 指令是 Nginx 中一个强大的工具,用于根据请求属性动态设置变量。
通过合理使用 map 指令,可以简化配置,提高性能和灵活性。
使用 Java 库 nginxparser 可以动态解析和处理 Nginx 配置文件,进一步增强配置管理的自动化和灵活性。
我们后续考虑继续学习下 rewrite try_files 等指令。
我是老马,期待与你的下次重逢。
开源地址
为了便于大家学习,已经将 nginx 开源