Apollo源碼分析(二): Apollo的代碼層次

不一樣與其它中間件框架,Apollo中有大量的業務代碼,它向咱們展現了大神是如何寫業務代碼的:maven依賴的層次結構,如何進行基礎包配置,以及工具類編寫,能夠稱之爲springboot之最佳實踐。html

一 apollo項目依賴

apollo中有7個子項目
最重要的有四個
apollo-portal:後臺管理服務
apollo-admin:後臺配置管理服務,用戶發佈了的配置項會通過portal->admin更新到數據庫
apollo-configservice: 配置管理服務,客戶端經過該服務拉取配置項
apollo-client:客戶端,集成該客戶端拉取配置項
此外還有apollo-biz,apollo-common,apollo-core提供基礎服務git

其依賴關係以下
圖片描述github

二 apollo-common分析

圖片描述

1 utils 基礎包

utils中集成了了一些通用方法,好比判斷非空,對象拷貝,字符串拼接等spring

BeanUtils 拷貝對象

實現不一樣類對象中屬性的拷貝,服務之間傳遞的都是dto對象,而在使用時必須轉換爲用法:數據庫

//在網絡中傳輸的爲DTO對象,而程序中處理的是實體類對象
@RequestMapping(path = "/apps", method = RequestMethod.POST)
public AppDTO create(@RequestBody AppDTO dto) {
 ...
//DTO拷貝成實體類
App entity = BeanUtils.transfrom(App.class, dto);
...
//實體類再拷貝成DTO   
dto = BeanUtils.transfrom(AppDTO.class, entity);

源碼:json

// 封裝{@link org.springframework.beans.BeanUtils#copyProperties},慣用與直接將轉換結果返回
  public static <T> T transfrom(Class<T> clazz, Object src) {
    if (src == null) {
      return null;
    }
    T instance = null;
    try {
      instance = clazz.newInstance();
    } catch (Exception e) {
      throw new BeanUtilsException(e);
    }
    org.springframework.beans.BeanUtils.copyProperties(src, instance, getNullPropertyNames(src));
    return instance;
  }

ExceptionUtils 將exception轉爲String

//將exception轉爲String
 } catch (IllegalAccessException ex) {
    if (logger.isErrorEnabled()) {
        logger.error(ExceptionUtils.exceptionToString(ex));

ExceptionUtils源碼tomcat

public static String toString(HttpStatusCodeException e) {
    Map<String, Object> errorAttributes = gson.fromJson(e.getResponseBodyAsString(), mapType);
    if (errorAttributes != null) {
      return MoreObjects.toStringHelper(HttpStatusCodeException.class).omitNullValues()
          .add("status", errorAttributes.get("status"))
          .add("message", errorAttributes.get("message"))

當中用到了Guava的MoreObjects的鏈式調用來優雅的拼接字符串,參考Guava Object的使用springboot

InputValidator

驗證ClusterName和AppName是否正確服務器

RequestPrecondition

作非空、正數判斷等,抽象出了一個類,而不用硬編碼了網絡

RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model
            .getReleaseTitle()),
        "Params(releaseTitle and releasedBy) can not be empty");

UniqueKeyGenerator

key值生成器

2 exception 異常包

封裝經常使用的異常處理類,對常見的異常作了分類,好比業務異常,服務異常,not found異常等,你們作異常時不妨參考下其對異常的分類。

AbstractApolloHttpException

apollo異常基類,設置了httpstatus,便於返回準確的http的報錯信息,其繼承了RuntimeException,並加入了一個httpStatus

public abstract class AbstractApolloHttpException extends RuntimeException{
      protected HttpStatus httpStatus;
      ...
}

BadRequestException

業務異常類,下圖能夠看出其對業務異常的分類描述
圖片描述

NotFoundException

某個值找不到了
圖片描述

ServiceException

當對應的服務不可達,好比這段

ServiceException e = new ServiceException(String.format("No available admin server."
                                                              + " Maybe because of meta server down or all admin server down. "
                                                              + "Meta server address: %s",
                                                              MetaDomainConsts.getDomain(env)));

BeanUtilsException

BeanUtils中進行對象轉換時發生異常類

3 Controller包

封裝了異常處理中心,報文轉換,http序列換等工具

3.1 WebMvcConfig

實現了WebMvcConfigurer和WebServerFactoryCustomizer

public class WebMvcConfig implements WebMvcConfigurer, WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

而咱們的WebMvcConfigurer是個接口,類實現這個接口來具有必定的能力,如下就列出了這些能力
clipboard.png

挑重點介紹下

HandlerMethodArgumentResolver
@Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    PageableHandlerMethodArgumentResolver pageResolver =
            new PageableHandlerMethodArgumentResolver();
    pageResolver.setFallbackPageable(PageRequest.of(0, 10));

    argumentResolvers.add(pageResolver);
  }

重載HandlerMethodArgumentResolver是作啥用的呢?簡單來講就是用來處理spring mvc中各種參數,好比@RequestParam、@RequestHeader、@RequestBody、@PathVariable、@ModelAttribute

而是使用了addArgumentResolvers後就加入了新的參數處理能力。HandlerMethodArgumentResolver中有兩個最重要的參數

supportsParameter:用於斷定是否須要處理該參數分解,返回true爲須要,並會去調用下面的方法resolveArgument。
resolveArgument:真正用於處理參數分解的方法,返回的Object就是controller方法上的形參對象。

好比apollo就加入的是對分頁的處理: PageableHandlerMethodArgumentResolver

這裏咱們能夠看個例子,有這樣一個業務場景,用戶傳的報文在網絡中作了加密處理,須要對用戶報文作解密,至關一個公共處理邏輯,寫到業務代碼中不方便維護,此時就能夠增長一個HandlerMethodArgumentResolver用於解密。代碼參考github:xxx

configureContentNegotiation
@Override
  public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer.favorPathExtension(false);
    configurer.ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON);
  }

