0%

Post类型过滤器

Post类型过滤器执行顺序为:Postfilter(10) -> RequestEventInfoCollectorFilter(99) -> sendResponse(1000) -> Stats(2000)

Postfilter

后置处理过滤器,主要负责对响应内容进行修饰(如添加响应头)。

// PostDecoration.groovy
boolean shouldFilter() {
    // 判断请求是否是从zuul路由到自身的
    if (true.equals(NFRequestContext.getCurrentContext().zuulToZuul))
        return false; //request was routed to a zuul server, so don't send response headers
    return true
}

Object run() {
    // 为响应添加一些header
    addStandardResponseHeaders(RequestContext.getCurrentContext().getRequest(), RequestContext.getCurrentContext().getResponse())
    return null;
}

void addStandardResponseHeaders(HttpServletRequest req, HttpServletResponse res) {
    println(originatingURL)

    String origin = req.getHeader(ORIGIN)   // 没用到
    RequestContext context = RequestContext.getCurrentContext()
    List<Pair<String, String>> headers = context.getZuulResponseHeaders()
    headers.add(new Pair(X_ZUUL, "zuul"))   // X-Zuul
    headers.add(new Pair(X_ZUUL_INSTANCE, System.getenv("EC2_INSTANCE_ID") ?: "unknown"))   // X-Zuul-instance
    headers.add(new Pair(CONNECTION, KEEP_ALIVE))   // Connection
    headers.add(new Pair(X_ZUUL_FILTER_EXECUTION_STATUS, context.getFilterExecutionSummary().toString()))   // X-Zuul-Filter-Executions
    headers.add(new Pair(X_ORIGINATING_URL, originatingURL))    // X-Originating-URL

    if (context.get("ErrorHandled") == null && context.responseStatusCode >= 400) {
        headers.add(new Pair(X_NETFLIX_ERROR_CAUSE, "Error from Origin"))   // X-Netflix-Error-Cause
        ErrorStatsManager.manager.putStats(RequestContext.getCurrentContext().route, "Error_from_Origin_Server")
    }
}

RequestEventInfoCollectorFilter

这个过滤器负责收集要发送到ESI, EventBus, Turbine等的数据,例如此次请求的数据和当前应用实例的数据。

TODO:: 虽然zuul收集了这些数据,但是并没有找到在哪里使用… mark一下

// RequestEventInfoCollector.groovy
boolean shouldFilter() {
    return true
}

Object run() {
    NFRequestContext ctx = NFRequestContext.getCurrentContext();
    final Map<String, Object> event = ctx.getEventProperties();

    try {
        // 往eventProperties中写入与此次请求有关的数据
        captureRequestData(event, ctx.request);
        // 往eventProperties中写入与当前实例有关的数据
        captureInstanceData(event);
    } catch (Exception e) {
        event.put("exception", e.toString());
        LOG.error(e.getMessage(), e);
    }
}

capture data这两个方法比较啰嗦,不过逻辑并不复杂,就是收集各种数据并写入map中

// RequestEventInfoCollector.groovy
void captureRequestData(Map<String, Object> event, HttpServletRequest req) {
    try {
        // 写入请求基本信息
        // basic request properties
        event.put("path", req.getPathInfo());
        event.put("host", req.getHeader("host"));
        event.put("query", req.getQueryString());
        event.put("method", req.getMethod());
        event.put("currentTime", System.currentTimeMillis());

        // 写入请求头
        // request headers
        for (final Enumeration names = req.getHeaderNames(); names.hasMoreElements();) {
            final String name = names.nextElement();
            final StringBuilder valBuilder = new StringBuilder();
            boolean firstValue = true;
            for (final Enumeration vals = req.getHeaders(name); vals.hasMoreElements();) {
                // only prepends separator for non-first header values
                if (firstValue) firstValue = false;
                else {
                    valBuilder.append(VALUE_SEPARATOR);
                }

                valBuilder.append(vals.nextElement());
            }

            event.put("request.header." + name, valBuilder.toString());
        }

        // 写入请求参数
        // request params
        final Map params = req.getParameterMap();
        for (final Object key : params.keySet()) {
            final String keyString = key.toString();
            final Object val = params.get(key);
            String valString;
            if (val instanceof String[]) {
                final String[] valArray = (String[]) val;
                if (valArray.length == 1)
                    valString = valArray[0];
                else
                    valString = Arrays.asList((String[]) val).toString();
            } else {
                valString = val.toString();
            }
            event.put("param." + key, valString);

            // some special params get promoted to top-level fields
            if (keyString.equals("esn")) {
                event.put("esn", valString);
            }
        }

        // 写入响应头
        // response headers
        NFRequestContext.getCurrentContext().getZuulResponseHeaders()?.each { Pair<String, String> it ->
            event.put("response.header." + it.first().toLowerCase(), it.second())
        }
    } finally {
    }
}

private static final void captureInstanceData(Map<String, Object> event) {
    try {
        final String stack = ConfigurationManager.getDeploymentContext().getDeploymentStack();
        if (stack != null) event.put("stack", stack);

        // TODO: add CLUSTER, ASG, etc.

        // 获取此实例(zuul)的信息
        final InstanceInfo instanceInfo = ApplicationInfoManager.getInstance().getInfo();
        // 写入实例信息,id和metadata等
        if (instanceInfo != null) {
            event.put("instance.id", instanceInfo.getId());
            for (final Map.Entry<String, String> e : instanceInfo.getMetadata().entrySet()) {
                event.put("instance." + e.getKey(), e.getValue());
            }
        }

        // AWS相关,跳过
        // caches value after first call.  multiple threads could get here simultaneously, but I think that is fine
        final AmazonInfo amazonInfo = AmazonInfoHolder.getInfo();

        for (final Map.Entry<String, String> e : amazonInfo.getMetadata().entrySet()) {
            event.put("amazon." + e.getKey(), e.getValue());
        }
    } finally {
    }
}

sendResponse

sendResponse是比较重要的一个过滤器,负责将响应写回请求来源。

// sendResponse.groovy
boolean shouldFilter() {
    return !RequestContext.currentContext.getZuulResponseHeaders().isEmpty() ||
            RequestContext.currentContext.getResponseDataStream() != null ||
            RequestContext.currentContext.responseBody != null
}

Object run() {
    // 添加一些响应头并收集响应debug信息到上下文
    addResponseHeaders()
    // 写响应流
    writeResponse()
}

