欢送拜访我的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的次要能力曾经相熟,接下来的章节会持续深刻开掘,欢送持续关注欣宸原创

欢送关注思否:程序员欣宸

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