怎么解决Maven和Spring配置重复的问题?

软件开发中的“重复”问题分为上下篇,这里是下篇:

上篇:软件开发中的“重复”问题,你真的了解吗?

你好,我是雷威。

我们都知道软件工程中重复的危害,它会让我们的代码难以维护,经常出现漏改和改错的问题。在上一期视频中,我和你聊了软件工程中的两种重复:代码重复和注释重复。而在本期视频中,我再来聊聊另外两种重复:Maven 重复和 Spring 配置重复。因为我发现很多的项目中,Maven 依赖管理很混乱,多个地方指定版本,版本不一致。还有种情况是,如果使用了第三方框架,每个项目都需要重复配置 Spring,包括 Bean 和 Properties 配置。接下来我就和你聊聊,如何解决这两类重复。

Maven 配置中的重复

首先,我们来聊聊 Maven 配置中的重复问题。很多项目会使用 Maven 管理大量的第三方依赖,涉及到依赖的版本、传递依赖的排除。如果是一个多模块项目,需要在每个模块的 POM 文件中定义这些依赖,会有大量重复的配置。

你应该已经知道了,Maven 提供了 Dependency Management 来统一管理依赖。一般是在项目的根 POM 中的 Dependency Management 中定义所有依赖,包括 Version 和 Exclusion。然后将其它模块的 Parent 指向根 POM。这样就引入了父 POM 定义的依赖,不用再指定版本和 Exclusion 了,只需要声明使用哪个依赖就可以了。准确说就是依赖的 Group ID 和 Artifact ID。

Dependency Management 的确方便了项目中的依赖管理,但它有一个限制,那就是必须将 Parent 指定为声明了 Dependency Management 的模块。我们知道,Maven 中模块的继承和 Java 类似,是单继承,即一个模块只能有一个 Parent。某些场景中,我们可能会为了使用某个框架,而不得不将项目的 Parent 指定为框架提供的一个 Parent。那么这时候,由于单继承的限制,就不能将模块的 Parent 同时设置为项目内部声明了 Dependency Management 的模块了。如果你使用过 Spring Boot,可能已经想到了,Spring Boot 可能会要求使用它提供的 Parent。

那么可以同时使用 Spring Boot 和项目内部的 Dependency Management 吗?答案是可以的。在面向对象设计中,有一个指导原则是:尽量使用组合,而不是继承。这个原则在今天的场景中也是适用的。Maven 提供了 BOM(Bill Of Material)和 Import 机制。允许以组合的方式来使用 Dependency Management,而不用修改模块的 Parent。

我们通过指定 Maven 依赖的 type 为 pom,scope 为 import,就可以将依赖中声明的 Dependency Management 导入到当前模块,供当前模块使用。

<parent>
<artifactId>example-project</artifactId>
<groupId>com.example</groupId>
<version>1.0.0</version>
</parent>

<dependencyManagement>
<dependencies>
<!-- Import dependency management from Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

这里再延伸一下,如果你的团队在使用 Spring Boot,而你正在编写一个多项目使用的公共组件或框架,就需要注意了。我们知道,Spring Boot 已经定义了一整套依赖,那么业务代码中可能会引入三套依赖:第一是项目本身引入的,第二是你编写的公共框架引入的,第三是 Spring Boot 引入的。为了保证这三套依赖是兼容的,你需要在多个地方维护依赖的信息,比如在项目代码和框架代码中指定相同的依赖版本和 Exclusion。如果你遇到了这个问题,应该使用 Maven 的 BOM 和 Import 机制来统一管理依赖。你可以在框架项目中通过 Import 方式引入 Spring Boot 的依赖,并定义项目使用的所有其他依赖。这样业务代码就只需要引入框架的 BOM 文件,所有的依赖都不同再重复指定版本号了,可以实现业务项目中,所有的 POM 文件只需指定一个版本号。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-boot-starters</artifactId>
<groupId>cn.inhope.xiaoan</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>starter-bom</artifactId>
<version>1.0.4</version>
<packaging>pom</packaging>
<properties>
<guava.version>20.0</guava.version>
<commons-httpclient.version>3.1</commons-httpclient.version>
<apache-common.version>2.6.0</apache-common.version>
<spring-boot.version>2.1.0.RELEASE</spring-boot.version>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<commons.text.version>1.8</commons.text.version>
<commons-collections.version>3.2.2</commons-collections.version>
<commons-lang.version>2.6</commons-lang.version>
</properties>
<!-- 这里统一管理在 spring-boot 和 spring-cloud 中没有提供的依赖,包括版本以及传递依赖的 exclusion-->
<!-- 所有的 starter 中都不要再指定版本了,这里统一管理各个组件的版本 -->
<dependencyManagement>
<dependencies>
<!-- 这里是 Spring Boot 中没有提供的依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>

<!-- 导入 Spring Boot 的依赖管理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 导入 Spring Cloud 的依赖管理 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