void writeResponse() {
    RequestContext context = RequestContext.currentContext

    // there is no body to send
    if (context.getResponseBody() == null && context.getResponseDataStream() == null) return;

    HttpServletResponse servletResponse = context.getResponse()
    servletResponse.setCharacterEncoding("UTF-8")

    OutputStream outStream = servletResponse.getOutputStream();
    InputStream is = null
    try {
        // 如果上下文中设置了responseBody,则覆盖掉原来的输出
        if (RequestContext.currentContext.responseBody != null) {
            String body = RequestContext.currentContext.responseBody
            writeResponse(new ByteArrayInputStream(body.getBytes(Charset.forName("UTF-8"))), outStream)
            return;
        }

        // 判断请求是否能接收gzip压缩
        boolean isGzipRequested = false
        final String requestEncoding = context.getRequest().getHeader(ZuulHeaders.ACCEPT_ENCODING)
        if (requestEncoding != null && requestEncoding.equals("gzip"))
            isGzipRequested = true;

        is = context.getResponseDataStream();
        InputStream inputStream = is
        if (is != null) {
            if (context.sendZuulResponse()) {
                // if origin response is gzipped, and client has not requested gzip, decompress stream
                // before sending to client
                // else, stream gzip directly to client
                // 如果原始响应是gzip压缩的,但请求来源并不接受gzip,就解压后再返回,否则直接返回gzip响应
                if (context.getResponseGZipped() && !isGzipRequested)
                    try {
                        inputStream = new GZIPInputStream(is);
                    } catch (java.util.zip.ZipException e) {
                        println("gzip expected but not received assuming unencoded response" + RequestContext.currentContext.getRequest().getRequestURL().toString())
                        inputStream = is
                    }
                else if (context.getResponseGZipped() && isGzipRequested)
                    servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip")
                writeResponse(inputStream, outStream)
            }
        }
    } finally {
        try {
            is?.close();
            outStream.flush()
            outStream.close()
        } catch (IOException e) {
        }
    }
}

// 其它方法比较简单,省略了...

Stats

负责收集统计数据,以及将filter执行过程中收集到的debug信息输出到控制台。

@Override
boolean shouldFilter() {
    return true
}

@Override
Object run() {
    int status = RequestContext.getCurrentContext().getResponseStatusCode();
    StatsManager sm = StatsManager.manager
    // 收集统计数据
    sm.collectRequestStats(RequestContext.getCurrentContext().getRequest());
    sm.collectRouteStats(RequestContext.getCurrentContext().route, status);
    // 打印debug信息
    dumpRoutingDebug()
    dumpRequestDebug()
}

Route类型过滤器

Route类型的过滤器在zuul中负责对请求进行转发,zuul作为网关的最核心的功能就体现在route类型过滤器中。Netflix提供了两个route类型的过滤器:ZuulHostRequest和ZuulNFRequest,但任意请求只会满足其中一个过滤器的执行条件。

Route类型过滤器执行顺序为:ZuulNFRequest(10) -> ZuulHostRequest(100)

ZuulHostRequest

ZuulHostRequest简单地根据host来转发请求,host的值根据上下文变量routeHost取得。

// ZuulHostRequest.groovy
boolean shouldFilter() {
    // 如果routeHost不为空,则说明此次请求是转发到指定host,由此过滤器进行处理,否则由ZuulNFRequest进行处理
    return RequestContext.currentContext.getRouteHost() != null && RequestContext.currentContext.sendZuulResponse()
}

Object run() {
    HttpServletRequest request = RequestContext.currentContext.getRequest();
    // 构建请求headers,会包括原请求请求头和zuul添加的请求头
    Header[] headers = buildZuulRequestHeaders(request)
    // 获取HTTP动词,即GET/POST之类
    String verb = getVerb(request);
    InputStream requestEntity = getRequestBody(request)
    HttpClient httpclient = CLIENT.get()

    String uri = request.getRequestURI()
    if (RequestContext.currentContext.requestURI != null) {
        // 如果在之前的过滤器中为上下文添加了requestURI,则覆盖掉原uri
        uri = RequestContext.currentContext.requestURI
    }

    try {
        // 转发请求并将响应保存到上下文
        HttpResponse response = forward(httpclient, verb, uri, request, headers, requestEntity)
        setResponse(response)
    } catch (Exception e) {
        throw e;
    }
    return null
}

ZuulHostRequest的run方法会根据原请求和上下文构建一个新的HTTP请求,然后进行转发,接下来看看forward方法

private static final AtomicReference<HttpClient> CLIENT = new AtomicReference<HttpClient>(newClient());

 HttpResponse forward(HttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) {
    // 如果开启了debugRequest的话,这里会返回一个wrap过的requestEntity(debugRequestEntity)用于debug
    requestEntity = debug(httpclient, verb, uri, request, headers, requestEntity)

    org.apache.http.HttpHost httpHost

    httpHost = getHttpHost()

    org.apache.http.HttpRequest httpRequest;

    switch (verb) {
        case 'POST':
            httpRequest = new HttpPost(uri + getQueryString())
            InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
            httpRequest.setEntity(entity)
            break
        case 'PUT':
            httpRequest = new HttpPut(uri + getQueryString())
            InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength())
            httpRequest.setEntity(entity)
            break;
        default:
            httpRequest = new BasicHttpRequest(verb, uri + getQueryString())
    }

    try {
        httpRequest.setHeaders(headers)
        HttpResponse zuulResponse = executeHttpRequest(httpclient, httpHost, httpRequest)
        return zuulResponse
    } finally {
        // When HttpClient instance is no longer needed,
        // shut down the connection manager to ensure
        // immediate deallocation of all system resources
        // 这行被注释掉了,可能因为之前用的client不是静态变量,现在换成静态变量后就不需要在这里释放连接了
//            httpclient.getConnectionManager().shutdown();
    }
}

HttpResponse executeHttpRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) {
    // 封装成hystrix command执行,有关hystrix的内容这里不做讨论
    HostCommand command = new HostCommand(httpclient, httpHost, httpRequest)
    command.execute();
}

ZuulNFRequest

ZuulNFRequest可以将请求路由到注册到eureka server的服务中。它使用Ribbon客户端,可以将请求就当前可用实例进行负载均衡。

// ZuulNFRequest.groovy
boolean shouldFilter() {
    return NFRequestContext.currentContext.getRouteHost() == null && RequestContext.currentContext.sendZuulResponse()
}

Object run() {
    NFRequestContext context = NFRequestContext.currentContext
    HttpServletRequest request = context.getRequest();

    // 构建请求headers,会包括原请求请求头和zuul添加的请求头
    MultivaluedMap<String, String> headers = buildZuulRequestHeaders(request)
    // 构建请求参数
    MultivaluedMap<String, String> params = buildZuulRequestQueryParams(request)
    Verb verb = getVerb(request);
    Object requestEntity = getRequestBody(request)
    // 通过routeVIP获取到相应的ribbon客户端
    IClient restClient = ClientFactory.getNamedClient(context.getRouteVIP());

    String uri = request.getRequestURI()
    if (context.requestURI != null) {
        uri = context.requestURI
    }
    //remove double slashes
    uri = uri.replace("//", "/")

    HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity)
    setResponse(response)
    return response
}

逻辑基本上与ZuulHostRequest一致,唯一不同的是使用的HTTP客户端不一样。ZuulNFRequest使用的客户端是从ClientFactory中获取的命名客户端(named client),每个命名客户端会有一个相应的命名配置(named config),这意味者每个客户端都可以有不同的配置。来看看相关代码

tip: 这个ClientFactory其实是属于ribbon的类,如果对ribbon有了解可以选择跳过下面的内容不看。

// ClientFactory.java
public static synchronized IClient getNamedClient(String name, Class<? extends IClientConfig> configClass) {
    if (simpleClientMap.get(name) != null) {
        return simpleClientMap.get(name);
    }
    try {
        // client不存在,尝试创建一个
        return createNamedClient(name, configClass);
    } catch (ClientException e) {
        throw new RuntimeException("Unable to create client", e);
    }
}

public static synchronized IClient createNamedClient(String name, Class<? extends IClientConfig> configClass) throws ClientException {
    // 获取命名配置
    IClientConfig config = getNamedConfig(name, configClass);
    return registerClientFromProperties(name, config);
}

