面试官:Maven 的这 7 个问题你思考过没有?
你知道的越多,不知道的就越多,业余的像一棵小草!
你来,我们一起精进!你不来,我和你的竞争对手一起精进!
编辑:业余草
dwz.cn/PHYFFK27
推荐:https://www.xttblog.com/?p=5269
现在的项目中 Maven 随处可见,面试的时候,经常会被问一些项目中 Maven 的问题,但是平时 Maven 项目一般不会出什么问题,可能你不太注意,以下7个问题,一般说出来并掌握,至少可以证明你 Maven 用的熟练度还可以。
「Maven的仓库管理、依赖管理、继承和聚合等特性为项目的构建提供了一整套完善的解决方案,如果你搞不懂Maven,那么一个多模块的项目足以让你头疼,依赖冲突就会让你不知所措,甚至搞不清楚项目是如何运行起来的...」
OK,博主就曾经被 Maven“伤害”过,所以,我们要:「彻底搞定Maven!」
回想一下,当你新到一家公司,安装完 JDK 后就会安装配置 Maven,很大可能性你需要修改 settings.xml 文件,比如你会修改本地仓库地址路径,比如你很可能会 copy 一段配置到你的 settings.xml 中(很可能就是私服的一些配置)。接下来,你会到 IDEA 或者 Eclipse 中进行 Maven 插件配置,然后你就可以在工程中的 pom.xml 里面开始添加标签来管理 jar 包,在 Maven 规范的目录结构下进行编写代码,最后你会通过插件的方式来进行测试、打包(jar or war)、部署、运行。
上面描述了对 Maven 的一些使用方式,下面我们进行一些思考:
1、本地仓库?Maven 到底有哪些仓库?它们什么关系?
Maven仓库:
本地仓库路径配置:
你要 jar 包,不可能每次都要联网去下载吧,多费劲,所以本地仓库就是相当于加了一层 jar 包缓存,先到这里来查。如果这里查不到,那么就去私服上找,如果私服也找不到,那么去中央仓库去找,找到 jar 后,会把 jar 的信息同步到私服和本地仓库中。
「私服」,就是公司内部局域网的一台服务器而已,你想一下,当你的工程 Project-A 依赖别人的 Project-B 的接口,怎么做呢?没有 Maven 的时候,当然是 copy Project-B jar 到你的本地 lib 中引入,那么 Maven 的方式,很显然需要其他人把 Project-B deploy 到私服仓库中供你使用。「因此私服中存储了本公司的内部专用的 jar!不仅如此,私服还充当了中央仓库的镜像,说白了就是一个代理!」
「中央仓库」:该仓库存储了互联网上的 jar,由 Maven 团队来维护,地址是:http://repo1.maven.org/maven2/。
2、关于<dependency>
的使用
「依赖管理:」
其实这个标签揭示了 jar 的查找坐标:「groupId」、「artifactId」、「version」。
一般而言,我们可以到私服上输入 artifactId 进行搜索,或者到http://search.maven.org/、http://mvnrepository.com/上进行查找确定坐标。
「version分为开发版本(Snapshot)和发布版本(Release),那么为什么要分呢?」
在实际开发中,我们经常遇到这样的场景,比如A服务依赖于 B 服务,A 和 B 同时开发,B 在开发中发现了 BUG,修改后,将版本由 1.0 升级为 2.0,那么 A 必须也跟着在 POM.XML 中进行版本升级。过了几天后,B 又发现了问题,进行修改后升级版本发布,然后通知A进行升级...可以说这是开发过程中的版本不稳定导致了这样的问题。
「Maven,已经替我们想好了解决方案,就是使用 Snapshot 版本,在开发过程中 B 发布的版本标志为 Snapshot 版本,A 进行依赖的时候选择 Snapshot 版本,那么每次B发布的话,会在私服仓库中,形成带有时间戳的 Snapshot 版本,而 A 构建的时候会自动下载 B 最新时间戳的 Snapshot 版本!」
3、既然 Maven 进行了依赖管理,为什么还会出现依赖冲突?处理依赖冲突的手段是?
依赖的版本?
首先来说,「对于 Maven 而言,同一个 groupId 同一个 artifactId 下,只能使用一个 version」!
根据上图的依赖顺序,将使用 1.2 版本的 jar。
现在,我们可以思考下了,比如工程中需要引入 A、B,而 A 依赖 1.0 版本的 C,B 依赖 2.0 版本的 C,那么问题来了,C 使用的版本将由引入 A、B 的顺序而定?这显然不靠谱!如果A的依赖写在 B 的依赖后面,将意味着最后引入的是 1.0 版本的 C,很可能在运行阶段出现类(「ClassNotFoundException」)、方法(「NoSuchMethodError」)找不到的错误(因为B使用的是高版本的 C)!
「这里其实涉及到了 2 个概念:依赖传递(transitive)、Maven的最近依赖策略。」
「依赖传递:如果 A 依赖 B,B 依赖 C,那么引入 A,意味着 B 和 C 都会被引入。」
「Maven 的最近依赖策略:如果一个项目依赖相同的 groupId、artifactId 的多个版本,那么在依赖树(mvn dependency:tree)中离项目最近的那个版本将会被使用。(从这里可以看出 Maven 是不是有点小问题呢?能不能选择高版本的进行依赖么?据了解,Gradle 就是 version + 策略)」
现在,我们可以想想如何处理依赖冲突呢?
想法1:要使用哪个版本,我们是清楚的,那么能不能不管如何依赖传递,都可以进行版本锁定呢?
「使用<dependencyManagement>
这种主要用于子模块的版本一致性中」
想法2:在依赖传递中,能不能去掉我们不想依赖的?
「使用<exclusions>
在实际中我们可以在 IDEA 中直接利用插件帮助我们生成」
想法3:既然是最近依赖策略,那么我们就直接使用显式依赖指定版本,那不就是最靠近项目的么?
「使用<dependency>
」
4、引入依赖的最佳实践,提前发现问题!
在工程中,我们避免不了需要加一些依赖,也许加了依赖后运行时才发现存在依赖冲突在去解决,似乎有点晚!那么能不能提前发现问题呢?
「如果我们新加入一个依赖的话,那么先通过 mvn dependency:tree 命令形成依赖树,看看我们新加入的依赖,是否存在传递依赖,传递依赖中是否和依赖树中的版本存在冲突,如果存在多个版本冲突,利用上文的方式进行解决!」
5、Maven规范化目录结构
「这里需要注意2点:」
1、src/main 下内容最终会打包到 Jar/War 中,而 src/test 下是测试内容,并不会打包进去。
2、src/main/resources 中的资源文件会 COPY 至目标目录,这是 Maven 的默认生命周期中的一个规定动作。(想一想,hibernate/mybatis 的映射 XML 需要放入 resources 下,而不能在放在其他地方了)
6、Maven 的生命周期
「Maven生命周期:」
我们只需要注意一点:「执行后面的命令时,前面的命令自动得到执行。」
「实际上,我们最常用的就是这么几个:」
1、clean:有问题,多清理!2、package:打成 Jar or War 包,会自动进行 clean + compile 3、install:将本地工程 Jar 上传到本地仓库 4、deploy:上传到私服
7、关于 scope 依赖范围
既然,Maven 的生命周期存在编译、测试、运行这些过程,那么显然有些依赖只用于测试,比如「junit」;有些依赖编译用不到,只有运行的时候才能用到,比如** mysql 的驱动包「在编译期就用不到(「编译期用的是 JDBC 接口」),而是在运行时用到的;还有些依赖,编译期要用到,而运行期不需要提供,因为有些容器已经提供了,比如」servlet-api**在 tomcat 中已经提供了,我们只需要的是编译期提供而已。
总结来说:
1、compile:默认的 scope,运行期有效,需要打入包中。
2、provided:编译期有效,运行期不需要提供,不会打入包中。
3、runtime:编译不需要,在运行期有效,需要导入包中。(接口与实现分离)
4、test:测试需要,不会打入包中。
5、system:非本地仓库引入、存在系统的某个路径下的 jar。(一般不使用)