共计 11949 个字符,预计需要花费 30 分钟才能阅读完成。
欢送拜访我的 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):
名称 | 链接 | 备注 |
---|---|---|
我的项目主页 | 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 协定 |
- 这个 git 我的项目中有多个文件夹,本篇的源码在 <font color=”blue”>nginx-clojure-tutorials</font> 文件夹下的 <font color=”red”>handler-demo</font> 子工程中,如下图红框所示:
-
本篇波及到 nginx.conf 的批改,残缺的参考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/ng…
maven 工程
- 新建名为 <font color=”blue”>handler-demo</font> 的 maven 工程,明天实战的代码都在这外面
- 我这里为了对立治理代码和依赖库,整个《Java 扩大 Nginx》系列的源码都放在父工程 <font color=”blue”>nginx-clojure-tutorials</font> 上面,本篇的 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<String, Object> map) throws IOException { // 能够依据理论需要执行初始化操作,这里作为演示,只打印日志 NginxClojureRT.log.info("MyInitHandler.invoke executed"); return null; } }
- 用命令 <font color=”blue”>mvn clean package -U</font>,生成名为 <font color=”red”>handler-demo-1.0-SNAPSHOT.jar</font> 的文件,将其放入 nginx 的 <font color=”blue”>jars</font> 目录下
-
再在 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,与之前不同的是新增了配置项 <font color=”blue”>content_handler_property</font>,该配置项能够增加自定义配置,整个 location 如下所示:
location /contentdemo { # 第一个自定义属性 content_handler_property foo.name 'foo.value'; # 第二个自定义属性 content_handler_property bar.name 'bar.value'; # 逻辑解决类 content_handler_name 'com.bolingcavalry.handlerdemo.MyContentHandler'; }
- 从下面的配置可见,通过 <font color=”blue”>content_handler_property</font> 减少了两个配置项,名字别离是 <font color=”red”>foo.name</font> 和 <font color=”red”>bar.name</font>
-
再来看 MyContentHandler 类的源码,重点是实现了 <font color=”blue”>Configurable</font> 接口,而后在 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 拜访 <font color=”blue”>/contentdemo</font>,响应如下,可见合乎预期,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 拜访 <font color=”blue”>/myproxy</font>,成果如下图,从返回后果可见申请被胜利转发到 <font color=”red”>/contentdemo</font>:
- 此刻,置信聪慧的您应该想到了:既然 rewrite handler 的逻辑代码能够本人用 java 写,那意味着能够依照本人的业务需要随便定制,那岂不是本人能够在 nginx 上写一个负载平衡的性能进去了?没错,从下图可见官网也是这么说的:
– 如果您的环境中有注册核心,例如 eureka 或者 nacos,您还能够获得后盾服务列表,这样,不光是负载平衡,各种转发调度逻辑都能够在 nginx 上开发进去了 -
还有一点要留神的,下图是方才写的 MyRewriteProxyPassHandler.java 的源码,留神红框地位,是 invoke 办法的返回值,如果返回的不是 <font color=”blue”>PHASE_DONE</font>,nginx-clojure 框架就不再执行前面 poss_proxy 操作,而是把此 handler 当做一般的 content handler 来解决了:
Nginx Access Handler(鉴权)
- access handler 的定位,是用于执行鉴权相干的逻辑
- 其实看过了后面的 rewrite handler,聪慧的您应该会想到:rewrite handler 既能够重定向,也能够间接返回 code 和 body,那岂不是间接用来做鉴权?鉴权不通过就在 rewrite handler 上返回 <font color=”blue”>401(Unauthorized)</font> 或者 <font color=”blue”>403(Forbidden)</font>
- 从技术实现的角度来看,您说得没错,access handler 来自 nginx-clojure 对性能和职责的划分,官网倡议将鉴权的工作都交给 access handler 来做:
- 失常状况下,一次申请被后面几种 handler 执行的程序如下:
- 写一个 access handler 的配置和代码验证试试,为了省事儿,就在后面 rewrite handler 的根底上改变吧
-
首先是配置,如下所示,在方才的 rewrite handler 的配置中,减少了 <font color=”blue”>access_handler_type</font> 和 <font color=”blue”>access_handler_name</font>,这就意味着该 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 再次申请 <font color=”blue”>/myproxy</font>,因为 header 中没有 <font color=”blue”>authorization</font> 字段,所以返回 401 谬误:
- 而后在 header 中减少一个属性,如下图红框,名字 <font color=”blue”>authorization</font>,值 <font color=”red”>Basic xfeep:hello!</font>,再发一次申请,蓝框中显示返回码失常,并且返回内容也是重定向后的 location 生成的:
-
而后成心用谬误的明码试试,如下图,鉴权未通过,并且返回 body 精确形容了具体的错误信息:
Nginx Log Handler(日志输入)
- 最初一个 handler 是作为辅助作用的日志输入,只管在其余 handler 中,咱们能够间接调用 <font color=”blue”>NginxClojureRT.log</font> 办法将日志输入到 error.log 文件中,但还是能够猜出官网定义 Log Handler 的用意:
- 明确划分各个 handler 的职责
- 让日志与业务性能解耦合,让 Log Handler 做纯正的日志输入工作
- 日志模块偏差于组件化,各个 location 能够依照需要抉择用或者不必,而且还能够设计成多个 location 复用
- 另外 Log Handler 也有属于本人的个性:
- 仍旧是 NginxJavaRingHandler 接口的实现,<font color=”blue”>invoke</font> 办法被执行的机会是 request 被销毁前
- 有专用的配置属性 <font color=”blue”>log_handler_property</font>
- <font color=”blue”>invoke</font> 办法的返回值 <font color=”red”> 无意义,会被 nginx-clojure 疏忽 </font>
-
接下来通过实例学习 log handler,找到后面的 <font color=”blue”>content handler</font> 的 demo,给它加上日志输入试试,将配置文件批改如下,可见减少了 <font color=”blue”>log_handler_name</font> 用于指定日志输入的执行类,另外还有两个 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" }; } */ }
- 上述代码中,有上面几处中央要留神:
- 以上代码来自官网 demo,我这里做了点小的改变 (次要是文件门路改为内部参数传入)
- 整体性能是取出申请和响应的一些参数,打印在日志文件中
- logUserAgent 参数管制了 user agent 是否打印,这个比拟实用,能够通过配置来做一些开关管制
- 这个 demo<font color=”red”> 不要用于生产环境 </font>,从代码能够看出,每一次申请都做了一次 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 申请 <font color=”blue”>/contentdemo</font> 试试,如下图,首先确保响应和之前统一,证实 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 的次要能力曾经相熟,接下来的章节会持续深刻开掘,欢送持续关注欣宸原创
欢送关注思否:程序员欣宸
学习路上,你不孤独,欣宸原创一路相伴 …