需要注意到的是在createNamedClient()中,传入的client config是一个类型,通过getNamedConfig(name, configClass)获取到当前客户端对应的命名配置。

命名配置

所谓命名配置即是为每份配置起一个名字,然后在运行时就可以根据不同的名字来获取不同的配置,例如我们可以这样配置

# zuul.properties
origin.zuul.client.DeploymentContextBasedVipAddresses=ORIGIN
origin.zuul.client.Port=8080
foo.zuul.client.DeploymentContextBasedVipAddresses=FOO
foo.zuul.client.Port=8081
bar.zuul.client.DeploymentContextBasedVipAddresses=BAR
bar.zuul.client.Port=8082

然后构造出来的origin, foo, bar这三个客户端对应的配置分别是vip: ORIGIN, FOO, BAR; port: 8080, 8081, 8082

获取命名配置的代码如下

// ClientFactory.java
public static IClientConfig getNamedConfig(String name, Class<? extends IClientConfig> clientConfigClass) {
    IClientConfig config = namedConfig.get(name);
    if (config != null) {
        return config;
    } else {
        try {
            config = (IClientConfig) clientConfigClass.newInstance();
            // 以名称前缀加载相应的配置
            config.loadProperties(name);
        } catch (Throwable e) {
            logger.error("Unable to create client config instance", e);
            return null;
        }
        config.loadProperties(name);
        IClientConfig old = namedConfig.putIfAbsent(name, config);
        if (old != null) {
            config = old;
        }
        return config;
    }
}

Pre类型

Pre类型的过滤器执行顺序为:DebugFilter(1) -> Routing(1) -> PreDecoration(20) -> WeightedLoadBalancer(30) -> DebugRequest(10000)

DebugFilter

判断是否为此次请求开启debug的一个过滤器。

// Debug.groovy
boolean shouldFilter() {
    // 配置中是否开启debug
    if ("true".equals(RequestContext.currentContext.getRequest().getParameter(debugParameter.get())))
        return true;
    return routingDebug.get();
}

Object run() {
    // 设置当前请求的上下文的debug标识为true,作为之后执行filter时是否记录debug信息的依据
    RequestContext.getCurrentContext().setDebugRequest(true)
    RequestContext.getCurrentContext().setDebugRouting(true)
    return null;
}

Routing

Routing过滤器判断是将请求路由到静态资源还是其它服务。

// Routing.groovy
boolean shouldFilter() {
    return true
}

Object staticRouting() {
    // 路由到静态资源
    FilterProcessor.instance.runFilters("healthcheck")
    FilterProcessor.instance.runFilters("static")
}

Object run() {
    staticRouting() //runs the static Zuul

    // TODO:: 这里routeVIP的值是固定的(origin),后面也没有找到修改该值的地方,导致zuul只能路由到某个单一的服务,暂时不知道原因,先mark一下
    // 目标Eureka VIP
    ((NFRequestContext) RequestContext.currentContext).routeVIP = defaultClient.get()
    String host = defaultHost.get()
    if (((NFRequestContext) RequestContext.currentContext).routeVIP == null) ((NFRequestContext) RequestContext.currentContext).routeVIP = ZuulApplicationInfo.applicationName
    if (host != null) {
        final URL targetUrl = new URL(host)
        RequestContext.currentContext.setRouteHost(targetUrl);
        ((NFRequestContext) RequestContext.currentContext).routeVIP = null
    }

    // host与routeVIP不能同时为null
    if (host == null && RequestContext.currentContext.routeVIP == null) {
        throw new ZuulException("default VIP or host not defined. Define: zuul.niws.defaultClient or zuul.default.host", 501, "zuul.niws.defaultClient or zuul.default.host not defined")
    }

    String uri = RequestContext.currentContext.request.getRequestURI()
    // 如果在之前的filter当中给上下文的requestURI赋值了,则覆盖原uri的值
    if (RequestContext.currentContext.requestURI != null) {
        uri = RequestContext.currentContext.requestURI
    }
    if (uri == null) uri = "/"
    if (uri.startsWith("/")) {
        uri = uri - "/"
    }

    // 截取路径的第一段为route
    // TODO:: 这个route有什么用,也暂时没发现
    ((NFRequestContext) RequestContext.currentContext).route = uri.substring(0, uri.indexOf("/") + 1)
}

PreDecoration

预处理过滤器,负责对请求做一些预处理操作,如添加请求头。

// PreDecoration.groovy
@Override
boolean shouldFilter() {
    return true
}

@Override
Object run() {
    if (RequestContext.currentContext.getRequest().getParameter("url") != null) {
        try {
            // routeHost通过请求的参数url指定
            // 如果routeHost有值,则在route阶段会由ZuulHostRequest进行处理
            RequestContext.getCurrentContext().routeHost = new URL(RequestContext.currentContext.getRequest().getParameter("url"))
            // 开启GZip
            RequestContext.currentContext.setResponseGZipped(true)
        } catch (MalformedURLException e) {
            // url格式错误,返回400
            throw new ZuulException(e, "Malformed URL", 400, "MALFORMED_URL")
        }
    }
    setOriginRequestHeaders()
    return null
}

void setOriginRequestHeaders() {
    RequestContext context = RequestContext.currentContext
    context.addZuulRequestHeader("X-Netflix.request.toplevel.uuid", UUID.randomUUID().toString())
    // 添加被代理者的ip地址到XFF
    context.addZuulRequestHeader(X_FORWARDED_FOR, context.getRequest().remoteAddr)
    // 设置Host头
    context.addZuulRequestHeader(X_NETFLIX_CLIENT_HOST, context.getRequest().getHeader(HOST))
    if (context.getRequest().getHeader(X_FORWARDED_PROTO) != null) {
        context.addZuulRequestHeader(X_NETFLIX_CLIENT_PROTO, context.getRequest().getHeader(X_FORWARDED_PROTO))
    }
}

WeightedLoadBalance

TODO:: 加权负载均衡器,似乎与金丝雀发布(灰度发布)有关,暂不深入了解。

DebugRequest

负责添加Request的debug信息的过滤器

// DebugRequest.groovy
@Override
boolean shouldFilter() {
    return Debug.debugRequest()
}

