
用Spring data rest开发基于HATEOAS的API
概念H2
首先,HATEOAS (Hypermedia as the Engine of Application State) 是REST应用架构的一个约束。一个hypermedia-driven的站点通过响应中的超媒体链接动态地提供了导航到站点的REST接口的信息。
下面是一个基于HATEOAS的响应
bash
{"name": "Alice","links": [ {"rel": "self","href": "http://localhost:8080/customer/1"} ]}
Spring使用HAL作为HATEOAS的实现。
Spring Data Rest是Spring Data的子项目,那么该项目是为了实现类似Spring Data JPA的统一数据访问层接口的目的。 实际情况,只要定义了Spring Data的标准Repository接口,那么Spring Data Rest便会为你提供一套标准的基于HTEOAS的REST接口。 如果你的应用架构是基于DDD的,那么对Spring Data Rest接口接入会显得非常友好。
Spring对于HAL的实现H2
使用Spring Data Rest时,RepositoryRestMvcConfiguration中注册的jacksonHttpMessageConverter是负责把ResourceSupport子类对象渲染成HAL格式的JSON字符串的。通常情况下对于请求的Media Type是application/hal+json时,才会用这个converter进行转化。useHalAsDefaultJsonMediaType可以控制,当请求JSON media type时,是否默认使用HAL。这个参数的默认值是true,也就是说如果客户端请求的是普通的application/json,对于ResourceSupport子类对象依然可以返回HAL格式的JSON。 另外如果请求时不指定Media Type的,那么Spring Data Rest的defaultMediaType配置将会生效,默认值为application/hal+json。
下面是注册jacksonHttpMessageConverter的相关代码
bash
@Beanpublic TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter() {ArrayList<MediaType> mediaTypes = new ArrayList<MediaType>();mediaTypes.add(MediaTypes.HAL_JSON);// Enable returning HAL if application/json is asked if it's configured to be the default typeif (config().useHalAsDefaultJsonMediaType()) {mediaTypes.add(MediaType.APPLICATION_JSON);}int order = config().useHalAsDefaultJsonMediaType() ? Ordered.LOWEST_PRECEDENCE - 10: Ordered.LOWEST_PRECEDENCE - 1;TypeConstrainedMappingJackson2HttpMessageConverter converter = new ResourceSupportHttpMessageConverter(order);converter.setObjectMapper(halObjectMapper());converter.setSupportedMediaTypes(mediaTypes);return converter;}
如果没有使用Spring Data Rest而是单独使用Spring HATOAS的话,这个jacksonHttpMessageConverter将由HypermediaSupportBeanDefinitionRegistrar来注册。 Spring Boot的情况,HypermediaAutoConfiguration会导入HypermediaHttpMessageConverterConfiguration来针对spring.hateoas.use-hal-as-default-json-media-type配置来支持application/json。
这里是关于Spring HATEOAS和Spring Data Rest的自动配置的一些说明。在这次修复之前,由于RepositoryRestMvcAutoConfiguration会早于JacksonAutoConfiguration运行,导致JacksonAutoConfiguration被间接的关闭,没有注册@Primary的ObjectMapper,从而导致注入到JacksonHttpMessageConvertersConfiguration的ObjectMapper是一个被HAL全局污染的ObjectMapper。
使用中可能会遇到的一些问题H2
下面的问题都是针对Spring Data Rest配合Spring Data JPA一起使用,并使用Hibernate作为JPA的Vendor。
自定义方法实现H3
有些时候,我们希望覆盖Spring Data Rest的标准实现,或者实现一些额外的接口,就需要自定义处理方法。
自定义的Controller需要使用@RepositoryRestController注解,这样才能让Spring Data Rest处理。
URL的路径必须属于某个Repository的资源路径,下面是
RepositoryRestHandlerMapping的一段处理逻辑bash
@Overrideprotected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {HandlerMethod handlerMethod = super.lookupHandlerMethod(lookupPath, request);if (handlerMethod == null) {return null;}String repositoryLookupPath = new BaseUri(configuration.getBaseUri()).getRepositoryLookupPath(lookupPath);// Repository root resourceif (!StringUtils.hasText(repositoryLookupPath)) {return handlerMethod;}return mappings.exportsTopLevelResourceFor(getRepositoryBasePath(repositoryLookupPath)) ? handlerMethod : null;}注入PersistentEntityResourceAssembler来装配符合HATEOAS的响应JSON对象
API version的实现H3
通过上面Spring对HAL的实现中提到的,如果我们想通过请求application/vnd.xxx.vxx+hal+json来实现版本的话,jacksonHttpMessageConverter是不会起作用的,因为它只会严格匹配application/json和application/hal+json。因此那么我们需要扩展匹配的Media Type来实现。
bash
@Bean@DependsOn("halMessageConverterSupportedMediaTypeCustomizer")public HalVersionMessageConverterSupportedMediaTypesCustomizer registerCustomMediaType(@Qualifier("halJacksonHttpMessageConverter") TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter) {return new HalVersionMessageConverterSupportedMediaTypesCustomizer();}private class HalVersionMessageConverterSupportedMediaTypesCustomizer implements BeanFactoryAware {private static final String HAL_JACKSON_HTTP_MESSAGE_CONVERTER_BEAN_NAME = "halJacksonHttpMessageConverter";private volatile BeanFactory beanFactory;@PostConstructpublic void customizedSupportedMediaTypes() {TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter = beanFactory.getBean(HAL_JACKSON_HTTP_MESSAGE_CONVERTER_BEAN_NAME, TypeConstrainedMappingJackson2HttpMessageConverter.class);List<MediaType> supportedMediaTypes = new ArrayList<>(halJacksonHttpMessageConverter.getSupportedMediaTypes());supportedMediaTypes.add(new MediaType("application", "*+hal+json"));halJacksonHttpMessageConverter.setSupportedMediaTypes(supportedMediaTypes);}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = beanFactory;}}
必须在HalMessageConverterSupportedMediaTypesCustomizer之后执行,否则会被其覆盖supportedMediaTypes
没有定义Repository的实体的关联的问题H3
如果实体A包含一个到实体B的关联,查询实体A时,返回的结果会根据实体B的Repository是否存在会有不同。
- 存在B的Repository 那么A到B关联会被处理成link,而返回的A的对象中并不会直接包含B的对象
- 不存在B的Repository 那么由于不存在B的资源的链接,自然不会生成link,并且A对象中会直接包含B对象。 如果在查询A对象时B关联是lazy的,那么这里就会产生额外的查询。所以需要注意,如果包含的关联中是实体不存在Repository时,查询时最好就把这些关联对象fetch出来,否则会对性能产生一定的影响。
- 另外不能定义class级别的@RequestMapping,否则路径会被注册两遍。因为标准的
RequestMappingHandlerMapping是这样判断是否要注册映射的bash
@Overrideprotected boolean isHandler(Class<?> beanType) {return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||(AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));}
Excerpt不起作用H3
官方文档中提到,Excerpt只针对单个资源的请求有效,如果资源是集合,那么Excerpt是不会生效的
Excerpt projections are NOT applied to single resources automatically. They have to be applied deliberately. Excerpt projections are meant to provide a default preview of collection data, but not when fetching individual resources.
这是框架中的RepositoryEntityController处理的逻辑。PersistentEntityResourceAssembler有toFullResource和toResource方法,前者会忽略Excerpt。RepositoryEntityController针对集合的情况会调用toFullResource,因而Excerpt是不起作用的。
定义了Excerpt的实体的关联对象会产生额外的查询的问题H3
如果实体A包含一个到实体B的关联,实体B的Repository定义了Excerpt的话,那么虽然最终的返回的对象A中并不包含对象B,只是包含了一个link。但是对象的B的值仍然会被获取。在JPA中懒加载的情况下,这个应该不被加载的关联,就会被触发fetch,而产生额外的查询,对性能产生影响。
下面是PersistentEntityResourceAssember#doWithAssociation中相关的代码
bash
@Overridepublic void doWithAssociation(Association<? extends PersistentProperty<?>> association) {PersistentProperty<?> property = association.getInverse();if (!associationLinks.isLinkableAssociation(property)) {return;}if (!projector.hasExcerptProjection(property.getActualType())) {return;}Object value = accessor.getProperty(association.getInverse());if (value == null) {return;}……}
如果实体会被关联的话,所以不要轻易定义Excerpt。
如何指定多个ProjectionH3
如果返回的一个列中包含A和B两个实体,针对这两个实体都想指定Projection怎么办。虽然URL中只能指定一个projection参数,但是projection是可以重名的,只要他们对应的types即projection针对的实体不一样即可。
@BasePathAwareController注解的Controller中懒加载出错H3
RepositoryRestHandlerMapping中通过JpaHelper在interceptor中添加了OpenEntityManagerInViewInterceptor,但是BasePathAwareHandlerMapping并没有这个拦截器。
通过
bash
@Beanpublic MappedInterceptor basePathAwareOpenEntityManagerInViewInterceptor(EntityManagerFactory factory) {OpenEntityManagerInViewInterceptor omivi = new OpenEntityManagerInViewInterceptor();omivi.setEntityManagerFactory(factory);return new MappedInterceptor(new String[]{"/api/**"}, omivi);}
即可在所有的HandlerMapping中添加拦截器,可能和RepositoryRestHandlerMapping已经存在的会有点重复,但是并没有什么副作用。
评论
新的评论
上一篇
Spring AOP的实现
概念 Aspect 切面,指的是切分多个类的模块化的关注点,包括Pointcut或Advice Join point 程序的执行点, 可用于插入代码 (在Spring里面指的是方法的执行) Advice 对特定的Join point执行的动作 Pointcut 匹配Join p…
下一篇
使用coveralls统计测试覆盖率
coveralls 可以持续跟踪代码的测试覆盖率。 为了让它能够跟踪到测试覆盖率,需要测试的时候生成覆盖率数据,并提供给coveralls service。 生成测试覆盖率数据 jacoco和cobertura都可以用来统计代码的测试覆盖率,并且各自都有gradle和maven…