业务项目中引入框架的 BOM 文件,无需重复指定每个依赖的版本和 Exclusion。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.inhope.xiaoan</groupId>
<artifactId>xiaoan-gxf</artifactId>
<version>0.0.1-SNAPSHOT</version>

<packaging>pom</packaging>
<name>xiaoan-gxf</name>
<description> 股行分 </description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>cn.inhope.xiaoan</groupId>
<artifactId>inhope-bom-starter</artifactId>
<version>1.0.18</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

Spring 配置重复

接下来聊聊 Spring 配置的重复问题。Spring 框架已经成为了 Java 语言事实上的工业标准了,几乎所有的业务项目都会使用 Spring。Spring 的基础是 IoC 和 AOP,我们在应用中配置 Spring Bean,Spring 框架会帮我们进行管理。在使用 Spring 的过程中,我发现很多团队的多个项目中都会有大量重复的 Spring Bean 配置,尤其是在使用微服务架构时。我举一个真实场景,你可能使用过 XXL-JOB。它是一款优秀的分布式任务管理平台。在使用 XXL-JOB 时,需要运行任务执行器,任务执行器通常是一个 Spring Boot 应用。我们需要在项目中配置一个 Spring Bean,它的类型是 XxlJobSpringExecutor。你可以参考 XXL-JOB 的官网来查看具体的配置。

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}

所有需要使用 XXL-JOB 的项目都需要定义这个 Bean。这就是一种重复。定义 Bean 的代码就是一个被 @Bean 修饰的方法,这个方法声明了一个 Spring Bean。

这种场景和一些简单的方法重复,看起来好像是一样的。可以将这个方法抽取到一个公共类库中,然后每个项目引用这个类库,调用这个方法来消除重复吗?这是不可以的,因为这个方法不是一个简单的方法,它是被 Spring 框架执行的。

要避免这种重复,可以使用自定义 。我们编写一个 Spring Boot Starter,将该方法抽取到 Starter 中,并配置 Starter 启动时自动配置 Bean。这样每个项目就只需要引入一个 Starter 依赖。如果执行器需要指定不同的参数,Starter 也是支持的。可以在项目的 application.properties 中配置参数。

编写一个 Starter 分两大步。首先,编写一个 Spring Configuration 类来声明 XXL-JOB 需要的 Bean:

com.example.starter.xxljob.XxlJobAutoConfiguration

public class XxlJobAutoConfiguration {
private final static Integer PORT = 8765;
private final static String LOG_PATH = "/tmp/xxl-job/jobhandler/";
/**
* xxl-job-admin 地址,注册地址
*/
@Value("${xxl.job.admin.addresses}")
private String adminAddress;
/**
* xxl-job 的执行器名字,根据该名字自动注册到对应的执行器中,需要先在页面创建执行器
*/
@Value("${xxl.job.executor.appname}")
private String appName;
@Bean
@ConditionalOnProperty(name = "xxl.job.enable", havingValue = "true")
public XxlJobExecutor xxlJobExecutor() {
LOGGER.info("xxl-job config init.");
XxlJobSpringExecutor xxlJobExecutor = new XxlJobSpringExecutor();
// ip 为空会自动获取本机 ip 进行注册
xxlJobExecutor.setIp("");
xxlJobExecutor.setPort(PORT);
xxlJobExecutor.setAppName(appName);
xxlJobExecutor.setAdminAddresses(adminAddress);
xxlJobExecutor.setLogPath(LOG_PATH);
xxlJobExecutor.setAccessToken("");
// xxl-job 日志保留 7 天
xxlJobExecutor.setLogRetentionDays(7);
LOGGER.info("xxl-job config init end.");
return xxlJobExecutor;
}
}

然后,在 src/main/resources/spring.factories 文件中添加自动化配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.starter.xxljob.XxlJobAutoConfiguration

这样,我们就编写好了一个 XXL-JOB 的 Starter。它会自动配置好 Spring Bean。所有需要使用 XXL-JOB 的应用,只需要添加这个 Starter 依赖就可以了。不需要再重复定义 Spring Bean 了。

总结 + 延伸思考

今天的分享到这里就结束了,对于今天这节课所讲的内容呢,我画了一张思维导图,供你参考。

img

最后有一个小思考,是不是所有的重复都是不好的,都应该被消除呢?

我们思考这样一个问题,在微服务架构中,多个服务之间需要引用公共的类库吗?

微服务的领域模型需要提取到一个公共的类库中,然后每个微服务进行引用吗?

你会发现,当在微服务中使用公共类库时,尤其是通过共享类库共享领域模型时,微服务间的耦合会大大增强。服务化最根本的初衷是为了服务独立、自治。结果因为共享库导致服务间耦合大大增强,得不偿失。所以,重复肯定是不好的,但是为了消除重复,可能会导致其他的问题,这是一个权衡的过程。我的经验是,应用内部不应该出现重复,但是多个应用之间允许一些重复,以实现高内聚、低耦合。

参考文章:整理于极客时间每日一课
https://time.geekbang.org/dailylesson/detail/100056856?tid=148

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值