@Override
Object run() {
    // 获取原始请求
    HttpServletRequest req = RequestContext.currentContext.request as HttpServletRequest

    // 收集客户端ip信息
    Debug.addRequestDebug("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort())
    // 收集HTTP请求行信息
    Debug.addRequestDebug("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + " " + req.getProtocol())

    // 收集原请求的请求头信息
    Iterator headerIt = req.getHeaderNames().iterator()
    while (headerIt.hasNext()) {
        String name = (String) headerIt.next()
        String value = req.getHeader(name)
        Debug.addRequestDebug("REQUEST:: > " + name + ":" + value)
    }

    // 收集原请求的请求体信息
    final RequestContext ctx = RequestContext.getCurrentContext()
    if (!ctx.isChunkedRequestBody()) {
        InputStream inp = ctx.request.getInputStream()
        String body = null
        if (inp != null) {
            body = inp.getText()
            Debug.addRequestDebug("REQUEST:: > " + body)

        }
    }
    return null;
}

打印出来的调试信息类似下面这样:

REQUEST_DEBUG::REQUEST:: http 0:0:0:0:0:0:0:1:54075
REQUEST_DEBUG::REQUEST:: > GET /auth-center/foo/bar HTTP/1.1
REQUEST_DEBUG::REQUEST:: > Host:localhost:8080
REQUEST_DEBUG::REQUEST:: > Connection:keep-alive
REQUEST_DEBUG::REQUEST:: > Cache-Control:max-age=0
REQUEST_DEBUG::REQUEST:: > Upgrade-Insecure-Requests:1
REQUEST_DEBUG::REQUEST:: > User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
REQUEST_DEBUG::REQUEST:: > Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
REQUEST_DEBUG::REQUEST:: > Accept-Encoding:gzip, deflate, br
REQUEST_DEBUG::REQUEST:: > Accept-Language:zh-CN,zh;q=0.9,zh-TW;q=0.8
REQUEST_DEBUG::REQUEST:: > Cookie:Idea-9d03e8f=36659dd1-27e5-4e9b-8824-19bf2de30b9e; _ga=GA1.1.79701639.1521035841; wcsid=u5MHraIxIjs3Na0G3m39N0Hab53odbCD; hblid=5HLUbXnG2OuboFyp3m39N0HbjAa3Cd5a; _oklv=1542679881769%2Cu5MHraIxIjs3Na0G3m39N0Hab53odbCD; _okdetect=%7B%22token%22%3A%2215426798825920%22%2C%22proto%22%3A%22http%3A%22%2C%22host%22%3A%22localhost%3A4040%22%7D; olfsk=olfsk020058268955480463; _okbk=cd4%3Dtrue%2Cvi5%3D0%2Cvi4%3D1542679883339%2Cvi3%3Dactive%2Cvi2%3Dfalse%2Cvi1%3Dfalse%2Ccd8%3Dchat%2Ccd6%3D0%2Ccd5%3Daway%2Ccd3%3Dfalse%2Ccd2%3D0%2Ccd1%3D0%2C; _ok=1700-237-10-3483; _gauges_unique_month=1; _gauges_unique_year=1; _gauges_unique=1; freeform=3213
REQUEST_DEBUG::REQUEST:: > 

其它类型

Options

好像是一个没用的filter…感觉是还未完成?

// Options.groovy
boolean shouldFilter() {
    String method = RequestContext.currentContext.getRequest() getMethod();
    // 处理OPTIONS方法的HTTP请求
    if (method.equalsIgnoreCase("options")) return true;
}

@Override
String uri() {
    return "any path here"
}

@Override
String responseBody() {
    // 啥也不返回
    return "" // empty response
}

Healthcheck

Healthcheck过滤器提供了一个静态资源/healthcheck,方便检测zuul应用是否正常运行。但是功能好像过于弱鸡了… - -

// Healthcheck.groovy
@Override
String filterType() {
    return "healthcheck"
}

@Override
String uri() {
    return "/healthcheck"
}

@Override
String responseBody() {
    // 只返回了一个ok...简单粗暴...
    RequestContext.getCurrentContext().getResponse().setContentType('application/xml')
    return "<health>ok</health>"
}

ErrorResponse

// ErrorResponse.groovy
boolean shouldFilter() {
    // 根据标识判断错误是否已被处理
    return RequestContext.getCurrentContext().get("ErrorHandled") == null
}

Object run() {
    RequestContext context = RequestContext.currentContext
    Throwable ex = context.getThrowable()
    try {
        LOG.error(ex.getMessage(), ex);
        throw ex
    } catch (ZuulException e) {
        String cause = e.errorCause
        if (cause == null) cause = "UNKNOWN"
        // 添加错误原因到请求头X-Netflix-Error-Cause
        RequestContext.getCurrentContext().getResponse().addHeader("X-Netflix-Error-Cause", "Zuul Error: " + cause)
        // 对该次错误请求进行统计
        if (e.nStatusCode == 404) {
            ErrorStatsManager.manager.putStats("ROUTE_NOT_FOUND", "")
        } else {
            ErrorStatsManager.manager.putStats(RequestContext.getCurrentContext().route, "Zuul_Error_" + cause)
        }

        // 判断是否改写响应状态,则请求传入的参数决定
        if (overrideStatusCode) {
            RequestContext.getCurrentContext().setResponseStatusCode(200);
        } else {
            RequestContext.getCurrentContext().setResponseStatusCode(e.nStatusCode);
        }
        // 设置标识,表示不再返回zuul转发请求得到的响应结果(如果有)
        context.setSendZuulResponse(false)
        // 设置zuul的异常响应body
        context.setResponseBody("${getErrorMessage(e, e.nStatusCode)}")
    } catch (Throwable throwable) {
        // 处理未知异常,与处理ZuulException的逻辑大体相同
        RequestContext.getCurrentContext().getResponse().addHeader("X-Zuul-Error-Cause", "Zuul Error UNKNOWN Cause")
        ErrorStatsManager.manager.putStats(RequestContext.getCurrentContext().route, "Zuul_Error_UNKNOWN_Cause")

        if (overrideStatusCode) {
            RequestContext.getCurrentContext().setResponseStatusCode(200);
        } else {
            RequestContext.getCurrentContext().setResponseStatusCode(500);
        }
        context.setSendZuulResponse(false)
        context.setResponseBody("${getErrorMessage(throwable, 500)}")
    } finally {
        // 设置标识,表示错误已经被处理(防止存在多个error过滤器时重复处理)
        context.set("ErrorHandled") //ErrorResponse was handled
        return null;
    }
}

简介

过滤器是zuul最重要的组件,几乎它所有功能都是通过过滤器来实现的。因此,理解各个过滤器的功能是阅读源码必不可少的环节。

过滤器的分类

按功能分类

如果按照功能分类,过滤器主要有四大类:pre/route/post/error,它们之间的逻辑关系在ZuulServlet中有描述。
除此之外你也可以自定义特殊类型的过滤器,比如源码中就有一个healthcheck类型。不过自定义类型的过滤器不会被zuul自动识别,需要使用者手动触发调用。

ZuulFilter

ZuulFilter是所有过滤器的基类,其核心方法是runFilter,应用了模板方法模式,来看下代码

// ZuulFilter.java
public ZuulFilterResult runFilter() {
    ZuulFilterResult zr = new ZuulFilterResult();
    // 当前filter是否被禁用
    if (!isFilterDisabled()) {
        // 是否满足filter执行条件
        if (shouldFilter()) {
            Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
            try {
                // 具体的filter逻辑执行的地方
                Object res = run();
                // wrap一下结果
                zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
            } catch (Throwable e) {
                t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                zr.setException(e);
            } finally {
                t.stopAndLog();
            }
        } else {
            zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
        }
    }
}

上面代码中的关键方法有两个:shouldFilter()和run()。

这两个方法都是抽象的,需要由具体的过滤器去实现。一般来说,我们在阅读过滤器源码时只需要重点关注这两个方法即可。

zuul-netflix-webapp提供的过滤器

zuul-netflix-webapp模块中提供了一些有用的过滤器,在src/main/groovy/filters目录下

名称 类别 Order
DebugFilter pre 1
Routing pre 1
PreDecoration pre 20
WeightedLoadBalancer pre 30
DebugRequest pre 10000
ZuulNFRequest route 10
ZuulHostRequest route 100
Postfilter post 10
RequestEventInfoCollectorFilter post 99
sendResponse post 1000
Stats post 2000
ErrorResponse error 1
Options static 0
Healthcheck healthcheck 0

由于篇幅关系,详细的代码分析拆分为以下三个章节:

zuul-simple-webapp

zuul-simple-webapp提供的过滤器较为简单,实现的功能基本上就是zuul-netfilx-webapp功能的一个子集,因此这里就不列出来了。

工作原理

关于zuul架构,官方的How-it-Works已经讲得很清楚了,这里简单地翻译一下:

Zuul的核心就是一系列的过滤器,能够在HTTP请求和响应的过程中进行一系列的操作。Zuul提供了一个可以在运行时动态地读取、编译并运行这些过滤器的框架。
以下是过滤器的一些重要属性:

  • 类型:通常定义了过滤器会作用在路由的哪个阶段
  • 执行顺序:当同一阶段存在多个过滤器时,决定了这些过滤器的执行顺序
  • 执行条件:决定过滤器是否被执行
  • 行为:当符合条件的时候执行的操作

过滤器之间不会直接进行通信,而是通过每个请求唯一对应的RequestContext实例进行状态共享。
过滤器目前只能通过Groovy语言编写(指的是动态过滤器),尽管从理论上来说Zuul支持所有以JVM为执行环境的语言。
过滤器的源码应该写到指定的目录中,Zuul Server会周期性地检查这些目录的变化。一旦某些过滤器有更新,它们将会被Zuul读取并动态地编译到运行时环境中,在随后到来的每个请求中都将被Zuul调用。

zuul_architecture

图片来源 - netflix techblog

核心类

ZuulServlet

<!-- web.xml -->
<servlet>
    <servlet-name>ZuulServlet</servlet-name>
    <servlet-class>com.netflix.zuul.http.ZuulServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>ZuulServlet</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

Zuul是一个web应用,在web.xml中其使用的Servlet实现是ZuulServlet。因此毫无疑问的,ZuulServlet就是整个Zuul的核心。

来看看servier方法

// ZuulServlet.java

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    try {
        // 初始化当前的zuul request context,将request和response放入上下文中
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        // Marks this request as having passed through the "Zuul engine", as opposed to servlets
        // explicitly bound in web.xml, for which requests will not have the same data attached
        RequestContext context = RequestContext.getCurrentContext();
        // 为此次请求设置标识
        context.setZuulEngineRan();

        // zuul对请求的处理流程 start
        // 以下几个try块部分是zuul对一个请求的处理流程:pre -> route -> post
        // 可以看到:
        // 1. post是必然执行的(可以类比finally块),但如果在post中抛出了异常,交由error处理完后就结束,避免无限循环
        // 2. 任何阶段抛出了ZuulException,都会交由error处理
        // 3. 非ZuulException会被封装后交给error处理
        try {
            preRoute();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            route();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            postRoute();
        } catch (ZuulException e) {
            error(e);
            return;
        }
        // zuul对请求的处理流程 end

    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
    } finally {
        // 此次请求完成,移除相应的上下文对象
        RequestContext.getCurrentContext().unset();
    }
}

上面代码逻辑正好zuul官方的架构图相对应

zuul_architecture1

RequestContext

RequestContext是各filter之间进行消息传递的介质,其本质上是一个Map,这样就可以在上下文中存储任何kv pair。

TODO:: 这里有一个疑问,为什么RequestContext是继承自ConcurrentHashMap?因为理论上来说RequestContext对象是绝对线程安全的(线程隔离)。

RequestContext包含一个类变量threadLocal,

protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
    @Override
    protected RequestContext initialValue() {
        try {
            // 上下文实例类型取决于contextClass,例如在NFRequestContext中就改写了contextClass
            return contextClass.newInstance();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

在代码的任何地方都可以通过静态方法getCurrentContext获取到属于当前线程(实际上经过zuul的处理,context实例是按请求隔离的)的RequestContext实例

public static RequestContext getCurrentContext() {
    if (testContext != null) return testContext;

    RequestContext context = threadLocal.get();
    return context;
}

NFRequestContext

NFRequestContext继承自RequestContext,定义了一些与netflix其它组件有关的特定概念和数据的key,例如eureka VIP,Ribbon client返回的repsonse等。它有一个静态方法

// NFRequestContext.java
static {
    RequestContext.setContextClass(NFRequestContext.class);
}

也就是说,只要加载了类NFRequestContext,应用中所有RequestContext的实例都将是NFRequestContext类型。

ZuulRunner

ZuulServlet并不做任何实际的操作,而是将所有操作交给ZuulRunner完成。

而事实上ZuulRunner的大部分操作也是委托给FilterProcessor去完成的,除了init方法。

// ZuulRunner.java
/**
 * sets HttpServlet request and HttpResponse
 */
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
    RequestContext ctx = RequestContext.getCurrentContext();
    if (bufferRequests) {
        // wrap request并缓存其body
        // 所谓buffer是指将其内容缓存起来,使得可以安全地重复调用getReader(), getInputStream()等方法
        // 因为一般来说,流操作一次之后就不能重复操作了
        // 类com.netflix.zuul.http.HttpServletRequestWrapper注释上有详解
        ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
    } else {
        ctx.setRequest(servletRequest);
    }

    // response没得选,肯定是wrap过的
    ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}

FilterProcessor

FilterProcessor是个单例,通过getInstance()方法获取。

FilterProcessor的核心方法有两个:runFilters和processZuulFilter

// FilterProcessor.java
/**
 * 这个是真正执行filter的方法,每调用一次都会执行同一类型的所有filter
 * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
 *
 * @param sType the filterType.
 * @throws Throwable throws up an arbitrary exception
 */
public Object runFilters(String sType) throws Throwable {
    // 添加debug信息
    if (RequestContext.getCurrentContext().debugRouting()) {
        Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
    }
    boolean bResult = false;
    // 通过FilterLoader获取指定类型的所有filter
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    if (list != null) {
        // 这里没有进行try...catch... 意味着只要任何一个filter执行失败了整个过程就会中断掉
        for (int i = 0; i < list.size(); i++) {
            ZuulFilter zuulFilter = list.get(i);
            Object result = processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                // 注意这里写的是|=不是!=
                // TODO:: 为什么要用这种写法?既然只有result为boolean类型时才执行,直接赋值不行吗
                bResult |= ((Boolean) result);
            }
        }
    }
    return bResult;
}
// FilterProcessor.java
/**
 * 执行单个zuul filter
 * Processes an individual ZuulFilter. This method adds Debug information. Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code.
 *
 * @param filter
 * @return the return value for that filter
 * @throws ZuulException
 */
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

    RequestContext ctx = RequestContext.getCurrentContext();
    boolean bDebug = ctx.debugRouting();
    final String metricPrefix = "zuul.filter-";     // 这个变量没有用到...
    long execTime = 0;
    String filterName = "";
    try {
        long ltime = System.currentTimeMillis();
        filterName = filter.getClass().getSimpleName();

        RequestContext copy = null;
        Object o = null;
        Throwable t = null;

        if (bDebug) {
            Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
            // copy了一份context用于debug
            copy = ctx.copy();
        }

        ZuulFilterResult result = filter.runFilter();
        ExecutionStatus s = result.getStatus();

        // 统计执行时间
        execTime = System.currentTimeMillis() - ltime;

        // 这段对过滤器的执行状态进行记录
        switch (s) {
            case FAILED:
                t = result.getException();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                break;
            case SUCCESS:
                o = result.getResult();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                if (bDebug) {
                    Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                    Debug.compareContextState(filterName, copy);
                }
                break;
            default:
                break;
        }

        if (t != null) throw t;

        // 触发一个filter usage回调
        // 当前notifier的实现固定是BasicFilterUsageNotifier,通过Servo统计filter的调用
        usageNotifier.notify(filter, s);
        return o;

    } catch (Throwable e) {
        if (bDebug) {
            Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
        }
        usageNotifier.notify(filter, ExecutionStatus.FAILED);
        if (e instanceof ZuulException) {
            throw (ZuulException) e;
        } else {
            ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
            // 如果在line35之前抛出了异常,这个execTime的值会是0
            // 不过ZuulFilter.runFilter()中做了try...catch...处理,理论上来说不会出现异常
            ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
            throw ex;
        }
    }
}
// FilterProcessor.java
/**
 * Publishes a counter metric for each filter on each use.
 */
