在本文中,我们将探讨 Spring 应用程序背后的基本架构原则。我们将展现clean干净的架构(软件架构和设计工匠指南)。
让我们考虑支持 Blog 的后端系统的构建块。
首先,如果您考虑应用程序的“级别”,则数据在持久化之前经过了多少层:
- 中间的实体
- 我们围绕它们构建的用例(验证、拼写分析、审批系统)
- 接口适配器(Spring 控制器)
- 外部接口(Web)

后端系统的边界
从上面的界限,我们必须保留一些关于依赖规则的事实。
内圈不知道外圈。
当数据穿过圆时,它总是以对内圆最方便的形式存在。
外圈声明的数据格式不得用于内圈
清洁架构,罗伯特 C 马丁
插件架构
从外圈到内圈:
- 首先,我们想要构建一个 API,能够公开API 规范会很好。因此,我们将插入springfox swagger2 库。
- 其次,在数据进入我们的系统之前验证数据显然很好。为此,Spring 带有自己的库,构建在hibernate 验证器之上。因为验证器与传输对象无缝集成,这使得它非常方便。
- 此外,编写传输对象可能很乏味,我们也会让它们从一开始就保持不变,以后不用担心。为方便起见,我们将使用Project Lombok。
- 此外,我们希望 Transfer Objects 的数据转换尽可能快,并且可能是一个我们可以尽可能少地交互的库。让我们记住,从一个圆圈移动到另一个圆圈的数据结构本身必须是简单的。多年来,我发现MapStruct是最好的解决方案。这个库的权衡是你必须自己编写多少代码与你可以拥有多少速度。我们涉及的反射越多,代码就越慢。 一个统计表示。
- 最后,我们希望为我们的数据库提供一个结构,对其进行版本控制,将其映射到我们的实体上,并对其进行控制。因此,有人可能会请使用Liquibase或Flyway.。我个人更喜欢Flyway。
- 与数据库的交互完全通过 Spring Data 项目来处理。
源代码在Github 中。
作为项目先决条件,您应该安装docker和docker-compose。注意项目中的 docker-compose.yaml文件。因此,我们已经为您准备了一个 pg-admin 实例,您可以通过运行来使用它:
docker-compose up -d
Swagger配置
由于 Spring-boot 及其自动配置,我们几乎没有配置。实际上,您的应用程序中唯一的配置是 Swagger 使用的 Docket Bean 的自定义字段。
@Configuration public class SwaggerConfig { private static final String SNAPSHOT = "1.0"; private static final String HTTP_WWW_APACHE_ORG_LICENSES_LICENSE_2_0_HTML = "Usage of this code is forbidden without consent"; private static final String APACHE_2_0 = "Apache 2.0";
@Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .useDefaultResponseMessages(false) .select() .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build() .apiInfo(apiEndPointsInfo()); }
private ApiInfo apiEndPointsInfo() { return new ApiInfoBuilder().title("Blog API") .description("Example of a blog API") .contact(new Contact("Catalin Patrut", "NO URL", "api@cpatrut.ro")) .license(APACHE_2_0) .licenseUrl(HTTP_WWW_APACHE_ORG_LICENSES_LICENSE_2_0_HTML) .version(SNAPSHOT) .build(); }
}
|
我们数据转换的单位是传输对象。毫无疑问,我们通常会用注解来装饰它们,以在它们上无缝启用 Hibernate Validator 功能并添加Swagger 文档。同样,我们使用lombok来减少样板代码。最后,它们都被简化为我们使用的框架的一组配置类。
@ApiModel(description = "Class representing a post on the blog") @Value @AllArgsConstructor public class PostTO {
@ApiModelProperty(value = UUID_MESSAGE) UUID id;
@ApiModelProperty(position = 1, required = true, example = "Hello title") @Size(min = 5, max = 255) @NotNull String title;
@ApiModelProperty(value = ONLY_FOR_GET_MESSAGE, position = 2) String author;
@ApiModelProperty(position = 3, required = true, example = "Hello hello") @Size(min = 10) @NotNull String content;
@ApiModelProperty(value = ONLY_FOR_GET_MESSAGE, position = 5) @Null String updateTime;
@Builder private static PostTO newPostTo(final UUID id, final String title, final String author, final String content, final String updateTime ) { return new PostTO(id, title, author, content, updateTime); } }
|
我们也用注释来装饰接口适配器。他们的主要目的应该是数据验证。因此,用例、业务逻辑都会写在服务层。
@RestController @RequestMapping(PostController.PATH) @Api(tags = "Posts") @Slf4j public class PostController {
final static String PATH = "posts";
private final PostService postService;
public PostController(final PostService postService) { this.postService = postService; }
@PostMapping public ResponseEntity<PostTO> save(@Valid @RequestBody final PostTO post) { log.debug("saving post: " + post); return new ResponseEntity<>(postService.save(post), HttpStatus.CREATED); }
@PutMapping public ResponseEntity<Void> update(@Valid @RequestBody final PostTO post) { log.debug("updating post: " + post); postService.update(post); return new ResponseEntity<>(HttpStatus.OK); }
@GetMapping public ResponseEntity<PostTO> getById(@NotNull @RequestParam("id") final UUID id) { return new ResponseEntity<>(postService.getById(id), HttpStatus.OK); }
@GetMapping("/all") public ResponseEntity<List<PostTO>> getAll(@NotNull @RequestParam("page") final int page, @NotNull @RequestParam("size") final int size) { return new ResponseEntity<>(postService.findAll(PageRequest.of(page, size)), HttpStatus.OK); }
@DeleteMapping public ResponseEntity<Void> delete(@NotNull @RequestParam("id") final UUID id) { postService.delete(id); return new ResponseEntity<>(HttpStatus.OK); } }
|
我们的服务将保存、更新、获取或删除数据库中的数据。因此,他们操纵 Map Struct 映射器,有时会在数据不合适时执行在应用程序级别强加特定行为的验证。
@Service @Slf4j public class PostServiceImpl implements PostService { private final PostRepository postRepository; private final PostMapper postMapper;
@Autowired public PostServiceImpl(final PostRepository postRepository, final PostMapper postMapper) { this.postRepository = postRepository; this.postMapper = postMapper; }
@Override @Transactional public PostTO save(final PostTO post) { final PostEntity entity = postMapper.toEntity(post); log.debug(post.toString()); entity.setId(UUID.randomUUID()); final PostEntity savedEntity = postRepository.save(entity); return postMapper.toTransferObject(savedEntity); }
@Override public PostTO getById(final UUID id) { final PostEntity entity = postRepository.getById(id) .orElseThrow(EntityNotFoundException::new); return postMapper.toTransferObject(entity); }
@Override public List<PostTO> findAll(final Pageable pageable) { return postRepository.findAll(pageable).get() .map(postMapper::toTransferObject) .collect(Collectors.toList()); }
@Override public void update(final PostTO post) { postRepository.updatePost(post.getTitle(), post.getAuthor(), post.getContent(), ZonedDateTime.now(), post.getId()); }
@Override public void delete(final UUID id) { postRepository.deleteById(id); } }
|
映射器也是应用程序的重要组成部分。它们代表了太多我们无法自己编写的锅炉位置,因此我们使用 MapStruct。看看图书馆保护我们的样板数量。
@Mapper public interface PostMapper { @Mapping(target = "creationTime", ignore = true) PostEntity toEntity(final PostTO post);
PostTO toTransferObject(final PostEntity post); } @Generated( value = "org.mapstruct.ap.MappingProcessor", date = "2021-08-08T13:28:41+0200", comments = "version: 1.4.2.Final, compiler: javac, environment: Java 16.0.2 (Oracle Corporation)" ) @Component public class PostMapperImpl implements PostMapper {
@Override public PostEntity toEntity(PostTO post) { if ( post == null ) { return null; }
PostEntity postEntity = new PostEntity();
postEntity.setId( post.getId() ); postEntity.setTitle( post.getTitle() ); postEntity.setAuthor( post.getAuthor() ); postEntity.setContent( post.getContent() ); if ( post.getUpdateTime() != null ) { postEntity.setUpdateTime( ZonedDateTime.parse( post.getUpdateTime() ) ); }
return postEntity; }
@Override public PostTO toTransferObject(PostEntity post) { if ( post == null ) { return null; }
PostTOBuilder postTO = PostTO.builder();
postTO.id( post.getId() ); postTO.title( post.getTitle() ); postTO.author( post.getAuthor() ); postTO.content( post.getContent() ); if ( post.getUpdateTime() != null ) { postTO.updateTime( DateTimeFormatter.ISO_DATE_TIME.format( post.getUpdateTime() ) ); }
return postTO.build(); } }
|
数据访问层是负责与数据库交互的应用程序的边界。
@NoRepositoryBean public interface BlogRepository<T, ID extends Serializable> extends Repository<T, ID> { Optional<T> getById(ID id);
<S extends T> S save(S entity); } public interface PostRepository extends BlogRepository<PostEntity, UUID> {
@Transactional Page<PostEntity> findAll(Pageable pageable);
@Transactional @Modifying @Query("update PostEntity p set p.title = ?1, p.content = ?2, p.author = ?3" + ",p.updateTime =?4 where p.id = ?5") int updatePost(final String title, final String author, final String content, final ZonedDateTime updateTime, final UUID postId);
@Modifying @Transactional @Query("delete from PostEntity p where p.id=?1") int deleteById(final UUID id); }
|
最后,为了确保数据完整性,我们的 JPA 实体也添加了一些元数据。毫无疑问,我们越少使用我们的数据库进行此类操作越好。
@Entity @Table(schema = "blog", name = "posts") @Getter @Setter public class PostEntity { @Id @Column(name = "id", nullable = false) private UUID id;
@Column(name = "title", nullable = false) private String title;
@Column(name = "author", nullable = false) private String author;
@Column(name = "content", nullable = false) private String content;
@Column(name = "update_time") @UpdateTimestamp private ZonedDateTime updateTime;
@Column(name = "creation_time", nullable = false) @CreationTimestamp private ZonedDateTime creationTime; }
|
值得一提的是,我们正在通过使用 Flyway 对启动时的数据库结构进行建模。
create schema if not exists blog;
create table if not exists blog.posts ( id uuid primary key, title varchar(255) not null, author varchar(255) not null, content text not null, creation_time timestamp not null, update_time timestamp );
create table if not exists blog.comments ( id uuid primary key, title varchar(255) not null, content text not null, approved boolean default false, creation_time timestamp not null, update_time timestamp, post_id uuid not null references blog.posts );
|