欢迎访问我的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(日志输出)
  • 接下来,一起在实战中学习它们

源码下载

名称 链接 备注
项目主页 https://github.com/zq2599/blog_demos 该项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该项目源码的仓库地址,https协议
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该项目源码的仓库地址,ssh协议

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<String, Object> 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<String, String> config; /**
* location中配置的content_handler_property属性会通过此方法传给当前类
* @param map
*/
@Override
public void config(Map<String, String> map) {
this.config = map;
} @Override
public Object[] invoke(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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\""),
"<HTML><BODY><H1>401 Unauthorized.</H1></BODY></HTML>" };
} // 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\""),
"<HTML><BODY><H1>401 Unauthorized BAD USER & PASSWORD.</H1></BODY></HTML>" };
}
}
  • 编译构建部署之后,咱们来试试效果,用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<String, Object> 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<String, String> 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的主要能力已经熟悉,接下来的章节会继续深入挖掘,欢迎继续关注欣宸原创

欢迎关注博客园:程序员欣宸

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

Java扩展Nginx之五:五大handler(系列最核心)的更多相关文章

  1. nginx高性能WEB服务器系列之五--实战项目线上nginx多站点配置

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

  2. nginx高性能WEB服务器系列之九--nginx运维故障日常解决方案

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

  3. Nginx+lua+openresty精简系列

    1. CentOS系统安装openresty 你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum update 命令).运行下面的 ...

  4. 【java集合框架源码剖析系列】java源码剖析之ArrayList

    注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本. 本博客将从源码角度带领大家学习关于ArrayList的知识. 一ArrayList类的定义: public class Arr ...

  5. 【java集合框架源码剖析系列】java源码剖析之HashMap

    前言:之所以打算写java集合框架源码剖析系列博客是因为自己反思了一下阿里内推一面的失败(估计没过,因为写此博客已距阿里巴巴一面一个星期),当时面试完之后感觉自己回答的挺好的,而且据面试官最后说的这几 ...

  6. Java经典设计模式之五大创建型模式

    转载: Java经典设计模式之五大创建型模式 一.概况 总体来说设计模式分为三大类: (1)创建型模式,共五种:工厂方法模式.抽象工厂模式.单例模式.建造者模式.原型模式. (2)结构型模式,共七种: ...

  7. nginx高性能WEB服务器系列之八--nginx日志分析与切割

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

  8. nginx高性能WEB服务器系列之七--nginx反向代理

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

  9. nginx高性能WEB服务器系列之六--nginx负载均衡配置+健康检查

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

  10. nginx高性能WEB服务器系列之四配置文件详解

    nginx系列友情链接:nginx高性能WEB服务器系列之一简介及安装https://www.cnblogs.com/maxtgood/p/9597596.htmlnginx高性能WEB服务器系列之二 ...

随机推荐

  1. graphhopper-ios 编译过程详解

    一.写在前面 GraphHopper 是一个快速且高效的路径规划引擎,它默认使用OpenStreetMap和GTFS数据, 也可以导入其他数据源.它可以用作java库或独立的web服务器,去计算两个或 ...

  2. MySQL中binlog备份脚本分享

    关于MySQL的二进制日志(binlog),我们都知道二进制日志(binlog)非常重要,尤其当你需要point to point灾难恢复的时侯,所以我们要对其进行备份.关于二进制日志(binlog) ...

  3. 03-webpack五个概念

  4. 基于Canal实现MySQL 8.0 数据库数据同步

    前言 服务器说明 主机名称 操作系统 说明 192.168.11.82 Ubuntu 22.04 主库所在服务器 192.168.11.28 Oracle Linux Server 8.7 从库所在服 ...

  5. win11 计算器的进制转换

  6. 可视化大屏:mapbox+vue全攻略

    如题图,mapbox是一个支持真3D地形展示的webGIS框架,与常用的Leaflet.Cesium和Openlayers并称四大框架,本文将介绍mapbox-gl js 在 vue 中的用法. 为什 ...

  7. 目标检测(Object Detection)

    文章目录 目标检测(Object Detection) 一.基本概念 1. 什么是目标检测 2. 目标检测的核心问题 3. 目标检测算法分类 1)Tow Stage 2)One Stage 4. 目标 ...

  8. DP做题记录

    P1140 相似基因 考虑如何设计状态. 设给出的两个串为串 \(A\) 和串 \(B\),长度分别为 \(n\) 和 \(m\). 我们用 \(f[i][j]\) 来表示前 \(i\) 个 \(A\ ...

  9. 2023-02-21:请用go语言调用ffmpeg,解码mp4文件,输出视频信息和总帧数。

    2023-02-21:请用go语言调用ffmpeg,解码mp4文件,输出视频信息和总帧数. 答案2023-02-21: 使用 github.com/moonfdd/ffmpeg-go 库,这个库比go ...

  10. 2021-01-02:java中,MinorGC、MajorGC、FullGC 什么时候发生?

    福哥答案2021-01-02: MinorGC 在年轻代空间不足的时候发生.MajorGC 指的是老年代的 GC,出现 MajorGC 一般经常伴有 MinorGC.FullGC 老年代无法再分配内存 ...