public static class BasicFilterUsageNotifier implements FilterUsageNotifier {
    private static final String METRIC_PREFIX = "zuul.filter-";

    @Override
    public void notify(ZuulFilter filter, ExecutionStatus status) {
        // 通过Netflix Servo对每个filter进行调用计数
        DynamicCounter.increment(METRIC_PREFIX + filter.getClass().getSimpleName(), "status", status.name(), "filtertype", filter.filterType());
    }
}

StartServer

StartServer是一个ServletContextListener,负责在web应用启动后执行一些初始化操作

// zuul-netflix-webapp
// StartServer.java
protected void initialize() throws Exception {
    // 这个操作是触发静态变量AmazonInfoHolder.INFO的初始化,并不是没有意义的
    AmazonInfoHolder.getInfo();
    // 监控、度量等初始化
    initPlugins();
    // 动态Filter等相关类的初始化
    initZuul();
    // cassandra初始化
    initCassandra();
    // NIWS: Netflix Internal Web Service
    // 主要是初始化ribbon的客户端之类的
    initNIWS();

    // 初始化完成,修改当前应用实例的状态为up
    ApplicationInfoManager.getInstance().setInstanceStatus(InstanceInfo.InstanceStatus.UP);
}

相关链接

下载源码

netflix-zuul

