不一样与其它中间件框架,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; } }