本文主要基于java的Spring Boot 框架,探索在SpringBoot下不同模块间互相引用导致的依赖循环问题。
一、三层架构概述
对于三层架构,有几种不同的表示,但大致可以归类为以下三种。
-
表示层
Presentation Layer
-
视图
View
:提供UI界面与用户进行交互 -
控制器
Controller
:接收用户的请求并协调调度其他层的处理
-
-
业务逻辑层
Business Logic Layer
-
服务层
Service
:协调数据访问层,处理业务逻辑
-
-
数据访问层
Data Access Layer
-
数据访问对象
DAO
或者Repository
:负责与数据库进行交互
-
二、三层架构的循环依赖问题
这里的模块以数据表作为划分依据,例如:user表就为user模块,product表就为product模块
作者在原先的业务开发中,Controller层中几乎只充当一个路由的作用,然后直接调用Service
层的某一方法,做一个甩手掌柜。而DAO
层一般只提供与数据库的交互,所以会在服务层Service
封装很多方法(例如:封装了可以走缓存的方法,对于用户存不存在的判别等等)。这样就导致了所有的业务处理都积压在了Service
层上,现在有Aservice,Bservice,Amapper,Bmapper。如果A要调用B模块的相关内容,Aservice中就会使用Bservice作为依赖注入。然而,如果有一天Bservice又要使用A模块的内容,就得只能退而求其次,使用Amapper作为依赖注入。来避免一个循环依赖的问题。
作者深受其扰,在一个service中参杂着不同的mapper和不同的service,实在是太恶心了。于是,决定探索在SpringBoot的三层架构中优雅解决互相依赖的问题。
三、不同解耦方式的探索
我们先约定一个场景和相关的类。
场景:我们要为设计一个电商后台管理系统,来管理平台的店家,商品分类,商品等。下文也将基于此场景
类:
@Data public class Shop { //电商平台不同的店铺 private Integer shopId; private String shopCode; private String shopName; //.... 省略其他信息 } @Data @Accessors(chain = true) public class User { //电商用户类 private Integer userId; private String username; private String pwd; private String nickName; private Integer shopId; //每个电商用户隶属于不同的店铺 //...省略其他参数 } @Data public class ProductCategory { //商品分类 private Integer productCatId; private Integer shopId; //每个店铺有自己的不同的商品分类 private String productCatName; //...省略其他信息 } @Data public class Product { private Integer productId; private Integer productCatId; //每个商品隶属于不同的商品分类 private String productName; private BigDecimal price; //...省略其他信息 }
1、使用控制器协调服务层
我们不再只让Controller只发挥它的路由,让它参与数据的过滤,处理,协调各个service。service只调用mapper,mapper充当最底层的功能。
这里给出新增用户的场景示例。
public interface UserMapper extends BaseMapper<User> { } public interface ShopMapper extends BaseMapper<Shop> { } @Service @RequiredArgsConstructor public class ShopServiceImpl implements ShopService { private final ShopMapper shopMapper; @Override public boolean checkExist(Integer shopId) { return shopMapper.selectById(shopId) != null; } @Override @Cacheable(value = "shopName", key = "#shopId") public String getShopName(Integer shopId) { //..........其他逻辑,这里简单使用缓存注解代替 return shopMapper.selectById(shopId).getShopName(); } } @Service @RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; @Override public void addUser(User user) { userMapper.insert(user); } @Override @Cacheable(value = "user", key = "#id") public User getUser(Integer id) { //省略其他逻辑 return userMapper.selectById(id); } }
我们定义如上代码的相关mapper和service,各个service专注于自己的事,只使用mapper当作底层架构。
然后,我们的Controller也同理,将service当作底层架构,只调用不同service提供的服务。
@RestController @RequestMapping("/user") @RequiredArgsConstructor public class UserController { private final UserService userService; private final ShopService shopService; @PostMapping public R<Void> addUser(@RequestBody User user) { //为了举例方便,直接使用DO接收 if (!shopService.checkExist(user.getShopId())) { //.........其他逻辑 throw new RuntimeException("店铺不存在"); } userService.addUser(user); return R.ok(); } @GetMapping("/{userId}") public R<UserVo> getUser(@PathVariable Integer userId) { User user = userService.getUser(userId); if (user == null) { //...........其他逻辑 return R.fail(); } UserVo userVo = new UserVo(user); //调用 shopService中添加了缓存的get方法进行渲染 userVo.setShopName(shopService.getShopName(user.getShopId())); return R.ok(userVo); } } @Data public class UserVo extends User { private String shopName; public UserVo(User user) { this.setUserId(user.getUserId()); this.setUsername(user.getUsername()); this.setPwd(user.getPwd()); this.setNickName(user.getNickName()); this.setShopId(user.getShopId()); } }
这样,我们就实现了一个优雅的解耦,让service层变得不再臃肿,提高了代码的复用率。
不过,我们一定要确保上层架构做足了数据处理。
2、在服务层抽象出公共服务层
如果,我们不希望Controller做如此多的业务内容。可以尝试从service中抽象出公共service和业务service,业务service不再调用mapper,而只调用公共service,使用公共service提供的方法。然后controller在协调调用业务service。
我们采用如上图的架构,将原先ProductService细分为ProductCommonService和ProductService(NEW)
,
将ProductCatService细分为ProductCatCommonService提供基础服务。
然后使用ProductService(NEW)
联调 ProductCommonService 和 ProductCatCommonService。
在service层进行了更细致的划分,具体的类关系如下
@Service @RequiredArgsConstructor public class ProductCatCommonServiceImpl implements ProductCatCommonService { private final ProductCategoryMapper productCategoryMapper; @Override public boolean checkExist(Integer productCatId) { return productCategoryMapper.selectById(productCatId) != null; } } @Service @RequiredArgsConstructor public class ProductCommonServiceImpl implements ProductCommonService { private final ProductMapper productMapper; @Override public void save(Product product) { productMapper.insert(product); //.....其他逻辑 } } @Service @RequiredArgsConstructor public class ProductServiceImpl implements ProductService { private final ProductCommonService productCommonService; private final ProductCatCommonService productCatCommonService; @Override public void addProduct(Product product) { if (!productCatCommonService.checkExist(product.getProductCatId())) { //....其他逻辑 throw new RuntimeException("商品分类不存在"); } //....其他校验逻辑 productCommonService.save(product); } }
不过优劣也很明显:减轻了Controller的工作,细化了代码职责,强化了底层的构筑。但是导致了类的膨胀,抽出公共service的做法相当于又多了一个协调层来代替原先Controller应该做的工作,加大了代码量。
3、使用注解进行数据渲染
一般情况下,我们不能直接将数据库的实体类对象直接返回给前端。我们将其封装为VO类,而VO类一般需要对数据的关联项进行渲染。
举个例子,我们的Product中具有 product_cat_id 这一属性,我们需要将其渲染为具体的 cat_name,然后我们还可能需要拿到 product的所属店铺。
也就是封装成如下的类。
@Data public class ProductVo { private Integer productId; private String productName; private Integer productCatId; private String productCatName; private Integer shopId; private String shopName; private BigDecimal price; } @Data public class Product { private Integer productId; private Integer productCatId; private String productName; private BigDecimal price; //省略其他信息 }
一般写法下,我们将在一个方法中,显示的协调调用各个service或者mapper中提供好的服务,来查询出相关信息进行一个封装。
一个提供基础服务的方法可能被多处进行使用,为了解决这种重复的劳动,作者开发了一款使用简单功能但功能强大的框架,做个使用实例。
相关文档链接:
目前我们的getDetailById方法没有做任何的动作,而调用相关接口的返回对象也没有对非Product的属性做任何的填充。
@Override public ProductVo getDetailById(Integer id) { Product product = productCommonService.getById(id); return new ProductVo(product); }
在以往的代码开发中,我们需要引入ProductCatService和ShopService,才能对ProductVo的catName,shopId,shopName进行渲染。
我们在其他服务中已经写好了相关方法。
@Override public ProductCategory getById(Integer id) { return productCategoryMapper.selectById(id); } @Override @Cacheable(value = "shopName", key = "#shopId") public String getShopName(Integer shopId) { //..........其他逻辑,这里简单使用缓存注解代替 return shopMapper.selectById(shopId).getShopName(); }
我们现在要做的就是,让他们成为翻译器,然后在想要执行翻译的字段上使用该翻译器,最后挑选一个喜欢的地方执行翻译即可。
说的复杂,但是3个注解就足以实现。
@Override @Cacheable(value = "productCatName", key = "#id") @Translator("id_to_product_category") public ProductCategory getById(Integer id) { return productCategoryMapper.selectById(id); } @Override @Cacheable(value = "shopName", key = "#shopId") @Translator("id_to_shop_name") public String getShopName(Integer shopId) { //..........其他逻辑,这里简单使用缓存注解代替 return shopMapper.selectById(shopId).getShopName(); }
在这两个非常常用的方法,添加 @Translator
注解,将其注册为翻译器。
然后,在我们的Vo类上,使用@Mapping
注解对productVo的相关字段进行翻译。
@Data public class ProductVo { private Integer productId; private String productName; private Integer productCatId; @Mapping(translator = "id_to_product_category",mapper = "productCatId", receive = "productCatName",sort = 0) private String productCatName; @Mapping(translator = "id_to_product_category",mapper = "productCatId", receive = "shopId",sort = 0) private Integer shopId; @Mapping(translator = "id_to_shop_name",mapper = "shopId", sort = 1) private String shopName; private BigDecimal price; public ProductVo(Product product) { this.productId = product.getProductId(); this.productName = product.getProductName(); this.productCatId = product.getProductCatId(); this.price = product.getPrice(); } }
@Mapping中的mapper属性是为数组,可以获取该对象的其他属性然后传递给翻译器,receive属性是接收翻译器翻译结果的某一属性。
可以注意到,shopId和productCatName调用了同一翻译器,且mapper指定一致,说明我们可以只调用一次方法,就可以获取这两个属性。而翻译器确实也只会执行一次在这种情况下,所以开发者无需担心带来的IO问题。
我们可以使用sort控制翻译器的执行顺序,来做一个简单的编排。 当然,还有许多更加强大的功能,如异步翻译,回调翻译,条件补充翻译等作者不一一举例。
下一步,启用翻译
@Override @TranslationExecute public ProductVo getDetailById(Integer id) { Product product = productCommonService.getById(id); return new ProductVo(product); }
增强我们的方法,让翻译器执行
当然,我们也可以在Controller层进行处理,起到的作用是一样的。
@GetMapping("/{id}") @TranslationExecute(field = "data") public R<ProductVo> getProduct(@PathVariable Integer id){ return R.ok(productService.getDetailById(id)); }
各位开发者要是觉得还可以,希望大家可以去gitee或github上给个star,支持一下作者。
结语
以上就是作者对三层架构下,依赖混乱的处理方法和思考,这三中策略可以互相依存。希望对大家能有所帮助。