建议fork一份,方便阅读时随手写些注释提交。

zuul模块介绍

  • zuul-core: 确立zuul整体架构及重要类的api
  • zuul-netflix: 提供了对netflix其它组件的集成相关实现,如hystrix, ribbon等。此外还有一些工具,如统计工具。主要是为zuul-netflix-webapp提供支持
  • zuul-netflix-webapp: Netflix提供的一个可用于生产环境的应用实现,包括Filter的具体实现,监控,动态Filter管理等功能
  • zuul-simple-webapp: 一个简单的示例app,功能比较简单,没有太大的研究价值

运行zuul-simple-webapp

直接运行以下命令即可

$ cd zuul-simple-webapp
$ ../gradlew jettyRun

然后可以通过 http://localhost:8080 访问httpbin

详细可以参考zuul-simple-webapp

什么是httpbin

A simple HTTP Request & Response Service

简单来说就是一个方便测试HTTP请求和响应的各种信息,比如cookie, ip, headers和登录验证等的工具。

运行zuul-netflix-webapp

由于这是一个可用于生产环境的应用,在其中集成了eureka等,因此需要运行起来还需要做一定的配置

配置zuul.properties,位于zuul-netflix-webapp/src/resources,这里只列出一些必须要覆盖的项

# zuul.properties
eureka.serviceUrl.default={你的eureka server注册地址}

# 指定filter存放的目录,由于只是调试代码,直接使用zuul提供的filter就好
zuul.filter.pre.path=src/main/groovy/filters/pre
zuul.filter.routing.path=src/main/groovy/filters/route
zuul.filter.post.path=src/main/groovy/filters/post

# origin client对应的eureka VIP
origin.zuul.client.DeploymentContextBasedVipAddresses=foo
origin.zuul.client.Port=8080

此外,建议修改代码打开debug开关

// Debug.groovy
boolean shouldFilter() {
    return true;
}

然后运行

$ cd zuul-netflix-webapp
$ ../gradlew jettyRun

不出意外的话,此时你的zuul应用应该已经注册到eureka server中了。
访问 http://localhost:8080/healthcheck ,如果出现ok,说明应用已运行成功
访问 http://localhost:8080/path 可以路由到你的foo服务的path路径中

debug运行(IntelliJ IDEA)

上面的操作可以运行zuul的webapp,但是还没办法进入断点调试,接下来我们尝试配置用IntelliJ IDEA来进行断点调试。

添加gradle run/debug配置

在IDEA的run/debug configurations添加一个Gradle配置,参数如下

  • Gradle project: zuul-simple-webapp或zuul-netflix-webapp
  • Tasks: jettyRun
  • VM Options: -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=7777

然后运行,这时应用会进入无限等待remote连接的状态,因此需要再添加一个remote配置。

添加remote run/debug配置

在IDEA的run/debug configrations添加一个Remote配置,参数如下

  • Host: localhost
  • Port: 7777(即之前Gradle配置中VM Options的address)

然后debug运行,就可以愉快地进行调试了。

前言

最近看了芋道eureka源码解析。上面还提到了一些阅读源码的方法,觉得挺有意思的,就想自己尝试一下。正好zuul相对完整系统的源码解析文章在网上还没有,而且zuul的源码相对来说还是比较简单的,所以第一篇源码解析文章就选择了zuul。

此系列文章基于zuul 1.3.0

此系列文章与spring cloud zuul无关,是对原生netflix zuul的源码解析。对于spring cloud zuul的源码解析,网上相关的文章还是比较多的。

在Zuul中,过滤器分为容器(jetty)的过滤器(Filter)和Zuul的过滤器(ZuulFilter)。由于在我们关注的更多的是Zuul的过滤器,因此在文章中如无特殊说明,提到过滤器均指Zuul的过滤器。

文章目录

TODO LIST

个人记录的todo list,请无视

  • monitor, tracer和counter - 都是netflix servo的东西
  • filter registry
  • groovy filter manager: FilterFileManager, FilterLoader
  • filter loader
  • filter usage notifier
  • groovy的filter如何执行?
  • zuul的配置与ServletConfig
  • zuul servlet, zuul runner, filter processor
  • httpbin
  • 贯串整个请求的RequestContext
  • pre, route, post, error
  • zuul的request wrapper有何不同?
  • zuul对一次请求的处理流程
  • Debug类
  • zuul的动态配置(DynamicPropertyFactory) - 属于zuul archaius的内容
  • zuul event
  • vip, routevip, altvip都是些啥
  • RequestContext与NFRequextContext到底用哪个,是在哪里决定的?
  • SurgicalDebugFilter
  • WeightedLoadBalancer

UNIX基础知识

操作系统简介

所有的操作系统都为它们所运行的程序提供服务,包括:执行新程序、打开文件、读写文件、分配存储区等。

严格意义上说,操作系统也是一种软件,它控制计算机硬件资源,提供程序运行环境。通常将这种软件称为内核。内核的接口被称为系统调用公用函数库建立在系统调用接口之上,shell则是一种特殊的应用程序,为其它应用程序提供接口。