視圖解析器,這裏的配置指的是不檢查accept頭,並且默認請求爲json格式。

addResourceHandlers

靜態資源控制器

@Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // 10 days
    addCacheControl(registry, "img", 864000);
    addCacheControl(registry, "vendor", 864000);
    // 1 day
    addCacheControl(registry, "scripts", 86400);
    addCacheControl(registry, "styles", 86400);
    addCacheControl(registry, "views", 86400);
  }

靜態資源的訪問時間。

WebServerFactoryCustomizer

定製tomcat,spring boot集成了tomcat,在2.0以上版本中,經過實現WebServerFactoryCustomizer類來自定義tomcat,好比在這裏設置字符集

@Override
  public void customize(TomcatServletWebServerFactory factory) {
    MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT);
    mappings.add("html", "text/html;charset=utf-8");
    factory.setMimeMappings(mappings );

  }

3.2 GlobalDefaultExceptionHandler

統一異常處理類,用於抓取controller層的全部異常,今後不再用寫超級多的try...catch了。只要加了@ControllerAdvice就能抓取全部異常了。

@ControllerAdvice
public class GlobalDefaultExceptionHandler {

然後使用@ExcepionHandler來抓取異常,好比這樣

//處理系統內置的Exception
  @ExceptionHandler(Throwable.class)
  public ResponseEntity<Map<String, Object>> exception(HttpServletRequest request, Throwable ex) {
    return handleError(request, INTERNAL_SERVER_ERROR, ex);
  }

在apollo中定義了這幾個異常:
內置異常: Throwable,HttpRequestMethodNotSupportedException,HttpStatusCodeException,AccessDeniedException
以及apollo自定義的異常AbstractApolloHttpException
將異常進行分類能方便直觀的展現所遇到的異常

3.3 HttpMessageConverters

根據用戶請求頭來格式化不一樣對象。請求傳給服務器的都是一個字符串流,而服務器根據用戶請求頭判斷不一樣媒體類型,而後在已註冊的轉換器中查找對應的轉換器,好比在content-type中發現json後,就能轉換成json對象了

@Configuration
public class HttpMessageConverterConfiguration {
  @Bean
  public HttpMessageConverters messageConverters() {
    GsonHttpMessageConverter gsonHttpMessageConverter = new GsonHttpMessageConverter();
    gsonHttpMessageConverter.setGson(
            new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create());
    final List<HttpMessageConverter<?>> converters = Lists.newArrayList(
            new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
            new AllEncompassingFormHttpMessageConverter(), gsonHttpMessageConverter);
    return new HttpMessageConverters() {
      @Override
      public List<HttpMessageConverter<?>> getConverters() {
        return converters;
      }
    };
  }
}

apollo中自定了GsonHttpMessageConverter,重寫了默認的json轉換器,這種轉換固然更快樂,Gson是google的一個json轉換器,固然,傳說ali 的fastjson會更快,可是貌似fastjson問題會不少。json處理中對於日期格式的處理也是一個大問題,因此這裏也定義了日期格式轉換器。

3.4 CharacterEncodingFilterConfiguration 過濾器

@Configuration
public class CharacterEncodingFilterConfiguration {

  @Bean
  public FilterRegistrationBean encodingFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new CharacterEncodingFilter());
    bean.addInitParameter("encoding", "UTF-8");
    bean.setName("encodingFilter");
    bean.addUrlPatterns("/*");
    bean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD);
    return bean;
  }
}

加入了一個CharacterEncodingFilter將全部的字符集所有轉換成UTF-8.

四 aop包

裏面只定義了一個類,用於給全部的數據庫操做都加上cat鏈路跟蹤,簡單看下它的用法

@Aspect  //定義一個切面
@Component
public class RepositoryAspect {
/**
** 全部Repository下的類都必須都添加切面
*/
  @Pointcut("execution(public * org.springframework.data.repository.Repository+.*(..))")
  public void anyRepositoryMethod() {
  }
/**
** 切面的具體方法
*/
  @Around("anyRepositoryMethod()")
  public Object invokeWithCatTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
    ...
}

五 condition 條件註解

cloud條件註解

@ConditionalOnBean:當SpringIoc容器內存在指定Bean的條件
@ConditionalOnClass:當SpringIoc容器內存在指定Class的條件
@ConditionalOnExpression:基於SpEL表達式做爲判斷條件
@ConditionalOnJava:基於JVM版本做爲判斷條件
@ConditionalOnJndi:在JNDI存在時查找指定的位置
@ConditionalOnMissingBean:當SpringIoc容器內不存在指定Bean的條件
@ConditionalOnMissingClass:當SpringIoc容器內不存在指定Class的條件
@ConditionalOnNotWebApplication:當前項目不是Web項目的條件
@ConditionalOnProperty:指定的屬性是否有指定的值
@ConditionalOnResource:類路徑是否有指定的值
@ConditionalOnSingleCandidate:當指定Bean在SpringIoc容器內只有一個,或者雖然有多個可是指定首選的Bean
@ConditionalOnWebApplication:當前項目是Web項目的條件

ConditionalOnBean

@Configuration
public class Configuration1 {

    @Bean
    @ConditionalOnBean(Bean2.class)
    public Bean1 bean1() {
        return new Bean1();
    }
}

@Configuration
public class Configuration2 {

@Bean
public Bean2 bean2(){
    return new Bean2();
}

在spring ioc的過程當中,優先解析@Component,@Service,@Controller註解的類。其次解析配置類,也就是@Configuration標註的類。最後開始解析配置類中定義的bean。

在apollo中使用自定義condition:

用註解實現spi

@Configuration
  @Profile("ctrip")
  public static class CtripEmailConfiguration {

    @Bean
    public EmailService ctripEmailService() {
      return new CtripEmailService();
    }

    @Bean
    public CtripEmailRequestBuilder emailRequestBuilder() {
      return new CtripEmailRequestBuilder();
    }
  }

spi的定義: SPI 全稱爲 Service Provider Interface,是一種服務發現機制。SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類。這樣能夠在運行時,動態爲接口替換實現類。正所以特性,咱們能夠很容易的經過 SPI 機制爲咱們的程序提供拓展功能。

@Profile("ctrip")是特指在系統環境變量中存在ctrip時纔會生效,限定了方法的生效環境。
還有一種常見的方式是作數據庫配置,好比在不一樣的dev,stg,prd環境中配置不一樣的地址,或者使用不一樣的數據庫:

@Profile("dev")
  @Profile("stg")
  @Profile("prd")

但這樣存在一個問題是沒法知足devops的一次編譯,多處運行的原則,所以最好是將配置放置與外部,經過不一樣環境特徵來獲取不一樣的配置文件。

看下自定義的condition是如何實現的:

定義註解

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnProfileCondition.class)   //具體的實現類
public @interface ConditionalOnMissingProfile {    //註解的名稱
  /**
   * The profiles that should be inactive
   * @return
   */
  String[] value() default {};
}

然後再實現類中實現了對環境變量的判斷

//實現condition接口
public class OnProfileCondition implements Condition {
  //若是match則返回true
  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    //獲取環境變量中全部的active值, spring.profile.active=xxx
    Set<String> activeProfiles = Sets.newHashSet(context.getEnvironment().getActiveProfiles());
    //獲取profile中的全部制
    Set<String> requiredActiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnProfile.class.getName());
    Set<String> requiredInactiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnMissingProfile.class
        .getName());

    return Sets.difference(requiredActiveProfiles, activeProfiles).isEmpty()
        && Sets.intersection(requiredInactiveProfiles, activeProfiles).isEmpty();
  }

  private Set<String> retrieveAnnotatedProfiles(AnnotatedTypeMetadata metadata, String annotationType) {
    if (!metadata.isAnnotated(annotationType)) {
      return Collections.emptySet();
    }

    MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(annotationType);

    if (attributes == null) {
      return Collections.emptySet();
    }

    Set<String> profiles = Sets.newHashSet();
    List<?> values = attributes.get("value");

    if (values != null) {
      for (Object value : values) {
        if (value instanceof String[]) {
          Collections.addAll(profiles, (String[]) value);
        }
        else {
          profiles.add((String) value);
        }
      }
    }

    return profiles;
  }
}
相關文章
相關標籤/搜索