1. 程式人生 > >Java分散式跟蹤系統Zipkin(五):Brave原始碼分析-Brave和SpringMVC整合

Java分散式跟蹤系統Zipkin(五):Brave原始碼分析-Brave和SpringMVC整合

上一篇博文中,我們分析了Brave是如何在普通Web專案中使用的,這一篇博文我們繼續分析Brave和SpringMVC專案的整合方法及原理。
我們分兩個部分來介紹和SpringMVC的整合,及XML配置方式和Annotation註解方式

pom.xml新增相關依賴spring-web和spring-webmvc

    <dependency>
      <groupId>io.zipkin.brave</groupId>
      <artifactId>brave-instrumentation-spring-web</artifactId
>
<version>${brave.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>${spring.version}</version> </dependency> <dependency
>
<groupId>io.zipkin.brave</groupId> <artifactId>brave-instrumentation-spring-webmvc</artifactId> <version>${brave.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId
>
spring-webmvc</artifactId> <version>${spring.version}</version> </dependency>

XML配置方式

在Servlet2.5規範中,必須配置web.xml,我們只需要配置DispatcherServlet,SpringMVC的核心控制器就可以了
相關程式碼在Chapter5/springmvc-servlet25中
web.xml

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">

  <display-name>SpringMVC Servlet2.5 Application</display-name>

  <servlet>
    <servlet-name>spring-webmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring-webmvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
</web-app>

然後在WEB-INF下配置spring-webmvc-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.springframework.org/schema/util
        http://www.springframework.org/schema/util/spring-util-3.2.xsd">

  <context:property-placeholder/>

  <bean id="sender" class="zipkin2.reporter.okhttp3.OkHttpSender" factory-method="create">
    <constructor-arg type="String" value="http://localhost:9411/api/v2/spans"/>
  </bean>

  <bean id="tracing" class="brave.spring.beans.TracingFactoryBean">
    <property name="localServiceName" value="${zipkin.service:springmvc-servlet25-example}"/>
    <property name="spanReporter">
      <bean class="brave.spring.beans.AsyncReporterFactoryBean">
        <property name="encoder" value="JSON_V2"/>
        <property name="sender" ref="sender"/>
        <!-- wait up to half a second for any in-flight spans on close -->
        <property name="closeTimeout" value="500"/>
      </bean>
    </property>
    <property name="propagationFactory">
      <bean id="propagationFactory" class="brave.propagation.ExtraFieldPropagation" factory-method="newFactory">
        <constructor-arg index="0">
          <util:constant static-field="brave.propagation.B3Propagation.FACTORY"/>
        </constructor-arg>
        <constructor-arg index="1">
          <list>
            <value>user-name</value>
          </list>
        </constructor-arg>
      </bean>
    </property>
    <property name="currentTraceContext">
      <bean class="brave.context.log4j2.ThreadContextCurrentTraceContext" factory-method="create"/>
    </property>
  </bean>

  <bean id="httpTracing" class="brave.spring.beans.HttpTracingFactoryBean">
    <property name="tracing" ref="tracing"/>
  </bean>

  <bean id="restTemplate" class="org.springframework.web.client.RestTemplate">
    <property name="interceptors">
      <list>
        <bean class="brave.spring.web.TracingClientHttpRequestInterceptor" factory-method="create">
          <constructor-arg type="brave.http.HttpTracing" ref="httpTracing"/>
        </bean>
      </list>
    </property>
  </bean>

  <mvc:interceptors>
    <bean class="brave.spring.webmvc.TracingHandlerInterceptor" factory-method="create">
      <constructor-arg type="brave.http.HttpTracing" ref="httpTracing"/>
    </bean>
  </mvc:interceptors>

  <!-- Loads the controller -->
  <context:component-scan base-package="org.mozhu.zipkin.springmvc"/>
  <mvc:annotation-driven/>
</beans>

使用brave.spring.beans.TracingFactoryBean建立tracing
使用brave.spring.beans.HttpTracingFactoryBean建立httpTracing
配置springmvc的攔截器brave.spring.webmvc.TracingHandlerInterceptor
並配置org.springframework.web.client.RestTemplate作為客戶端傳送http請求

再來看看兩個Controller:Frontend和Backend,和前面FrontendServlet,BackendServlet功能一樣

Frontend

@RestController
public class Frontend {
    private final static Logger LOGGER = LoggerFactory.getLogger(Frontend.class);
    @Autowired
    RestTemplate restTemplate;

    @RequestMapping("/")
    public String callBackend() {
        LOGGER.info("frontend receive request");
        return restTemplate.getForObject("http://localhost:9000/api", String.class);
    }
}

Frontend中使用Spring提供的restTemplate向Backend傳送請求

Backend

@RestController
public class Backend {

  private final static Logger LOGGER = LoggerFactory.getLogger(Backend.class);

  @RequestMapping("/api")
  public String printDate(@RequestHeader(name = "user-name", required = false) String username) {
    LOGGER.info("backend receive request");
    if (username != null) {
      return new Date().toString() + " " + username;
    }
    return new Date().toString();
  }
}

Backend中收到來自Frontend的請求,並給出響應,打出當前的時間戳,如果headers中存在user-name,也會新增到響應字串尾部

跟前面博文一樣,啟動Zipkin,然後分別執行

mvn jetty:run -Pbackend
mvn jetty:run -Pfrontend

瀏覽器訪問 http://localhost:8081/ 會顯示當前時間
在Zipkin的Web介面中,也能查詢到這次跟蹤資訊

現在來分析下兩個Spring相關的類
brave.spring.webmvc.TracingHandlerInterceptor - 服務端請求的攔截器,在這個類裡會處理服務端的trace資訊
brave.spring.web.TracingClientHttpRequestInterceptor - 客戶端請求的攔截器,在這個類裡會處理客戶端的trace資訊

TracingHandlerInterceptor

public final class TracingHandlerInterceptor extends HandlerInterceptorAdapter {

  public static AsyncHandlerInterceptor create(Tracing tracing) {
    return new TracingHandlerInterceptor(HttpTracing.create(tracing));
  }

  public static AsyncHandlerInterceptor create(HttpTracing httpTracing) {
    return new TracingHandlerInterceptor(httpTracing);
  }

  final Tracer tracer;
  final HttpServerHandler<HttpServletRequest, HttpServletResponse> handler;
  final TraceContext.Extractor<HttpServletRequest> extractor;

  @Autowired TracingHandlerInterceptor(HttpTracing httpTracing) { // internal
    tracer = httpTracing.tracing().tracer();
    handler = HttpServerHandler.create(httpTracing, new HttpServletAdapter());
    extractor = httpTracing.tracing().propagation().extractor(HttpServletRequest::getHeader);
  }

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) {
    if (request.getAttribute(SpanInScope.class.getName()) != null) {
      return true; // already handled (possibly due to async request)
    }

    Span span = handler.handleReceive(extractor, request);
    request.setAttribute(SpanInScope.class.getName(), tracer.withSpanInScope(span));
    return true;
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
      Object o, Exception ex) {
    Span span = tracer.currentSpan();
    if (span == null) return;
    ((SpanInScope) request.getAttribute(SpanInScope.class.getName())).close();
    handler.handleSend(response, ex, span);
  }
}

TracingHandlerInterceptor繼承了HandlerInterceptorAdapter,覆蓋了其中preHandle和afterCompletion方法,分別在請求執行前,和請求完成後執行。
這裡沒辦法向前面幾篇博文的一樣,使用try-with-resources來自動關閉SpanInScope,所以只能在preHandle中將SpanInScope放在request的attribute中,然後在afterCompletion中將其取出來手動close,其他程式碼邏輯和前面TracingFilter裡一樣

TracingClientHttpRequestInterceptor

public final class TracingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
  static final Propagation.Setter<HttpHeaders, String> SETTER = HttpHeaders::set;

  public static ClientHttpRequestInterceptor create(Tracing tracing) {
    return create(HttpTracing.create(tracing));
  }

  public static ClientHttpRequestInterceptor create(HttpTracing httpTracing) {
    return new TracingClientHttpRequestInterceptor(httpTracing);
  }

  final Tracer tracer;
  final HttpClientHandler<HttpRequest, ClientHttpResponse> handler;
  final TraceContext.Injector<HttpHeaders> injector;

  @Autowired TracingClientHttpRequestInterceptor(HttpTracing httpTracing) {
    tracer = httpTracing.tracing().tracer();
    handler = HttpClientHandler.create(httpTracing, new HttpAdapter());
    injector = httpTracing.tracing().propagation().injector(SETTER);
  }

  @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body,
      ClientHttpRequestExecution execution) throws IOException {
    Span span = handler.handleSend(injector, request.getHeaders(), request);
    ClientHttpResponse response = null;
    Throwable error = null;
    try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
      return response = execution.execute(request, body);
    } catch (IOException | RuntimeException | Error e) {
      error = e;
      throw e;
    } finally {
      handler.handleReceive(response, error, span);
    }
  }

  static final class HttpAdapter
      extends brave.http.HttpClientAdapter<HttpRequest, ClientHttpResponse> {

    @Override public String method(HttpRequest request) {
      return request.getMethod().name();
    }

    @Override public String url(HttpRequest request) {
      return request.getURI().toString();
    }

    @Override public String requestHeader(HttpRequest request, String name) {
      Object result = request.getHeaders().getFirst(name);
      return result != null ? result.toString() : null;
    }

    @Override public Integer statusCode(ClientHttpResponse response) {
      try {
        return response.getRawStatusCode();
      } catch (IOException e) {
        return null;
      }
    }
  }
}