Linux是GNU操作系统使用的内核。

每一个进程都有一个工作目录,所有的相对路径都从工作目录开始解释。

文件描述符

文件描述符是内核用以标识一个特定进程正在访问的文件,进程读写文件时使用。

每运行一个新程序时,所有的shell都为其打开3个文件描述符:标准输入0标准输出1标准错误2。如果不做特殊处理,则所这3个描述符都将链接向终端。大多数shell都提供将这3个描述符重定向的方法,如bash

$ ls > out.txt

参考

程序

程序是一个存储在磁盘上某个目录中的可执行文件。内核使用exec函数将程序读入内存并执行。

程序的执行实例称为进程。每个进程都有一个唯一的数字标符,称为进程ID。进程可以在前台运行,将输出显示在屏幕上,也可以在后台运行。内核控制着系统如何管理运行在系统上的所有进程。

信号

信号用于通知进程发生了某种情况。进程接收到信号后会进行相应的处理。

很多情况都会产生信号。如在终端键盘上可以通过中断键(Ctrl-C)退出键(Ctrl-\)产生相应的信息,用于中断当前运行的进程。如在另一个进程中可通过kill函数向另一个进程发送一个信号中断其运行。

时间

日历时间

其值为自协调世界时(UTC)

进程时间

也称CPU时间,用以度量进程使用的中央处理器资源。有3个进程时间值:

  • 时钟时间:进程运行的时间总量
  • 用户CPU时间:执行用户指令所用的时间量
  • 系统CPU时间:为该进程执行内核程序所经历的时间

系统调用和库函数

所有的操作系统都提供多种服务的入口点,由此程序向内核请求服务。这些入口点即称为系统调用

系统调用的接口是用c语言定义的,与具体系统如何执行该系统调用的实现技术无关。

通用库函数可能会调用一个或多个内核的系统调用,也可能不会,但它们并不是内核的入口点。从实现来看,库函数与系统调用之间有着本质区别。但从用户角度看,这区别并不重要——系统调用和库函数都是以c函数的形式出现。

系统内存管理

内核通过硬盘上的存储空间来实现虚拟内存,这块区域称为交换空间。内核不断地在交换空间和实际的物理内存之间反复交换虚拟内容中的内容,使得系统以为它拥有比物理内存更多的可用内存。

UNIX标准及实现

UNIX标准

  • ISO C
  • IEEE POSIX
  • SUS(Signle UNIX Spcification)

UNIX系统的实现

  • SVR4
  • FreeBSD
  • Linux
  • Max OS X
  • Solaris

其中,虽然只有Max OS X和Solaris能够称之为是一种UNIX系统,但它们都实现了UNIX标准并提供了UNIX的编程环境。

画框

图像的背景即是画框。

数码摄影之前,最常见的画面区域是3:2的水平画框。也是最广泛使用的相机格式。现在胶片的物理宽度不再受限制, 大多数中低端的相机采用了更窄更自然的4:3格式。

根据被摄主体和摄影师所选的处理方式的不同,画框边界对影像的影响可以很强烈,也可以很微弱。

对角线在画面中与边界画框相对照,能产生强烈的对角张力,使图像更具动态。

构图时需要考虑画框中有几个主体,如果有多个主体,就要进行平衡,使得主要的主体更突出。

主体大小

如何为主体分配在照片中占据的空间比例:

  • 如果主体很不寻常、很有趣,往往其细节比较重要,应该让其占据更大的比例
  • 反之就应该后退一些使我们能看到一些周围的环境

主体位置

任何一张只有一个主体的照片里,除非采用将画框填满的构图方式,否则总需要决定怎么安排主体的位置与四周留白的比例。一旦决定在四周保留空间,主体的位置就是一个关键。

避免中央构图:缺乏想象力,过于单调乏味。绝对的中心点太过稳定,使画面缺乏动感张力。略微偏离中心会使主体更加融入环境。

如何需要采用不同寻常的构图,那么需要有一个合适的理由,否则整个布局就显得很荒谬和反常。

分割画框

任何类型的任何影像都会自动分割画框(甚至平淡背景上的一个细小物体)。

如果从两个方向分割画框,就会产生一个交叉点。这通常是一个安排视觉重点或其他注意点的好位置。

地平线

地平线对画框进行横向分割。

如果地平线是唯一有意义的元素,那它的位置就非常重要。

如果其它元素富有趣味性,也可以让其占据大部分画面,而不需要太过于在意地平线的位置。

框中框

设计基础

构图的本质是组织,使画框里的的有图像元素有序化。设计重要的是要理解原理,即与各人的欣赏口味没有关系,并可以解释为什么某些照片能给人留下深刻印象,某些影像组织方式会有某种可预计的效果。

构图类似语言:画框是语境,设计基础是语法,图像元素是词汇,元素处理是句法

最基本的两种原理:对比平衡

设计还必须接受限制:观众对摄影的了解。许多观众也许对设计手法一窍不通,但是看过大量照片后也会了解一些惯例。所以,某些构图方法可以被看作是常规的,摄影师可以根据自己的意图遵从或挑战它们。

对比

对比强调的是照片中图像元素之间的区别(影调色彩形态等),两个相互对比的元素能相互加强。

平衡

平衡是隐含在对比之中的关系,是对立元素之间的主动关系。平衡是张力的结果,是对立的影响力相互匹配以提供均衡和协调的感觉。如果失去平衡,就会带来视觉张力

视觉的基本原理是眼睛总会设法寻找某种张力的平衡力。因此,平衡是和谐,是结果,是直观感觉到美学愉悦的状况。

无论我们在谈论的是影调、色彩、点布局还是其它什么东西时,目标都是在寻找视觉的『重心』

平衡类型

  • 静态(对称)平衡:每样东西到照片中心的距离都相等,较为严肃、沉闷
  • 动态平衡:使用不相等的重量和张力达成平衡,更加生动、活泼

对称平衡的张力被安排在居中的位置。动态平衡处理的则是不相等的重量和张力,这能使影像更加生动活泼。

tip

  • 相比如何进行平衡,先探讨是否需要平衡可能更重要。

动态张力

动态张力是使用各种结构内在的能力,以保持眼睛的警觉并使之从画面中心向外围移动。这与正规构图的静态特性正好相反。

取得动态张力的技术相当直接:一组向不同方向的斜线、相反的线条、任何引导眼睛走出画面的结构手段(最好是相反方向的)。

节奏

当场景中出现一些相似的元素时,通过对它们的布局可以构成有节奏的视觉结构。

眼睛和大脑天生熟练于扩展所见的,会很乐意假设节奏的延续性。因此,感知到的重复影像会比实际看到的更长。

打破节奏的异常事物可以使影响更具动感。最好把该事物放在右边(因为眼睛通常从左往右跟踪节奏化结构,这样让眼睛有足够的时间建立节奏)。

透视与纵深

『恒常比例』现象:两条平等线从我们身边延伸,会聚在远方,但同时我们却能感觉到它们是笔直而平等的。

纵深感对照片来说总是很重要。纵深感可以进一步影响照片的写实性。

摄影必须采用各种策略来加强或减弱画面纵深感。而影像有其自身的参照系,而不是正常的感知系。

线形透视

线形透视的特点是会聚的直线,而这些直线在大多数场景里其实的平等线。

