最近,同事的
文章在Habré上闪过,他描述了一种将Generics和Spring功能结合起来的相当有趣的方法。 她使我想起了我用来编写微服务的一种方法,因此我决定与读者分享。

在输出中,我们获得了一个传输系统,在其中添加了一个新实体,我们将需要限制我们自己初始化存储库-服务-控制器束的每个元素中的一个bean。
立即
资源 。
分支,如我所不愿:
standart_version 。
本文中描述的方法位于
abstract_version分支中。
我通过
Spring Initializr组合了一个项目,添加了JPA,Web和H2框架。 Gradle,Spring Boot 2.0.5。 这样就足够了。

首先,请考虑从控制器到存储库的经典版本传输,反之亦然,而无需任何其他逻辑。 如果您想了解该方法的本质,请向下滚动至抽象版本。 但是,尽管如此,我还是建议您阅读全文。
经典版本。
该示例的
资源为它们提供了几个实体和方法,但是在本文中,我们仅拥有一个User实体和一个save()方法,我们将它们从存储库中通过服务拖动到控制器。 在资源中,有7种,但是在一般情况下,Spring CRUD / JPA Repository允许您使用大约十二种保存/接收/删除方法,此外还可以使用
某些通用方法 。 同样,我们也不会因验证,映射dto等必要事情而分心。 您可以自己添加它,也可以
在Habr的其他文章中学习。
域:
@Entity public class User implements Serializable { private Long id; private String name; private String phone; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Column(nullable = false) public String getName() { return name; } public void setName(String name) { this.name = name; } @Column public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; }
仓库:
@Repository public interface UserRepository extends CrudRepository<User, Long> { }
服务:
public interface UserService { Optional<User> save(User user); }
服务(实施):
@Service public class UserServiceImpl implements UserService { private final UserRepository userRepository; @Autowired public UserServiceImpl(UserRepository userRepository) { this.userRepository = userRepository; } @Override public Optional<User> save(User user) { return Optional.of(userRepository.save(user)); } }
控制器:
@RestController @RequestMapping("/user") public class UserController { private final UserService service; @Autowired public UserController(UserService service) { this.service = service; } @PostMapping public ResponseEntity<User> save(@RequestBody User user) { return service.save(user).map(u -> new ResponseEntity<>(u, HttpStatus.OK)) .orElseThrow(() -> new UserException( String.format(ErrorType.USER_NOT_SAVED.getDescription(), user.toString()) )); } }
我们获得了一组相关的类,这些类将帮助我们在CRUD级别上对User实体进行操作。 在我们的示例中,这是一种方法,资源更多。 在
standart_version分支中提供了完全不是抽象的书写层版本。
假设我们需要添加另一个实体,例如Car。 我们不会在实体级别彼此赚钱(如果需要,您可以映射它)。
首先,创建一个实体。
@Entity public class Car implements Serializable { private Long id; private String brand; private String model; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; }
然后创建一个存储库。
public interface CarRepository extends CrudRepository<Car, Long> { }
然后服务...
public interface CarService { Optional<Car> save(Car car); List<Car> saveAll(List<Car> cars); Optional<Car> update(Car car); Optional<Car> get(Long id); List<Car> getAll(); Boolean deleteById(Long id); Boolean deleteAll(); }
然后执行服务.......控制器.........
是的,您可以简单地从User类复制并粘贴相同的方法(此处通用),然后将User更改为Car,然后对实现,控制器进行相同的操作,然后再进入下一个实体,在那里它们已经越来越多了...通常,您会厌烦第二种情况,为数十个实体创建服务架构(复制粘贴,替换实体名称,在错误的地方,密封的地方...)会导致任何单调的工作所引起的折磨。 尝试在闲暇时给二十个实体开个处方,您就会理解我的意思。
因此,有一次,当我只对泛型和典型参数感兴趣时,我意识到可以使该过程变得不那么常规。
因此,基于典型参数的抽象。
这种方法的意思是将所有逻辑带到抽象中,将抽象绑定到接口的典型参数,并将其他容器注入容器中。 就是这样。 Bean中没有逻辑。 仅注射其他豆类。 这种方法涉及一次编写架构和逻辑,并且在添加新实体时不进行复制。
让我们从抽象的基石开始-抽象实体。 正是从她那里,抽象依赖关系链将开始,它将作为服务的框架。
所有实体都有至少一个公共字段(通常是多个字段)。 这是ID。 我们将此字段带入一个单独的抽象实体,并从中继承User和Car。
AbstractEntity:
@MappedSuperclass public abstract class AbstractEntity implements Serializable { private Long id; @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } }
记住要用@MappedSuperclass批注标记抽象-Hibernate还应该知道这是一个抽象。
使用者:
@Entity public class User extends AbstractEntity { private String name; private String phone;
因此,与Car相同。
在每一层中,除了容器外,我们还将有一个具有典型参数的接口和一个具有逻辑的抽象类。 除了存储库-得益于Spring Data JPA的细节,这里的一切将更加简单。
我们在存储库中需要的第一件事是共享存储库。
CommonRepository:
@NoRepositoryBean public interface CommonRepository<E extends AbstractEntity> extends CrudRepository<E, Long> { }
在此存储库中,我们为整个链设置了通用规则:参与其中的所有实体都将从抽象中继承。 接下来,对于每个实体,我们必须编写我们自己的存储库接口,在其中指示该存储库-服务-控制器链将与哪个实体一起使用。
UserRepository:
@Repository public interface UserRepository extends CommonRepository<User> { }
得益于Spring Data JPA的功能,存储库设置到此结束-一切都将像这样工作。 接下来是服务。 我们需要创建一个公共接口,抽象和bean。
CommonService:
public interface CommonService<E extends AbstractEntity> { { Optional<E> save(E entity);
AbstractService:
public abstract class AbstractService<E extends AbstractEntity, R extends CommonRepository<E>> implements CommonService<E> { protected final R repository; @Autowired public AbstractService(R repository) { this.repository = repository; }
在这里,我们重新定义所有方法,并为将来的存储库创建一个参数化的构造函数,然后在Bean中重新定义。 因此,我们已经在使用尚未定义的存储库。 我们尚不知道在此抽象中将处理哪个实体以及我们将需要哪个存储库。
UserService:
@Service public class UserService extends AbstractService<User, UserRepository> { public UserService(UserRepository repository) { super(repository); } }
在容器中,我们做最后一件事-我们明确定义所需的存储库,然后在抽象构造函数中调用该存储库。 就是这样。
使用界面和抽象,我们创建了一条高速公路,通过该高速公路将驱动所有实体。 在垃圾箱中,我们将这些物品带到公路上,通过公路我们将显示我们所需的实体。
控制器基于相同的原理构建:接口,抽象,容器。
CommonController:
public interface CommonController<E extends AbstractEntity> { @PostMapping ResponseEntity<E> save(@RequestBody E entity);
AbstractController:
public abstract class AbstractController<E extends AbstractEntity, S extends CommonService<E>> implements CommonController<E> { private final S service; @Autowired protected AbstractController(S service) { this.service = service; } @Override public ResponseEntity<E> save(@RequestBody E entity) { return service.save(entity).map(ResponseEntity::ok) .orElseThrow(() -> new SampleException( String.format(ErrorType.ENTITY_NOT_SAVED.getDescription(), entity.toString()) )); }
UserController:
@RestController @RequestMapping("/user") public class UserController extends AbstractController<User, UserService> { public UserController(UserService service) { super(service); } }
这是整个结构。 它只写一次。
接下来是什么?
现在,让我们假设我们有一个已经从AbstractEntity继承的新实体,我们需要为其编写相同的链。 我们需要一分钟。 并且没有复制粘贴和更正。
采取已经继承自AbstractEntity Car的方式。
汽车资料库:
@Repository public interface CarRepository extends CommonRepository<Car> { }
汽车服务:
@Service public class CarService extends AbstractService<Car, CarRepository> { public CarService(CarRepository repository) { super(repository); } }
CarController:
@RestController @RequestMapping("/car") public class CarController extends AbstractController<Car, CarService> { public CarController(CarService service) { super(service); } }
如我们所见,复制相同的逻辑仅在于添加一个bean。 无需通过更改参数和签名来重写每个存储箱中的逻辑。 它们只编写一次,并在以后的每种情况下工作。
结论
当然,该示例描述了一种球形情况,其中每个实体的CRUD具有相同的逻辑。 它不会发生-您仍然必须在垃圾箱中重新定义一些方法或添加新方法。 但这将来自处理实体的特定需求。 好吧,如果CRUD方法总数的60%将保持抽象状态。 这将是一个很好的结果,因为我们手动生成冗余代码的次数越多,单调工作所花费的时间就越多,错误或错别字的风险也就越高。
希望本文对您有所帮助,感谢您的关注。
UPD
由于该建议,
aleksandy设法将Bean的初始化引入了构造函数,从而显着改进了该方法。 如果您看到其他可以改进示例的方法,请在评论中写下,然后您的建议将被提交。