Java扩展Nginx之五:五大handler(系列最核心)
off999 2025-03-12 19:18 26 浏览 0 评论
欢迎访问我的GitHub
- 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
- 本文是《Java扩展Nginx》系列的第五篇,如题,本篇是整个系列的最核心内容,咱们写的代码主要都集中在nginx-clojure定义的五种handler中,不同handler分别发挥着各自的作用,它们是:
- Initialization Handler for nginx worker(初始化)
- Content Ring Handler for Location(location对应的业务处理)
- Nginx Rewrite Handler(地址重定向)
- Nginx Access Handler(鉴权)
- Nginx Log Handler(日志输出)
- 接下来,一起在实战中学习它们
源码下载
- 《Java扩展Nginx》的完整源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
- 这个git项目中有多个文件夹,本篇的源码在nginx-clojure-tutorials文件夹下的handler-demo子工程中,如下图红框所示:
- 本篇涉及到nginx.conf的修改,完整的参考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/nginx-clojure-tutorials/files/nginx.conf
maven工程
- 新建名为handler-demo的maven工程,今天实战的代码都在这里面
- 我这里为了统一管理代码和依赖库,整个《Java扩展Nginx》系列的源码都放在父工程nginx-clojure-tutorials下面,本篇的handler-demo也是nginx-clojure-tutorials的一个子工程
- 接下来,编码实战每种handler
Initialization Handler for nginx worker(初始化)
- Initialization Handler,顾名思义,是用于执行初始化逻辑的handler,它在nginx配置中是http级别的,有以下几个特性:
- 每个worker都是独立的进程,启动的时候都会调用一次Initialization Handler
- Initialization Handler也是NginxJavaRingHandler接口的实现类,其invoke方法会被调用,所以初始化逻辑代码应该写在invoke方法中
- 接下来写代码试试,新增MyInitHandler.java,代码如下:
package com.bolingcavalry.handlerdemo;
import nginx.clojure.NginxClojureRT;
import nginx.clojure.java.NginxJavaRingHandler;
import java.io.IOException;
import java.util.Map;
public class MyInitHandler implements NginxJavaRingHandler {
@Override
public Object[] invoke(Map map) throws IOException {
// 可以根据实际需求执行初始化操作,这里作为演示,只打印日志
NginxClojureRT.log.info("MyInitHandler.invoke executed");
return null;
}
} - 用命令mvn clean package -U,生成名为handler-demo-1.0-SNAPSHOT.jar的文件,将其放入nginx的jars目录下
- 再在nginx.conf的http配置中增加以下两行配置:
jvm_handler_type 'java';
jvm_init_handler_name 'com.bolingcavalry.handlerdemo.MyInitHandler'; - 重启nginx,打开logs/error.log文件,发现里面新增一行日志,这就是初始化日志:
2022-02-05 23:02:37[info][73954][main]MyInitHandler.invoke executed- 如果之前部署的location还在,可以用postman发请求试试,应该可以正常响应,表示nginx的worker已经正常工作:
Content Ring Handler for Location(location对应的业务处理)
- content handler是最常用的handler,这是个location配置,定义了nginx收到某个请求后应该如何处理,前面的文章中已经用到了
- 现在咱们再写一个content handler,与之前不同的是新增了配置项content_handler_property,该配置项可以添加自定义配置,整个location如下所示:
location /contentdemo {
# 第一个自定义属性
content_handler_property foo.name 'foo.value';
# 第二个自定义属性
content_handler_property bar.name 'bar.value';
# 逻辑处理类
content_handler_name 'com.bolingcavalry.handlerdemo.MyContentHandler';
} - 从上面的配置可见,通过content_handler_property增加了两个配置项,名字分别是foo.name和bar.name
- 再来看MyContentHandler类的源码,重点是实现了Configurable接口,然后在config方法被调用的时候,入参map中保存的就是content_handler_property配置的key和value了,在invoke方法中可以直接使用:
package com.bolingcavalry.handlerdemo;
import nginx.clojure.Configurable;
import nginx.clojure.java.ArrayMap;
import nginx.clojure.java.NginxJavaRingHandler;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Map;
import static nginx.clojure.MiniConstants.CONTENT_TYPE;
import static nginx.clojure.MiniConstants.NGX_HTTP_OK;
public class MyContentHandler implements NginxJavaRingHandler, Configurable {
private Map config;
/**
* location中配置的content_handler_property属性会通过此方法传给当前类
* @param map
*/
@Override
public void config(Map map) {
this.config = map;
}
@Override
public Object[] invoke(Map map) throws IOException {
String body = "From MyContentHandler, "
+ LocalDateTime.now()
+ ", foo : "
+ config.get("foo.name")
+ ", bar : "
+ config.get("bar.name");
return new Object[] {
NGX_HTTP_OK, //http status 200
ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map
body
};
}
} - 编译、配置、重启nginx,再用postman访问/contentdemo,响应如下,可见符合预期,content_handler_property配置的值可以在invoke方法中使用:
Nginx Rewrite Handler(地址重定向)
- rewrite handler顾名思义,就是咱们常在nginx上配置的rewrite功能,在nginx-clojure中又略有不同,为了方便记忆,这里将整个rewrite分为三段处理:
- 下面就是一个完整的rewrite handler,这些内容都是写在http配置内的:
# 1. 定义变量,用于保存路径
set $myhost "";
location /myproxy {
rewrite_handler_type 'java';
# 2. java代码中为变量赋值
rewrite_handler_name 'com.bolingcavalry.handlerdemo.MyRewriteProxyPassHandler';
# 3. 用变量的值作为地址进行跳转
proxy_pass $myhost;
} - 对应的MyRewriteProxyPassHandler.java如下:
package com.bolingcavalry.handlerdemo;
import nginx.clojure.NginxClojureRT;
import nginx.clojure.java.NginxJavaRequest;
import nginx.clojure.java.NginxJavaRingHandler;
import java.util.Map;
import static nginx.clojure.java.Constants.PHASE_DONE;
public class MyRewriteProxyPassHandler implements NginxJavaRingHandler {
@Override
public Object[] invoke(Map req) {
// 根据业务情况定制计算出的path
String myhost = computeMyHost(req);
// 用setVariable方法设置myhost变量的值,这个myhost在这个location中被定义,跳转的时候就用这个值作为path
((NginxJavaRequest)req).setVariable("myhost", myhost);
// 返回PHASE_DONE之后,nginx-clojure框架就会执行proxy_pass逻辑,
// 如果返回的不是PHONE_DONE,nginx-clojure框架就把这个handler当做content handler处理
return PHASE_DONE;
}
/**
* 这里写入业务逻辑,根据实际情况确定返回的path
* @param req
* @return
*/
private String computeMyHost(Map req) {
// 确认是http还是https
String scheme = (String)req.get("scheme");
// 确认端口号
String serverPort = (String)req.get("server-port");
// /contentdemo是nginx.conf中配置的一个location,您可以根据自己的业务情况来决定返回值
String myhost = scheme + "://127.0.0.1:" + serverPort + "/contentdemo";
NginxClojureRT.log.info("pass address [" + myhost + "]");
return myhost;
}
} - 编译构建运行起来,用postman访问/myproxy,效果如下图,从返回结果可见请求被成功转发到/contentdemo:
- 此刻,相信聪明的您应该想到了:既然rewrite handler的逻辑代码可以自己用java写,那意味着可以按照自己的业务需求随意定制,那岂不是自己可以在nginx上写一个负载均衡的功能出来了?没错,从下图可见官方也是这么说的:
- 如果您的环境中有注册中心,例如eureka或者nacos,您还可以取得后台服务列表,这样,不光是负载均衡,各种转发调度逻辑都可以在nginx上开发出来了
- 还有一点要注意的,下图是刚才写的MyRewriteProxyPassHandler.java的源码,注意红框位置,是invoke方法的返回值,如果返回的不是PHASE_DONE,nginx-clojure框架就不再执行后面poss_proxy操作,而是把此handler当做普通的content handler来处理了:
Nginx Access Handler(鉴权)
- access handler的定位,是用于执行鉴权相关的逻辑
- 其实看过了前面的rewrite handler,聪明的您应该会想到:rewrite handler既可以重定向,也可以直接返回code和body,那岂不是直接用来做鉴权?鉴权不通过就在rewrite handler上返回401 (Unauthorized)或者403 (Forbidden)
- 从技术实现的角度来看,您说得没错,access handler来自nginx-clojure对功能和职责的划分,官方建议将鉴权的工作都交给access handler来做:
- 正常情况下,一次请求被前面几种handler执行的顺序如下:
- 写一个access handler的配置和代码验证试试,为了省事儿,就在前面rewrite handler的基础上改动吧
- 首先是配置,如下所示,在刚才的rewrite handler的配置中,增加了access_handler_type和access_handler_name,这就意味着该location的请求,先由MyRewriteProxyPassHandler处理,再交给BasicAuthHandler处理,如果鉴权通过,才会交给proxy_pass处理:
# 1. 定义变量,用于保存路径
set $myhost "";
location /myproxy {
# 指定access handler的类型是java
access_handler_type 'java';
# 指定access handler的执行类类
access_handler_name 'com.bolingcavalry.handlerdemo.BasicAuthHandler';
rewrite_handler_type 'java';
# 2. java代码中为变量赋值
rewrite_handler_name 'com.bolingcavalry.handlerdemo.MyRewriteProxyPassHandler';
# 3. 用变量的值作为地址进行跳转
proxy_pass $myhost;
}- BasicAuthHandler.java的内容如下,已添加详细注释,就不多赘述了:
package com.bolingcavalry.handlerdemo;
import nginx.clojure.java.ArrayMap;
import nginx.clojure.java.NginxJavaRingHandler;
import javax.xml.bind.DatatypeConverter;
import java.util.Map;
import static nginx.clojure.MiniConstants.DEFAULT_ENCODING;
import static nginx.clojure.MiniConstants.HEADERS;
import static nginx.clojure.java.Constants.PHASE_DONE;
public class BasicAuthHandler implements NginxJavaRingHandler {
@Override
public Object[] invoke(Map request) {
// 从header中获取authorization字段
String auth = (String) ((Map)request.get(HEADERS)).get("authorization");
// 如果header中没有authorization,就返回401错误,并带上body
if (auth == null) {
return new Object[] { 401, ArrayMap.create("www-authenticate", "Basic realm=\"Secure Area\""),
"401 Unauthorized.
" };
}
// authorization应该是 : Basic xfeep:hello!,所以这里先将"Basic "去掉,然后再用":"分割
String[] up = auth.substring("Basic ".length()).split(":");
// 只是为了演示,所以账号和密码的检查逻辑在代码中是写死的,
// 如果账号等于"xfeep",并且密码等于"hello!",就返回PHASE_DONE,这样nginx-clojure就会继续执行后面的content handler
if (up[0].equals("xfeep") && up[1].equals("hello!")) {
return PHASE_DONE;
}
// 如果账号密码校验不过,就返回401,body内容是提示账号密码不过
return new Object[] { 401, ArrayMap.create("www-authenticate", "Basic realm=\"Secure Area\""),
"401 Unauthorized BAD USER & PASSWORD.
" };
}
} - 编译构建部署之后,咱们来试试效果,用postman再次请求/myproxy,因为header中没有authorization字段,所以返回401错误:
- 然后在header中增加一个属性,如下图红框,名字authorization,值Basic xfeep:hello!,再发一次请求,蓝框中显示返回码正常,并且返回内容也是重定向后的location生成的:
- 然后故意用错误的密码试试,如下图,鉴权未通过,并且返回body准确描述了具体的错误信息:
Nginx Log Handler(日志输出)
- 最后一个handler是作为辅助作用的日志输出,尽管在其他handler中,我们可以直接调用NginxClojureRT.log方法将日志输出到error.log文件中,但还是可以猜出官方定义Log Handler的用意:
- 明确划分各个handler的职责
- 让日志与业务功能解耦合,让Log Handler做纯粹的日志输出工作
- 日志模块偏向于组件化,各个location可以按照需求选择用或者不用,而且还可以设计成多个location复用
- 另外Log Handler也有属于自己的特性:
- 依旧是NginxJavaRingHandler接口的实现,invoke方法被执行的时机是request被销毁前
- 有专用的配置属性log_handler_property
- invoke方法的返回值无意义,会被nginx-clojure忽略
- 接下来通过实例学习log handler,找到前面的content handler的demo,给它加上日志输出试试,将配置文件修改如下,可见增加了log_handler_name用于指定日志输出的执行类,另外还有两个log_handler_property配置项作为自定义属性传入:
location /contentdemo {
# 第一个自定义属性
content_handler_property foo.name 'foo.value';
# 第二个自定义属性
content_handler_property bar.name 'bar.value';
content_handler_name 'com.bolingcavalry.handlerdemo.MyContentHandler';
# log handler类型是java
log_handler_type java;
# log handler的执行类
log_handler_name 'com.bolingcavalry.handlerdemo.MyLogHandler';
# 自定义属性,在MyLogHandler中作为是否打印User Agent的开关
log_handler_property log.user.agent on;
# 自定义属性,在MyLogHandler中作为日志目录
log_handler_property log.file.path logs/contentdemo.log;
}- 对应的MyLogHandler.java,有几处要注意的地方稍后会提到:
package com.bolingcavalry.handlerdemo;
import nginx.clojure.Configurable;
import nginx.clojure.NginxClojureRT;
import nginx.clojure.java.NginxJavaRequest;
import nginx.clojure.java.NginxJavaRingHandler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
public class MyLogHandler implements NginxJavaRingHandler, Configurable {
/**
* 是否将User Agent打印在日志中
*/
private boolean logUserAgent;
/**
* 日志文件路径
*/
private String filePath;
@Override
public Object[] invoke(Map request) throws IOException {
File file = new File(filePath);
NginxJavaRequest r = (NginxJavaRequest) request;
try (FileOutputStream out = new FileOutputStream(file, true)) {
String msg = String.format("%s - %s [%s] \"%s\" %s \"%s\" %s %s\n",
r.getVariable("remote_addr"),
r.getVariable("remote_user", "x"),
r.getVariable("time_local"),
r.getVariable("request"),
r.getVariable("status"),
r.getVariable("body_bytes_sent"),
r.getVariable("http_referer", "x"),
logUserAgent ? r.getVariable("http_user_agent") : "-");
out.write(msg.getBytes("utf8"));
}
return null;
}
@Override
public void config(Map properties) {
logUserAgent = "on".equalsIgnoreCase(properties.get("log.user.agent"));
filePath = properties.get("log.file.path");
NginxClojureRT.log.info("MyLogHandler, logUserAgent [" + logUserAgent + "], filePath [" + filePath + "]");
}
// 下面这段代码来自官方demo,实测发现这段代码在打印日志的逻辑中并未发挥作用,
// 不论是否删除,日志输出的内容都是相同的
/*
@Override
public String[] variablesNeedPrefetch() {
return new String[] { "remote_addr", "remote_user", "time_local", "request", "status", "body_bytes_sent",
"http_referer", "http_user_agent" };
}
*/
} - 上述代码中,有下面几处地方要注意:
- 以上代码来自官方demo,我这里做了点小的改动(主要是文件路径改为外部参数传入)
- 整体功能是取出请求和响应的一些参数,打印在日志文件中
- logUserAgent参数控制了user agent是否打印,这个比较实用,可以通过配置来做一些开关控制
- 这个demo不要用于生产环境,从代码可以看出,每一次请求都做了一次io操作,这是存在性能隐患的,官方的demo只是展示log handler的作用而已,看看就好
- variablesNeedPrefetch方法的代码被我注释掉了,因为实际尝试发现不论这段代码是否存在,都不回影响日志的输出,去看源码也没弄明白…(水平有限,望理解),于是就注释掉了,毕竟只要日志输出正常就行
- 编译构建部署运行,先看logs/error.log,如下,可见MyLogHandler成功的接收到了配置项的值:
2022-02-08 08:59:22[info][69035][main]MyLogHandler, logUserAgent [true], filePath [logs/contentdemo.log]- 再用postman请求/contentdemo试试,如下图,首先确保响应和之前一致,证明log handler不影响主业务:
- 去logs目录下查看,发现新增了contentdemo.log文件,内容如下,postman自带的header参数已经被成功获取并打印在日志中了:
127.0.0.1 - x [08/Feb/2022:09:45:36 +0800] "GET /contentdemo HTTP/1.1" 200 "80" x PostmanRuntime/7.29.0- 至此,五大handler咱们已经全部实战体验过了,对nginx-clojure的主要能力已经熟悉,接下来的章节会继续深入挖掘,欢迎继续关注欣宸原创
欢迎关注头条号:程序员欣宸
- 学习路上,你不孤单,欣宸原创一路相伴...
相关推荐
- 一键清理垃圾下载(一键清除垃圾软件下载)
-
手机弹出广告是因为手机上的软件自动推送广告,可以在手机设置里关闭应用的消息通知,方法如下:1、找到手机设置,点击进入2、找到应用和通知,点击进入3、点击通知管理,点击进入4、我们可以看到自己开启消息通...
- ghost下载中文版官网(ghost8.0下载)
-
如果你下载的ghostwin7文件如果是用于系统安装。是不是映像文件要符合以下要求:1,压缩包完好无损。2,减压后的映像文件后缀名为ISO或者GHO.3,文件要在硬盘根目录才便于识别。4,映像文件后缀...
- win10最新版本是多少2025(win10最新版本是20h2吗)
-
1、打开软件,选择需要安装的win10系统。(4g以上内存选择64位系统)2、接着我们耐心等待下载重装资源。3、资源下载完成后,等待环境部署完毕重启即可。4、进入到pe系统后,打开小白工具,选择安装的...
- u盘显示被写保护怎么处理
-
U盘被写保护可以通过以下方法解除:格式化U盘:检查U盘上是否有写保护按钮,如果有,将其拨下,然后对U盘进行格式化即可。分区格式为exFat异常:这时需要Win+R打开窗口,输入cmd并点击确定,然后在...
- 电脑硬盘坏了恢复数据成功率高吗
-
1.不能全部恢复。因为电脑硬盘数据丢失可能是硬件故障、病毒攻击、人为误操作等原因造成,而不同的原因造成的数据丢失程度不同,可恢复的数据也有所不同。2.但也有可能可以全部恢复。如果是硬件故障引起的数...
- 移动硬盘怎么分区合并(移动硬盘分区合并最简单三个步骤)
-
1、按下组合键“win+R”打开运行窗口。2、在其中的输入框中输入“diskmgmt.msc”,再点击“确定”。3、在弹出的窗口中就可以看到要合并的磁盘了。4、在磁盘上单击鼠标右键。在弹出的选项框中点...
- 电脑bios有什么用(电脑bios能干什么)
-
电脑BIOS(基本输入输出系统)是计算机启动时运行的固件,它负责初始化计算机硬件,并提供操作系统加载所需的基本功能。BIOS主要功能包括:检测和配置硬件设备、加载操作系统、管理电源和温度、提供系统启动...
- 华硕电脑蓝屏怎么修复(华硕蓝屏怎么办)
-
华硕电脑蓝屏恢复的方法如下:安全模式进入系统。重启电脑后,连续按下F8键直至出现启动选项界面,选择安全模式进入系统,若此时能够正常运行,说明问题可能是由于软件冲突引起的。检查驱动程序兼容性。过于陈旧或...
- win10教育版怎么改成专业版(win10最稳定三个版本)
-
一、首先,点击Windows10“开始”菜单,找到电脑应用列表,二、然后,在应用列表中找到“Windows系统”文件夹中找到“命令提示符”,点击打开。三、然后,系统跳转到“命令提示符”窗口。四、然后,...
- ps下载官网(ps官网免费下载)
-
要从Adobe官网下载AdobePhotoshop(PS),可以按照以下步骤进行:1.打开网页浏览器,进入Adobe官网的主页。网址是:https://www.adobe.com。2.在网页的顶...
- 连wifi就能打电话的软件(无卡用wifi打电话)
-
我的手机是安卓2.2系统所有这里只针对Android2.2系统其他系统版本的朋友可以试一试需要一款拨号软件:PPPOE拨号软件;而拨号软件必须要获得root权限(管理权限)才能拨号;下载安装这...
- wifi优化大师下载(wifi优化软件)
-
1.设定-应用程序管理器-已下载-单击需要卸载的软件-卸载。2.点击最近应用程序键-进入任务管理器-已下载-点击""""卸载""""。3...
- 无网络单机游戏(好玩的无网络单机游戏)
-
一款能够让我们自由畅快的进行游戏的合集软件。在这个合集之中有着各种各样的单机小游戏。这些小游戏不需要联网就可以玩了,没有防沉迷系统,我们想玩多久就玩多久,合集之中小游戏的类型有很多种,有赛车类、射击类...
欢迎 你 发表评论:
- 一周热门
-
-
抖音上好看的小姐姐,Python给你都下载了
-
全网最简单易懂!495页Python漫画教程,高清PDF版免费下载
-
Python 3.14 的 UUIDv6/v7/v8 上新,别再用 uuid4 () 啦!
-
python入门到脱坑 输入与输出—str()函数
-
飞牛NAS部署TVGate Docker项目,实现内网一键转发、代理、jx
-
宝塔面板如何添加免费waf防火墙?(宝塔面板开启https)
-
Python三目运算基础与进阶_python三目运算符判断三个变量
-
(新版)Python 分布式爬虫与 JS 逆向进阶实战吾爱分享
-
慕ke 前端工程师2024「完整」
-
失业程序员复习python笔记——条件与循环
-
- 最近发表
- 标签列表
-
- python计时 (73)
- python安装路径 (56)
- python类型转换 (93)
- python进度条 (67)
- python吧 (67)
- python的for循环 (65)
- python格式化字符串 (61)
- python静态方法 (57)
- python列表切片 (59)
- python面向对象编程 (60)
- python 代码加密 (65)
- python串口编程 (77)
- python封装 (57)
- python写入txt (66)
- python读取文件夹下所有文件 (59)
- python操作mysql数据库 (66)
- python获取列表的长度 (64)
- python接口 (63)
- python调用函数 (57)
- python多态 (60)
- python匿名函数 (59)
- python打印九九乘法表 (65)
- python赋值 (62)
- python异常 (69)
- python元祖 (57)