如果相机是水平的,而场景是风光,那么水平线将会聚到地平线。如果相机向上指,建筑边缘这样的垂直线将会聚在天空中的某个点。

收缩透视

收缩透视主要出现在位于不同距离的很多相同或相似的物体。

空间透视

大气灰雾降低了场景远处部分的反差,提亮影调。我们的眼睛会把这作为一个纵深的提示。

影调透视

黑色背景前的明亮物体通常给人感觉很突出,因此有很强烈的纵深感。可以通过安排主体和光线来达到强化纵深的效果。

色彩透视

暖色调具有前倾的感觉,而冷色调则退后。

清晰度

清晰度高往往暗示着近距离,可以通过对焦来使画面的清晰度不同,从而达到强调纵深的效果。

内容的强与弱

有些照片本身的内容就非常充实,过分地考虑构图可能适得其反。

图形与摄影元素

所有形状中,隐隐约约构成的形状很储蓄、不惹眼,是最有用的,能帮助将一幅影像归整成可识别的形态,并通过简单的视觉效果引导眼睛满意地发现它们。

单点

单点占据画面非常小,为了醒目,它必须与环境存在对比。

多点

一旦加入哪怕一个点,点之间的距离也会给画面带来距离感。

画面上的两个点会使视线来回地从一点移向另一点,导致两点之间产生隐含的连接线

画面上的多个点则会隐含地让人感觉占据了它们之间的空间,导致它们组成了区域。

当画面上有非常多的点需要安排时,可以尝试自然随意的构图而非人为安排。

线

大多数线条实际上是边界。

对比在定义视觉线条时扮演了重要的角色。

水平线

垂直线

斜线

曲线

视线

形状

三角形

三角形是摄影构图中最有用的形状,部分因为它们易于构造或暗示,部分因为会聚效果。隐含三角形能给影像带来秩序,需要这种安排的地方通常是那些需要条理性的地方。

三角形天生是一种强烈的形状,非常吸引眼睛。而且,通常只要有两条线就够了,第三条可以假设或者使用合适的画框边界。

画框内构成的三角形应尽量大,充满整个画面,能使得照片看起来具有稳定感

倒三角则相反,它显得不稳定,更具挑衅性。

用光线与色彩构图

明暗与基调

大多数照片的信息内容主要位于中间影调。然而,阴影高光能对照片的情绪的气氛产生很大的影响。

如果忽略色彩,影调的分布由对比和亮度决定。把这两个因素当做指南,影像的风格选择基于三个方面:场景的特征、场景的照明方式、摄影师的诠释。

构图中的色彩

  • 色相:色相是各种颜色之所以得名的特性
  • 饱和度:饱和度是色相的密度或纯度
  • 明亮度:明亮度决定了色相是黑暗的还是明亮的

强烈的色相以多层次的方式被感知,除了光学现实有关之外,也与文化和经历相关联。不同的颜色有不同的象征意义,应根据要拍摄的场景和主题来选择使用合适的颜色。

三原色

  • 红色被认为是最强烈、密度最高的颜色,具有前进的倾向,可以强化纵深感。它充满活力、生机、强烈、温暖、炎热,暗示激情、侵略和危险。
  • 黄色是所有颜色中最明亮的,甚至没有黑暗的形式。它精力充沛、锐利、坚定、好斗、欢乐,能与太阳以及其他光源相关联。
  • 蓝色比黄色收敛,倾向于安静,相对较暗,显得冷静。蓝色较透明,有很多形式,很多人难以精确判断。

三补色

  • 绿色通常是正面的,象征生长、希望和进步。绿色的负面关联则是疾病和腐烂。
  • 紫色是难以捉摸的、稀有的颜色。难以寻找、捕捉,也难以精确再现。紫色是富有、奢侈、神秘、浩瀚的象征。
  • 橙色温暖、强烈、辉煌、有力,也代表炎热和干燥。

色彩关系

色彩必须按彼此之间的关系来处理。视觉相邻的色彩不同,它们的感受也不同。

色彩组合

两种和谐关系:

  • 互补色和谐
  • 相似色和谐

不同的色彩被人们感受到的亮度值是不同的。例如,黄色最亮,紫色最暗。

色彩重点

一小块对比色具有聚焦的效果。最明显的效果是当环境是相对水色的,而纯色相占据了局部区域。这种特别形式的色彩对比无可避免地会显得更为突出,称之为地方色彩

温和的色彩

除非在人造的环境,强烈的色彩在自然世界上相对稀少。

温和色彩更细微,提供更安静甚至更优雅的愉悦。

黑白

黑和白更能全面地表现影调变化、质感再现、形式模型和定义形状。

认识redisson

来看下官方的介绍:

Redis based In-Memory Data Grid for Java. State of the Art Redis client.

可以知道,redisson是Java的一款redis客户端。

作为redis客户端,它和大名鼎鼎的jedis有什么区别呢?redisson的宗旨是促进使用者对redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

换句话说,redisson对redis的操作进行了一些更高级的抽象,使得我们能够轻松地实现一些复杂的功能,如一系列的Java分布式对象,常用的分布式服务等。而作为抽象的代价,就是丢失了对底层细节的掌控。

Getting Start

redisson官方就支持与spring-boot集成,因此根据官方文档直接依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.8.0</version>
</dependency>

再配置相关yaml,以最简单的本地单机模式启动

application.yaml

spring:
  redis:
    redisson:
      config: classpath:redisson.yaml

redisson.yaml

singleServerConfig:
  address: redis://127.0.0.1:6379

然而添加依赖后直接启动spring boot应用居然报错了,注入RedissonClient失败!

妈耶,Google了一下无果,直接看源码,居然发下redisson-spring-boot-starter这个包没有spring.factories文件,也即是说RedissonAutoConfiguration不会自动加载。。

于是补上

@SpringBootApplication
@ImportAutoConfiguration(RedissonAutoConfiguration.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

虽然不知道redisson官方出于什么原因没有提供spring.factories文件,总之再次启动,正常。

redisson锁

实践

接口调用限制

redisson执行lua脚本

redisson提供了很方便地执行lua脚本的方式

redissonClient.getScript().eval(
    RScript.Mode.READ_ONLY, //执行模式
    "return redis.call('get', KEYS[1])",    // 要执行的lua脚本
    RScript.ReturnType.INTEGER, // 返回值类型
    Lists.newArrayList("tac"),  // 传入KEYS
    true, 1L, "hello"   //传入ARGV
)

更具体可以参考官方文档脚本执行

踩过的一些坑

传入的Boolean值参数会变成字符串

假设通过redisson的eval()传入的ARGV = false,那么在lua脚本中

print(type(ARGV[1]))    --输出'string'

传入数值也会变成字符串,非int型则会被序列化存储

假设通过redisson的eval()传入的ARGV = 1L,那么在lua脚本中获取会变成

print(ARGV[1])   --输出'["java.lang.Long",1]'

若传入的是ARGV = 1,则

print(ARGV[1])   --输出'1'

直接从redis.call()获取得值是int型,而在lua中进行了数值操作后得到的值却是long型

例如,以下结果是转换为java.lang.Integer

return redis.call('get', 'tac')

而以下结果却是转换为java.lang.Long

local tac = redis.call('get', 'tac')
return tac + 1

一些小细节

  • 注意是redisson,不要写成redission
  • redisson提供的所有类都是以R开头的,如RLock