摘要:不同與其它中間件框架,中有大量的業務代碼,它向我們展示了大神是如何寫業務代碼的依賴的層次結構,如何進行基礎包配置,以及工具類編寫,可以稱之為之最佳實踐。代碼參考視圖解析器,這里的配置指的是不檢查頭,而且默認請求為格式。
不同與其它中間件框架,Apollo中有大量的業務代碼,它向我們展示了大神是如何寫業務代碼的:maven依賴的層次結構,如何進行基礎包配置,以及工具類編寫,可以稱之為springboot之最佳實踐。
一 apollo項目依賴apollo中有7個子項目
最重要的有四個
apollo-portal:后臺管理服務
apollo-admin:后臺配置管理服務,用戶發布了的配置項會經過portal->admin更新到數據庫
apollo-configservice: 配置管理服務,客戶端通過該服務拉取配置項
apollo-client:客戶端,集成該客戶端拉取配置項
此外還有apollo-biz,apollo-common,apollo-core提供基礎服務
其依賴關系如下
utils中集成了了一些通用方法,比如判斷非空,對象拷貝,字符串拼接等
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);
源碼:
// 封裝{@link org.springframework.beans.BeanUtils#copyProperties},慣用與直接將轉換結果返回 public staticExceptionUtils 將exception轉為StringT transfrom(Class 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源碼
public static String toString(HttpStatusCodeException e) { MaperrorAttributes = 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的使用
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異常等,大家做異常時不妨參考下其對異常的分類。
AbstractApolloHttpExceptionapollo異常基類,設置了httpstatus,便于返回準確的http的報錯信息,其繼承了RuntimeException,并加入了一個httpStatus
public abstract class AbstractApolloHttpException extends RuntimeException{ protected HttpStatus httpStatus; ... }BadRequestException
業務異常類,下圖可以看出其對業務異常的分類描述
某個值找不到了
當對應的服務不可達,比如這段
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{
而我們的WebMvcConfigurer是個接口,類實現這個接口來具備一定的能力,以下就列出了這些能力
挑重點介紹下
@Override public void addArgumentResolvers(ListargumentResolvers) { 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 ); }3.2 GlobalDefaultExceptionHandler
統一異常處理類,用于抓取controller層的所有異常,從此再也不用寫超級多的try...catch了。只要加了@ControllerAdvice就能抓取所有異常了。
@ControllerAdvice public class GlobalDefaultExceptionHandler {
而后使用@ExcepionHandler來抓取異常,比如這樣
//處理系統內置的Exception @ExceptionHandler(Throwable.class) public ResponseEntity
在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> converters = Lists.newArrayList( new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(), new AllEncompassingFormHttpMessageConverter(), gsonHttpMessageConverter); return new HttpMessageConverters() { @Override public List > 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 SetactiveProfiles = Sets.newHashSet(context.getEnvironment().getActiveProfiles()); //獲取profile中的所有制 Set requiredActiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnProfile.class.getName()); Set requiredInactiveProfiles = retrieveAnnotatedProfiles(metadata, ConditionalOnMissingProfile.class .getName()); return Sets.difference(requiredActiveProfiles, activeProfiles).isEmpty() && Sets.intersection(requiredInactiveProfiles, activeProfiles).isEmpty(); } private Set retrieveAnnotatedProfiles(AnnotatedTypeMetadata metadata, String annotationType) { if (!metadata.isAnnotated(annotationType)) { return Collections.emptySet(); } MultiValueMap attributes = metadata.getAllAnnotationAttributes(annotationType); if (attributes == null) { return Collections.emptySet(); } Set 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; } }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/77852.html
摘要:零為何要學源碼簡單,是我現在看起來最簡單的源碼不會像封裝了一層又一層,把人繞暈,而沒有那么多封裝,上手快,我們學習就應該從簡單的開始憑什么非要去學封的像粽子一樣的源碼,我們就是要去學簡簡單單,平時樸素,接地氣的源碼最接近業務代碼的源碼。 零 為何要學apollo源碼 1 簡單,Apollo是我現在看起來最簡單的源碼不會像spring封裝了一層又一層,把人繞暈,而apollo沒有那么多封...
摘要:分鐘學是一個系列,簡單暴力,包學包會。接管了請求和狀態管理。一般在生產環境中,我們通常還希望做權限驗證請求攔截等事務處理。 21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。 搭建 Apollo client 端,集成 redux使用 apollo-client 來獲取數據修改本地的 apollo store 數據提供定制方案 請求攔截 封裝修改 clie...
摘要:改造背景前面我們講解了如何對接來持久化限流的規則,對接后可以直接通過的后臺進行規則的修改,推送到各個客戶端實時生效。因此推送規則正確做法應該是配置中心控制臺控制臺配置中心數據源,而不是經數據源推送至配置中心。 改造背景 前面我們講解了如何對接Apollo來持久化限流的規則,對接后可以直接通過Apollo的后臺進行規則的修改,推送到各個客戶端實時生效。 但還有一個問題就是Sentinel...
摘要:分鐘學是一個系列,簡單暴力,包學包會。一旦你丟失了,可能會導致寫入失敗,或者盡管寫入了,但本該攜帶的那一層的數據沒有寫入。 21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。 搭建 Apollo client 端,集成 redux使用 apollo-client 來獲取數據修改本地的 apollo store 數據提供定制方案 請求攔截 封裝修改 clie...
摘要:分鐘學是一個系列,簡單暴力,包學包會。那怎么辦呢本章就教你非常簡單地實現擴展的。我們可以借鑒的的寫法,為的實例添加一些自己的方法。更重要的是,也會有的效果,上一個的輸出會成為下一個的輸入,便于組合。 21 分鐘學 apollo-client 是一個系列,簡單暴力,包學包會。 搭建 Apollo client 端,集成 redux使用 apollo-client 來獲取數據修改本地的 ...
閱讀 1028·2023-04-26 02:26
閱讀 2134·2021-09-26 10:16
閱讀 1544·2019-08-30 12:57
閱讀 3461·2019-08-29 16:10
閱讀 3213·2019-08-29 13:47
閱讀 1182·2019-08-29 13:12
閱讀 2135·2019-08-29 11:11
閱讀 1330·2019-08-26 13:28