百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

Java扩展Nginx之五:五大handler(系列最核心)

off999 2025-03-12 19:18 14 浏览 0 评论

欢迎访问我的GitHub

  • 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《Java扩展Nginx》系列的第五篇,如题,本篇是整个系列的最核心内容,咱们写的代码主要都集中在nginx-clojure定义的五种handler中,不同handler分别发挥着各自的作用,它们是:
  1. Initialization Handler for nginx worker(初始化)
  2. Content Ring Handler for Location(location对应的业务处理)
  3. Nginx Rewrite Handler(地址重定向)
  4. Nginx Access Handler(鉴权)
  5. 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级别的,有以下几个特性:
  1. 每个worker都是独立的进程,启动的时候都会调用一次Initialization Handler
  2. 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.namebar.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_typeaccess_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的用意:
  1. 明确划分各个handler的职责
  2. 让日志与业务功能解耦合,让Log Handler做纯粹的日志输出工作
  3. 日志模块偏向于组件化,各个location可以按照需求选择用或者不用,而且还可以设计成多个location复用
  • 另外Log Handler也有属于自己的特性:
  1. 依旧是NginxJavaRingHandler接口的实现,invoke方法被执行的时机是request被销毁前
  2. 有专用的配置属性log_handler_property
  3. 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" };
    }
    */
}
  • 上述代码中,有下面几处地方要注意:
  1. 以上代码来自官方demo,我这里做了点小的改动(主要是文件路径改为外部参数传入)
  2. 整体功能是取出请求和响应的一些参数,打印在日志文件中
  3. logUserAgent参数控制了user agent是否打印,这个比较实用,可以通过配置来做一些开关控制
  4. 这个demo不要用于生产环境,从代码可以看出,每一次请求都做了一次io操作,这是存在性能隐患的,官方的demo只是展示log handler的作用而已,看看就好
  5. 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的主要能力已经熟悉,接下来的章节会继续深入挖掘,欢迎继续关注欣宸原创

欢迎关注头条号:程序员欣宸

  • 学习路上,你不孤单,欣宸原创一路相伴...

相关推荐

Python自动化脚本应用与示例(python自动化脚本教程)

Python是编写自动化脚本的绝佳选择,因其语法简洁、库丰富且跨平台兼容性强。以下是Python自动化脚本的常见应用场景及示例,帮助你快速上手:一、常见自动化场景文件与目录操作O批量重命名文件...

如何使用Python实现一个APP(如何用python做一个程序)

要使用Python实现一个APP,你可以选择使用一些流行的移动应用开发框架,如Kivy、PyQt或Tkinter。这里以Kivy为例,它是一个跨平台的Python框架,可以用于创建漂亮的图形用户界面(...

免费定时运行Python程序并存储输出文档的服务推荐

免费定时运行Python程序并存储输出文档的服务推荐以下是几种可以免费定时运行Python程序并存储输出结果的云服务方案:1.PythonAnywhere特点:提供免费的Python托管环境支持定时...

【Python程序开发系列】如何让python脚本一直在后台保持运行

这是我的第385篇原创文章。一、引言让Python脚本在后台持续运行,有几种常见的方式,具体方式可以根据你的系统环境和需求选择。二、Linux或macOS系统2.1使用nohup命令no...

运行和执行Python程序(运行python的程序)

一、Python是一种解释型的脚本编程语言,这样的编程语言一般支持两种代码运行方式:交互式编程在命令行窗口中直接输入代码,按下回车键就可以运行代码,并立即看到输出结果;执行完一行代码,你还可以继续...

Python 初学者指南:计算程序的运行时长

在编写Python程序时,了解程序的运行时长是一项很有用的技能。这不仅能帮助你评估代码的效率,还能在优化程序性能时提供关键的数据支持。对于初学者来说,计算程序运行时长其实并不复杂,接下来就让我们看...

pyest+appium实现APP自动化测试,思路全总结在这里

每天进步一点点,关注我们哦,每天分享测试技术文章本文章出自【码同学软件测试】码同学公众号:自动化软件测试码同学抖音号:小码哥聊软件测试01appium环境搭建安装nodejshttp://nodej...

血脉觉醒后,编程小白我是如何通过Deepseek和Trae轻松开发软件的

以下就是作为一个编程小白的我,是如何一步步开发软件的保姆级教程,请点赞收藏:第一步:打开#deepseek#(首先关闭深度思考和联网搜索)输入或复制你要让它做一个什么样软件的要求和提示词(你可以先用...

我用Deepseek+Trae写的python小软件,小白也能轻松用上模型啦!

利用AI大模型deepseek,搭配TraeCN,用半个小时做了一个本地Ollama安装部署和一键卸载的小工具,哈哈哈!感觉还不错#deepseek#一直想做一个本地Ollama安装部署和一键卸载...

在安卓设备上运行Python的方法(安卓能运行python吗)

技术背景在安卓设备上运行Python可以为开发者提供更多的开发选择和灵活性,能够利用Python丰富的库和简洁的语法来开发各种应用,如游戏、脚本工具等。然而,由于安卓系统原生不支持Python,需要借...

零基础小白,DeepSeek全自动编程,超详细提示词,一键生成软件!

我前面发表了文章,详细说了编程零基础小白,如何利用DeepSeek进行编程的全过程,感兴趣的可以去看看:DeepSeek全自动编程很多人不会写提示词,不知道怎么开始对话。话不多说,请先看下图中的对话,...

小白用DeepSeek+Python编写软件(用python制作软件)

周末无事,用DeepSeek生成全部代码,写了一个mp3音乐播放器,几分钟搞定,DeepSeek确实太强大了。我的提示语是这么写的:“请用Python语言写一个音乐播放器,支持常见音乐格式,我是Pyt...

零基础使用DeepSeek开发Windows应用程序,超简单超实用!

你敢相信,我居然用DeepSeek开发了一个能用的Windows软件!整个过程就像和学霸同桌组队做作业,我负责提需求,DeepSeek负责写代码改bug,全程碰到任何问题直接丢给DeepSeek即可。...

第二篇:如何安装Python并运行你的第一个程序

欢迎回到我的Python入门教程系列!在上一篇中,我们讨论了为什么Python是一门值得学习的编程语言。今天,我们将迈出第一步:安装Python并运行你的第一个程序。无论你是Windows、macOS...

Python 运行,带你找入口,快速读懂程序

有C或Java编程开发经验的软件开发者,初次接触python程序,当你想快速读懂python项目工程时,是否觉得python程序有些太过随意,让你看有些无所适从,进而有些茫然。这是...

取消回复欢迎 发表评论: