不一樣與其它中間件框架,Apollo中有大量的業務代碼,它向咱們展現了大神是如何寫業務代碼的:maven依賴的層次結構,如何進行基礎包配置,以及工具類編寫,能夠稱之爲springboot之最佳實踐。html
apollo中有7個子項目
最重要的有四個
apollo-portal:後臺管理服務
apollo-admin:後臺配置管理服務,用戶發佈了的配置項會通過portal->admin更新到數據庫
apollo-configservice: 配置管理服務,客戶端經過該服務拉取配置項
apollo-client:客戶端,集成該客戶端拉取配置項
此外還有apollo-biz,apollo-common,apollo-core提供基礎服務git
其依賴關係以下
github
utils中集成了了一些通用方法,好比判斷非空,對象拷貝,字符串拼接等spring
實現不一樣類對象中屬性的拷貝,服務之間傳遞的都是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; }
//將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
驗證ClusterName和AppName是否正確服務器
作非空、正數判斷等,抽象出了一個類,而不用硬編碼了網絡
RequestPrecondition.checkArguments(!StringUtils.isContainEmpty(model.getReleasedBy(), model .getReleaseTitle()), "Params(releaseTitle and releasedBy) can not be empty");
key值生成器
封裝經常使用的異常處理類,對常見的異常作了分類,好比業務異常,服務異常,not found異常等,你們作異常時不妨參考下其對異常的分類。
apollo異常基類,設置了httpstatus,便於返回準確的http的報錯信息,其繼承了RuntimeException,並加入了一個httpStatus
public abstract class AbstractApolloHttpException extends RuntimeException{ protected HttpStatus httpStatus; ... }
業務異常類,下圖能夠看出其對業務異常的分類描述
某個值找不到了
當對應的服務不可達,好比這段
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)));
BeanUtils中進行對象轉換時發生異常類
封裝了異常處理中心,報文轉換,http序列換等工具
實現了WebMvcConfigurer和WebServerFactoryCustomizer
public class WebMvcConfig implements WebMvcConfigurer, WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
而咱們的WebMvcConfigurer是個接口,類實現這個接口來具有必定的能力,如下就列出了這些能力
挑重點介紹下
@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
@Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { configurer.favorPathExtension(false); configurer.ignoreAcceptHeader(true).defaultContentType(MediaType.APPLICATION_JSON); }
視圖解析器,這裏的配置指的是不檢查accept頭,並且默認請求爲json格式。
靜態資源控制器
@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); }
靜態資源的訪問時間。
定製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 ); }
統一異常處理類,用於抓取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
將異常進行分類能方便直觀的展現所遇到的異常
根據用戶請求頭來格式化不一樣對象。請求傳給服務器的都是一個字符串流,而服務器根據用戶請求頭判斷不一樣媒體類型,而後在已註冊的轉換器中查找對應的轉換器,好比在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處理中對於日期格式的處理也是一個大問題,因此這裏也定義了日期格式轉換器。
@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.
裏面只定義了一個類,用於給全部的數據庫操做都加上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 { ... }
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; } }