TracingClientHttpRequestInterceptor裡的邏輯和前面博文分析的brave.okhttp3.TracingInterceptor類似,此處不再展開分析

下面再來介紹用Annotation註解方式來配置SpringMVC和Brave

Annotation註解方式

相關程式碼在Chapter5/springmvc-servlet3中
在Servlet3以後,web.xml不是必須的了,org.mozhu.zipkin.springmvc.Initializer是我們整個應用的啟動器

public class Initializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override protected String[] getServletMappings() {
    return new String[] {"/"};
  }

  @Override protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override protected Class<?>[] getServletConfigClasses() {
    return new Class[] {TracingConfiguration.class};
  }
}

org.mozhu.zipkin.springmvc.Initializer,繼承自AbstractDispatcherServletInitializer,實現了WebApplicationInitializer
WebApplicationInitializer

public interface WebApplicationInitializer {

    void onStartup(ServletContext servletContext) throws ServletException;

}

關於Servlet3的容器是如何啟動的,我們再來看一個類SpringServletContainerInitializer,該類實現了javax.servlet.ServletContainerInitializer介面,並且該類上有一個javax.servlet.annotation.HandlesTypes註解
Servlet3規範規定實現Servlet3的容器,必須載入classpath裡所有實現了ServletContainerInitializer介面的類,並呼叫其onStartup方法,傳入的第一個引數是類上HandlesTypes中所指定的類,這裡是WebApplicationInitializer的集合
在SpringServletContainerInitializer的onStartup方法中,會將傳入的WebApplicationInitializer類,全部例項化,並且排序,然後依次呼叫它們的initializer.onStartup(servletContext)。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {

        List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

        if (webAppInitializerClasses != null) {
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer) waiClass.newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }

        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }

        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }

}

另外Servlet3在ServletContext中提供了addServlet方法,允許以編碼方式向容器中新增Servlet

public ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet);

而在AbstractDispatcherServletInitializer中registerDispatcherServlet方法會將SpringMVC的核心控制器DispatcherServlet新增到Web容器中。

protected void registerDispatcherServlet(ServletContext servletContext) {
    String servletName = getServletName();
    Assert.hasLength(servletName, "getServletName() must not return empty or null");

    WebApplicationContext servletAppContext = createServletApplicationContext();
    Assert.notNull(servletAppContext,
            "createServletApplicationContext() did not return an application " +
            "context for servlet [" + servletName + "]");

    FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
    dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

    ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
    Assert.notNull(registration,
            "Failed to register servlet with name '" + servletName + "'." +
            "Check if there is another servlet registered under the same name.");

    registration.setLoadOnStartup(1);
    registration.addMapping(getServletMappings());
    registration.setAsyncSupported(isAsyncSupported());

    Filter[] filters = getServletFilters();
    if (!ObjectUtils.isEmpty(filters)) {
        for (Filter filter : filters) {
            registerServletFilter(servletContext, filter);
        }
    }

    customizeRegistration(registration);
}
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
    return new DispatcherServlet(servletAppContext);
}

以前用xml配置bean的方式,全改為在TracingConfiguration類裡用@Bean註解來配置,並且使用@ComponentScan註解指定controller的package,讓Spring容器可以掃描到這些Controller

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.mozhu.zipkin.springmvc")
@Import({TracingClientHttpRequestInterceptor.class, TracingHandlerInterceptor.class})
public class TracingConfiguration extends WebMvcConfigurerAdapter {

  @Bean Sender sender() {
    return OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans");
  }

  @Bean AsyncReporter<Span> spanReporter() {
    return AsyncReporter.create(sender());
  }

  @Bean Tracing tracing(@Value("${zipkin.service:springmvc-servlet3-example}") String serviceName) {
    return Tracing.newBuilder()
        .localServiceName(serviceName)
        .propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "user-name"))
        .currentTraceContext(ThreadContextCurrentTraceContext.create()) // puts trace IDs into logs
        .spanReporter(spanReporter()).build();
  }

  @Bean HttpTracing httpTracing(Tracing tracing) {
    return HttpTracing.create(tracing);
  }

  @Autowired
  private TracingHandlerInterceptor serverInterceptor;

  @Autowired
  private TracingClientHttpRequestInterceptor clientInterceptor;

  @Bean RestTemplate restTemplate() {
    RestTemplate restTemplate = new RestTemplate();
    List<ClientHttpRequestInterceptor> interceptors =
      new ArrayList<>(restTemplate.getInterceptors());
    interceptors.add(clientInterceptor);
    restTemplate.setInterceptors(interceptors);
    return restTemplate;
  }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(serverInterceptor);
  }
}

然後在getServletConfigClasses方法中指定TracingConfiguration,讓Spring容器可以載入所有的配置

  @Override protected Class<?>[] getServletConfigClasses() {
    return new Class[] {TracingConfiguration.class};
  }

Annotation和XML配置的方式相比,簡化了不少,而其中使用的Tracing相關的類都一樣,這裡就不用再分析了