Spring应用程序中的清洁Clean架构源码


在本文中,我们将探讨 Spring 应用程序背后的基本架构原则。我们将展现Clean干净的架构(软件架构和设计工匠指南)。
让我们考虑支持 Blog 的后端系统的构建块。
首先,如果您考虑应用程序的“级别”,则数据在持久化之前经过了多少层:

  • 中间的实体
  • 我们围绕它们构建的用例(验证、拼写分析、审批系统)
  • 接口适配器(Spring 控制器)
  • 外部接口(Web)


后端系统的边界
从上面的界限,我们必须保留一些关于依赖规则的事实。
内圈不知道外圈。
当数据穿过圆时,它总是以对内圆最方便的形式存在。
外圈声明的数据格式不得用于内圈
清洁架构,罗伯特 C 马丁

 
插件架构
从外圈到内圈:
  • 首先,我们想要构建一个 API,能够公开API 规范会很好。因此,我们将插入springfox swagger2 库
  • 其次,在数据进入我们的系统之前验证数据显然很好。为此,Spring 带有自己的库,构建在hibernate 验证器之上。因为验证器与传输对象无缝集成,这使得它非常方便。
  • 此外,编写传输对象可能很乏味,我们也会让它们从一开始就保持不变,以后不用担心。为方便起见,我们将使用Project Lombok
  • 此外,我们希望 Transfer Objects 的数据转换尽可能快,并且可能是一个我们可以尽可能少地交互的库。让我们记住,从一个圆圈移动到另一个圆圈的数据结构本身必须是简单的。多年来,我发现MapStruct是最好的解决方案。这个库的权衡是你必须自己编写多少代码与你可以拥有多少速度。我们涉及的反射越多,代码就越慢。 一个统计表示
  • 最后,我们希望为我们的数据库提供一个结构,对其进行版本控制,将其映射到我们的实体上,并对其进行控制。因此,有人可能会请使用LiquibaseFlyway.。我个人更喜欢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
);