Extensión de Java Nginx cinco: cinco controladores (el núcleo de la serie)

¡Acostúmbrate a escribir juntos! Este es el día 18 de mi participación en el "Nuevo plan diario de Nuggets · Desafío de actualización de abril", haga clic para ver los detalles del evento .

Bienvenido a mi GitHub

Todos los trabajos originales de Xinchen (incluido el código fuente de soporte) se clasifican y resumen aquí: github.com/zq2599/blog…

Resumen de este artículo

  • Este artículo es el quinto artículo de la serie "Java Extends Nginx". Como el título, este artículo es el contenido central de toda la serie. El código que escribimos se concentra principalmente en los cinco controladores definidos por nginx-clojure, y diferentes controladores desempeñan sus respectivos roles, son:
  1. Controlador de inicialización para el trabajador nginx (inicialización)
  2. Controlador de anillo de contenido para ubicación (procesamiento comercial correspondiente a la ubicación)
  3. Controlador de reescritura de Nginx (redireccionamiento de direcciones)
  4. Controlador de acceso Nginx (autenticación)
  5. Controlador de registro Nginx (salida de registro)
  • A continuación, apréndalos juntos en la práctica.

Descarga del código fuente

  • El código fuente completo de "Java Extension Nginx" se puede descargar desde GitHub. La dirección y la información del enlace se muestran en la siguiente tabla ( github.com/zq2599/blog…
nombre Enlace Observación
página de inicio del proyecto https://github.com/zq2599/blog… La página de inicio del proyecto en GitHub
dirección del repositorio git (https) https://github.com/zq2599/blog… La dirección del almacén del código fuente del proyecto, protocolo https
dirección del repositorio git (ssh) [email protected] :zq2599/blog_demos.git La dirección del almacén del código fuente del proyecto, protocolo ssh
  • Hay varias carpetas en este proyecto de Git. El código fuente de este artículo se encuentra en el subproyecto handler-demo en la carpeta nginx-clojure-tutorials, como se muestra en el cuadro rojo de la siguiente figura:

inserte la descripción de la imagen aquí

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. La función general es sacar algunos parámetros de la solicitud y la respuesta e imprimirlos en el archivo de registro.
  3. El parámetro logUserAgent controla si el agente de usuario se imprime o no. Esto es más práctico y se puede configurar para realizar algún control de cambio.
  4. Esta demostración no debe usarse en el entorno de producción . Se puede ver en el código que se realiza una operación io para cada solicitud, lo cual es un riesgo de rendimiento. La demostración oficial solo muestra el rol del controlador de registro. Solo eche un vistazo
  5. El código del método variablesNeedPrefetch fue comentado por mí, porque en realidad traté de averiguar si este código existe o no, no afecta la salida del registro, y no lo entendí cuando miré el código fuente. ... (nivel limitado, espero entender), así que comenté Dropped, después de todo, siempre que la salida del registro sea normal
  • Compile, construya, implemente y ejecute, primero mire logs/error.log, de la siguiente manera, se puede ver que MyLogHandler recibió correctamente el valor del elemento de configuración:
2022-02-08 08:59:22[info][69035][main]MyLogHandler, logUserAgent [true], filePath [logs/contentdemo.log]
复制代码
  • Luego intente usar postman request/contentdemo, como se muestra en la figura a continuación, primero asegúrese de que la respuesta sea la misma que antes, para demostrar que el controlador de registro no afecta el negocio principal:

在这里插入图片描述

  • Vaya al directorio de registros y encuentre que se ha agregado el archivo contentdemo.log. El contenido es el siguiente. Los parámetros de encabezado que vienen con el cartero se han obtenido e impreso con éxito en el registro:
127.0.0.1 - x [08/Feb/2022:09:45:36 +0800] "GET /contentdemo HTTP/1.1" 200 "80" x PostmanRuntime/7.29.0
复制代码
  • Hasta ahora, todos hemos experimentado los cinco controladores principales y estamos familiarizados con las capacidades principales de nginx-clojure. Continuaremos profundizando en los próximos capítulos. Bienvenido a seguir prestando atención a Xinchen Original

Bienvenido a los Nuggets: Programador Xin Chen

En el camino del aprendizaje, no estás solo, Xinchen Original te acompañará en todo el camino...

Supongo que te gusta

Origin juejin.im/post/7087713741910835207
Recomendado
Clasificación