Maven Notes🌈
https://github.com/blinkfox/jpack-maven-plugin 对 SpringBoot 服务打包为 Windows、Linux、Docker、Helm Chart下可部署包的 Maven 插件
Maven Note | 替换pom.xml为其他格式的文件 | 一个插件使用所有前端工具 跨平台, 服务于java项目的管理工具(项目构建, 依赖管理, 项目信息管理) 类似的还有 Gradle.
jetbrains package search : https://pkg.biuaxia.cn/
- 1. error solutions
- 2. mavend 更快的 Maven
- 3. 命令行
- 4. 内置变量
- 5. spring-io-platform
- 6. why maven
- 7. Maven是什么
- 8. maven wrapper
- 9. 安装
- 10. Maven项目目录规范
- 11. demo
- 12. 生命周期命令 and 包含效果
- 13. 安装卸载本地包
- 14. 打包成可执行jar
- 15. 使用archetype生成项目骨架
- 16. 坐标
- 17. 快照版本和发布版本
- 18. 依赖的配置
- 19. 案例:账户注册服务
- 20. Maven中的测试
- 21. 生命周期
- 22. 插件
- 22.1. jib-maven-plugin 构建 docker 镜像
- 22.2. 查看插件帮助文档
- 22.3. 插件绑定
- 22.4. 插件的自定义绑定
- 22.5. 在命令行给插件设置参数
- 22.6. 在pom中设置插件的全局参数
- 22.7. 在pom中给插件任务配置个性化参数
- 22.8. 常用的插件
- 22.8.1. 打可执行包插件
- 22.8.2. maven-war-plugin 打 war 包
- 22.8.3. maven-eclipse-plugin
- 22.8.4. 执行脚本命令
- 22.8.5. versions-maven-plugin 管理子模块版本
- 22.8.6. build-helper-maven-plugin 自定义 build目录结构
- 22.8.7. maven-dependency-plugin 管理依赖库
- 22.8.8. jetty和tomcat
- 22.8.9. maven-source-plugin 打包源码
- 22.8.10. native-maven-plugin 打二进制包
- 22.8.11. jib-maven-plugin 打 docker 镜像
- 22.8.12. maven-resources-plugins 处理资源替换
- 22.8.13. maven-compiler-plugin 编译
- 22.8.14. proguard-maven-plugin
- 22.8.15. spring-boot-maven-plugin
- 22.8.16. maven-install-plugin
- 22.8.17. frontend-maven-plugin 管理前端环境
- 22.9. 编写maven插件
- 23. maven属性
- 24. 开启资源文件过滤
- 25. 开启web资源过滤
- 26. maven profile 多环境配置
- 27. 模块的聚合\&继承
- 28. dependencyMangement元素
- 29. pluginManagement元素
- 30. 反应堆
- 31. 历史项目改造为maven项目
- 32. 检查依赖冲突
- 33. Maven项目的持续集成
- 34. 编写Maven模板archetype
1. error solutions
1.1. 仓库镜像有问题
error:
maven blocked mirror for repositories
solutions:
https://stackoverflow.com/questions/67833372/getting-blocked-mirror-for-repositories-maven-error-even-after-adding-mirrors
https://gist.github.com/vegaasen/1d545aafeda867fcb48ae3f6cd8fd7c7
Put this section in your ~/.m2/settings.xml-file, and rerun mvn with -U option. U're done ✅.
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 http://maven.apache.org/xsd/settings-1.2.0.xsd">
...
<mirrors>
<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>external:dont-match-anything-mate:*</mirrorOf>
<name>Pseudo repository to mirror external repositories initially using HTTP.</name>
<url>http://0.0.0.0/</url>
<blocked>false</blocked>
</mirror>
</mirrors>
...
</settings>
1.2. 依赖解析有问题
# 清除本地依赖 (或者清除指定依赖)
mvn dependency:purge-local-repository [-DmanualInclude=org.springframework:spring-webmvc]
2. mavend 更快的 Maven
https://github.com/apache/maven-mvnd#install-manually
mvnd -version
3. 命令行
手动安装jar到本地仓库: mvn install:install-file -Dfile=D:/soft/ojdbc6.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.3 -Dpackaging=jar -DgeneratePom=true
# install jar file
mvn install:install-file -Dfile=/Users/xiaoyu/repo/sanxia_gaoke/resources/ojdbc8/19.3.0.0/ojdbc8-19.3.0.0.jar -DgoupId=com.oracle -DartifactId=ojdbc8 -Dversion=19.3.0.0 -Dpackaging=jar -DgeneratePom=true
# reactor 反应堆
# 指定构建模块
# -am --also-make 同时构建指定模块的依赖模块;
# -amd -also-make-dependents 同时构建依赖指定模块的模块;
# -pl --projects <arg> 构建制定的模块,模块间用逗号分隔;
# -rf -resume-from <arg> 从指定的模块恢复反应堆。
mvn clean package -pl <指定模块工程路径> -am
4. 内置变量
${basedir} 项目根目录
${project.build.directory} 构建目录,缺省为target
${project.build.outputDirectory} 构建过程输出目录,缺省为target/classes
${project.build.finalName} 产出物名称,缺省为{project.artifactId}-${project.version}
${project.packaging} 打包类型,缺省为jar
${project.xxx} 当前pom文件的任意节点的内容
5. spring-io-platform
Spring IO Platform框架简单来说就是一个版本号兼容系统,是一个依赖维护平台
它将常用第三方类库的兼容的版本组织起来。只要我们在项目中引用了Spring IO Platform,就不需要为这些第三方类库设置版本号了,Spring IO Platform会自动帮我们设置所有兼容的版本号
可以完美的支持Maven和Gradle;
直接导入
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
or 设置为 parent pom
<parent>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Brussels-SR3</version>
<relativePath/>
</parent>
如果需要覆盖某个类库的版本号:
<properties>
<foo.version>1.1.0.RELEASE</foo.version>
</properties>
看一个 spring boot 项目的完整例子:
<?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>com.example</groupId>
<artifactId>helloworld</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Athens-SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.4.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Additional lines to be added here... -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.4.3.RELEASE</version>
<!-- 因为不是继承 parent pom, -->
<!-- 需要自己添加配置,绑定repackage Goal -->
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
6. why maven
ant有一个很致命的缺陷,那就是没办法管理依赖
7. Maven是什么
一般来说我们典型的工作场景是这样的: 一天的工作开始之前, 先从源码库 pull 出新的代码, 然后单元测试, 如果发现bug, 联系作者一起调试, 然后进行自己的工作(编写自己的单元测试和代码); 如果这时 QA 发过来几个bug, 先本地重现(生成war包, 部署到web容器, 启动查看), bug解决首提交代码; 如果这时leader需要看测试报告, 于是需要将相关工具集成进ide, 生成测试覆盖率报告;
观察每天的这个过程, 其实大部分都是一些重复的劳动(编译, 运行单元测试, 生成文档, 打包部署), 这一类繁琐的过程都可归类于 "构建";
Maven为所有的项目抽象出了一个构建过程的生命周期模型, 对于不同的生命周期有大量的Maven插件可用; 这样抽象的好处是以前的10个项目可能有十种构建方式, 而现在统一可以使用Maven命令进行构建;
至于依赖管理, 一般一个Java应用会依赖于大量第三方类库, 类库之间可能还会有互相的依赖, 版本冲突, 此时这些类库的管理就是个繁重的体力活, Maven给了一个优雅的解决方案, 后面细说;此外Maven还能帮我们管理分散在各个角落的项目信息(项目描述, 开发者列表, 版本控制仓库地址, 测试报告...)
8. maven wrapper
Maven Wrapper is designed to make the maven version of each member in the dev team to be the same.
执行mvnw,比如:mvnw clean,如果本地没有匹配的maven版本,直接会去下载maven,放在用户目录下的.m2/wrapper中
项目的依赖的jar包会直接放在项目目录下的repository目录,这样可以很清晰看到当前项目的依赖文件。
如果需要更换maven的版本,只需要更改项目当前目录下.mvn/wrapper/maven-wrapper.properties的distributionUrl属性值,更换对应版本的maven下载地址。mvnw命令就会自动重新下载maven。
We can generate maven wrapper manually: mvn -N io.takari:maven:wrapper [-Dmaven=3.3.3]
, but spring boot initializer is usually having maven wrapper generated automatically
8.1. 国内使用
Change maven-wrapper.properties:
https://archive.apache.org/dist/maven/maven-3/
https://maven.apache.org/download.cgi
distributionUrl=https://mirrors.cloud.tencent.com/apache/maven/maven-3/3.8.6/binaries/apache-maven-3.8.6-bin.zip
pom.xml has to make some changes to specify repository mirrors:
<project>
<!-- 在此配置,以阿里云源为例 -->
<repositories>
<repository>
<id>tencent</id>
<url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>tencent</id>
<url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>
</pluginRepository>
</pluginRepositories>
</project>
<!-- or -->
<repositories>
<repository>
<id>aliyunmaven</id>
<name>aliyunmaven</name>
<url>https://maven.aliyun.com/repository/public</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>MavenCentral</id>
<url>http://repo1.maven.org/maven2/</url>
</repository>
<repository>
<id>aliyunmavenApache</id>
<url>https://maven.aliyun.com/repository/apache-snapshots</url>
</repository>
</repositories>
<!-- or -->
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
9. 安装
- Maven依赖于jdk
- 升级只需要下载新版Maven, 更新M2_HOME即可
- 如果使用Maven报
outOfMemeoryError
, 可设置MAVEN_OPTS
环境变量为- Xms128m -Xmx512m
以扩大java默认最大可用内存; conf/setttings.xml
和.m2/setttings.xml
都可以配置Maven, 前者是全局范围生效, 后者只对当前用户生效, 推荐修改后者, 因为升级方便;用户配置文件设定后, 全局配置文件就失效了- eclipse集成时, 不要使用其内置的Maven, 使用外置的我们自己配置的Maven
9.1. aliyun 阿里云仓库
<repositories>
<!--阿里云主仓库,代理了maven central和jcenter仓库-->
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<!--阿里云代理Spring 官方仓库-->
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://maven.aliyun.com/repository/spring</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
9.2. spring 仓库
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
9.3. 配置本地仓库地址
项目寻找依赖, 会先从远程仓库将构件下载到本地仓库然后使用; 本地仓库默认在${user_dir}/.m2/repository(输入第一条Maven命令后生成); 如果希望自定义本地仓库位置, 可修改 ${user_dir}/.m2/setttings.xml(如果不存在, 可复制一份过来), 设置 localRepository 元素的值;
9.4. 配置远程仓库
如果默认的中央仓库无法满足要求, 如何配置其他的远程仓库地址呢?
可以在项目pom中配置
通过Repository子元素, 可以声明多个远程仓库, 仓库id必须唯一, 中央仓库id=central, 如果其他仓库使用这里id, 会覆盖中央仓库的配置;release和snapshoot元素用来控制发布版构件和快照版构件的下载, 快照版本和发布版本会写到, 此外, 这两个元素还有其他子元素
updatePolicy指定更新频率, 可取这些值: daily(默认, 每天检查), never(从不检查), always(每次构建都检查), interval: x(每隔x分钟检查一次, x为整数)
同时, 还可以在setttings中配置一份中央仓库的镜像, 规避GFW带来的慢速问题, 详细描述在这里
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf> <!-- 所有针对central仓库的请求都会被转到这个镜像 -->
</mirror>
</mirrors>
9.5. 配置远程仓库认证(非必须)
仓库信息可以再pom文件中配置, 认证只能在setttings中配置
9.6. 部署到远程仓库
编辑项目的pom, 配置distributionManagement元素
构件部署到远程仓库, 和下载不同, 这时通常需要验证, 需要在setttings中配置servers
最后运行 mvn clean deploy
9.7. 阿里云 aliyun 镜像
如果仓库a能够提供仓库b的全部内容, 那么a是b的一个镜像, 设置镜像通常是为了规避慢速问题
setttings中配置:
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf> <!-- 所有针对central仓库的请求都会被转到这个镜像 -->
</mirror>
</mirrors>
此外, Maven对于镜像的mirrorOf标签还支持更多语法
10. Maven项目目录规范
- 如果遵循这个规范, Maven会自动搜寻对应目录下的对应资源, 而无需额外配置
- 如果是历史项目改造成Maven, 代码目录无法改变了, 也可以在pom文件中配置, 下文详述
11. demo
先看pom文件:
<?xml version = "1.0" encoding = "UTF-8"?><!-- xml头, 指定xml版本, 编码方式 -->
<!-- project是所有pom的根元素, 属性中声明了一些命名空间, xsd, 不是必须的但是可以让第三方编辑器有智能提示 -->
<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">
<!-- pom模型版本, 对于Maven2和Maven3来说只能是4.0.0 -->
<modelVersion>4.0.0</modelVersion>
<!-- 三要素, 必须 -->
<groupId>com.xy.mvnbook</groupId><!-- 公司xy建立一个项目mvnbook -->
<artifactId>hello-world</artifactId><!-- 项目唯一id -->
<version>0.0.1-SNAPSHOT</version><!-- 版本会不断更新, 比如从1.0到1.1-snapshoot到1.1到2.0, snapshoot表示快照 -->
<name>Maven Hollo World Project</name><!-- 不是必须 -->
<packaging>jar</packaging><!-- 打包类型, 默认jar -->
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope><!-- 依赖范围, 默认是"compile"表示该依赖对主/测试代码均有效, 这里"test"表示只对测试生效, 即测试代码中可以正常import JUnit, 但是在主代码中import JUnit就会报错(证明依赖只是被加入主代码的classpath中) -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId><!-- 把依赖的lib打包到一个jar, 生成可执行jar -->
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.xy.mvnbook.hello.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
代码:
/**
* mvn helloworld
*
* @version 0.1
* @author xy
* @date 2018年3月24日 上午11:40:17
*/
public class HelloWorld {
public String hello() {
return "Hello Maven";
}
public static void main(String[] args) {
System.out.println(new HelloWorld().hello());
}
}
/**
* test helloWorld
*
* @version 0.1
* @author xy
* @date 2018年3月24日 下午1:07:07
*/
public class HelloWorldTest {
@Test
public void testHello() {
HelloWorld hw = new HelloWorld();
String result = hw.hello();
assertEquals("Hello Maven", result);
}
}
下面试试简单的mvn命令
mvn clean
: 清除输出目录target/(Maven构建的所有输出默认在target/下); 实际是maven-clean-plugin
插件的clean
目标在起作用;mvn compile
: 编译项目主代码, 主要是maven-resources-plugin:2.6:resources
和maven-compiler-plugin:3.1:compile
在生效;一般和clean联用,mvn clean compile
;mvn clean test
: 调用Maven执行测试, 测试前会首先进行项目主资源处理, 主代码编译, 测试资源处理, 测试代码编译,以及测试后的报告生成, 实际执行了 clean:clean, resources:resources, compiler:compile, resources:testResources, compile:testCompile, surefire:test(surefire是Maven中负责执行测试的插件, 他运行测试用例HelloWorldTest并输出测试报告);mvn clean package
: 打包, 打成jar类型包; 就是jar:jar在生效, 主代码打包成jar文件, 输出到target/目录中, 文件命名是根据artifact-version.jar规则进行命名的, 如果需要自定义输出jar的名字, 可以修改finalName;mvn clean install
: 安装到本地Maven仓库; 安装后, 其他项目才可以使用;
mvn clean test
由于需要做资源替换, 编译的工作, 输出信息较多, 截了下面2张图片
mvn clean package
需要先执行资源替换, 编译, 测试, 再才能打包, 输出信息较多
mvn clean install
需要先资源替换, 编译, 测试, 打包, 最后安装
12. 生命周期命令 and 包含效果
综上, mvn的几个基本命令执行时是有包含关系的,
- compile
- test
- package
- install
后面的命令效果会包含前面的命令
mvn clean install -N
只会安装 parent 工程, 不会安装子项目
mvn install -U
安装时, 强制更新 snapshot 模块, 对于 release 版本模块, 不会更新
13. 安装卸载本地包
# 安装
mvn install:install-file -Dfile=bpm-interface-2.0.1.jar -DgroupId=com.ctg.qdp -DartifactId=bpm-interface -Dversion=2.0.1 -Dpackaging=jar
mvn install:install-file -Dfile=ojdbc6.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.4 -Dpackaging=jar
mvn install:install-file -Dfile=ojdbc8-19.0.0.0.0.jar -DgroupId=com.oracle.jdbc -DartifactId=ojdbc8.jar -Dversion=19.0.0.0.0 -Dpackaging=jar -DgeneratePom=true
mvn install:install-file -Dfile=quickCode-spring-boot-starter-1.0.jar -DgroupId=com.beeei -DartifactId=quickCode-spring-boot-starter -Dversion=1.0 -Dpackaging=jar -DgeneratePom=true
mvn install:install-file -Dfile=tgpms-1.0.jar -DgroupId=com.ctgpc -DartifactId=tgpms -Dversion=1.0 -Dpackaging=jar -DgeneratePom=true
mvn install:install-file -Dfile=vform-1.0.jar -DgroupId=com.tgpms -DartifactId=vform -Dversion=1.0 -Dpackaging=jar -DgeneratePom=true
# 卸载
# 最方便的是直接到 $HOME/.m2 手动删除
mvn dependency:purge-local-repository -DmanualInclude="groupId:artifactId, ..."
14. 打包成可执行jar
https://www.baeldung.com/executable-jar-with-maven
需要用到maven-shade-plugin
插件, 通过mvn package
默认生成的jar虽然有main方法, 但是是不能够直接运行的, 因为main方法的信息没有添加到manifest中(打开jar中的META-INF/MANIFEST可以看到, 没有Main-Class这一行),
增加如下配置:
<plugin><!-- 位于project/build/plugins下 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId><!-- 把依赖的lib打包到一个jar, 生成可执行jar -->
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.xy.mvnbook.hello.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
此时再次打包, 输出两个jar, hello-world-1.0-SNAPSHOOT.jar
和original-hello-world-1.0-SNAPSHOOT.jar
, 前者是带有Main-class信息的可执行jar, 后者是原始的jar;
15. 使用archetype生成项目骨架
一般称项目基本的目录结构和pom文件为骨架; Maven中使用maven-archetype-plugin
插件完成这一工作;
Maven3中使用:
mvn archetype:generate
, 虽然没有指定插件版本, 但是maven3只会去下载最新的稳定版本很安全;Maven2中需要指定具体的插件版本(即按照严格的命令格式 mvn groupId:artifactId:version:goal), 不然maven2会去下载最新版本的插件, 可能会得到不稳定的snapshoot版本导致运行失败;
对于maven3, 如果之前未运行过该命令, 会先下载该插件, 而后需要选择一个骨架, maven默认选了一个骨架
maven-archetype-quickstart
直接回车以选择, 接着输入groupId, artifactId, version, package, 确认;- package表示目录结构, 即"src/main/java"下的目录结构
- artifactId表示项目名称
- groupId表示安装到仓库时的目录
16. 坐标
groupId
: 定义maven项目隶属的实际项目比如 spring-core隶属于SpringFramework, spring-context也隶属于SpringFramework; 同时也是install时安装到仓库的目录artifactId
: 定义实际项目中的一个maven模块, 推荐做法是使用实际项目名称作为其前缀, 比如groupId是'com.xy.project', 那么artifactId可设为'project-xxx', 这样方便寻找实际构件, maven生成的构件默认名称以artifactId开头version
: 版本, snapshoot..., 见 快照版本和发布版本packaging
: 定义maven打包方式(可选, 默认jar), jar/war/pomclassifier
: classifier可以是任意的字符串,用于拼接在GAV之后来确定依赖的坐标- 可以帮助定义构建输出的一些附属构件; 比如定义了主构件'xxx.jar', 有时可能还会使用一些插件生成'xxx-javadoc.jar', 'xxx-source.jar'这样的附属构件, 这时, 'javadoc', 'source'就是这两个附属构件的classifier ;(classifier不能直接定义)
- 可以区分不同jdk版本所生成的jar包, 如
<classifier>jdk15</classifier> , <classifier>jdk13</classifier>
17. 快照版本和发布版本
Snapchat版本作用
为什么需要区分快照版本和发布版本?
考虑这个场景: 小a在开发模块A(v: 2.1), 同时, 小m在开发模块B, B -> A, 开发中, 小a经常需要将模块最新版构建输出, 交给小m, 怎么方便的做这个工作呢?
Maven的快照机制可以解决这个问题.
小a将A版本设为2.1-snapshoot, 然后发布到私服, 这时Maven会自动为该构件打上时间戳(版本2.1表示已经稳定, 且只对应了唯一构件, 而版本2.1-snapshoot表示不稳定, 对应仓库中存在多个打了时间戳的构件)
有了时间戳, Maven能够随时找到仓库中的该构件2.1-snapshoot版本的最新文件. 这时小m配置对应依赖, 版本也设为2.1-snapshoot, Maven会自动检查最新版本, 检查频率可以在配置远程仓库时设置, 也可以强制检查更新通过mvn clean install-U
18. 依赖的配置
18.1. 基本元素
- groupId, artifactId, version
- type: 对应于坐标定义中的packaging, 一般不必声明, 默认为jar
- scope: 依赖的范围,
compile(默认), test, provided, runtime, system, import
, 见#scope - optional: 指定依赖是否可选, 见可选依赖
- exclusion: 排除#传递性依赖, 见#排除依赖
18.2. scope
项目的 编译, 测试, 运行 会使用三套不同的classpath,
compile
: 默认范围, 使用这种范围的构件在 编译, 测试, 运行时均有效, 典型如spring-core;test
: 只在 测试时引入, 在编译/运行时无法使用对应依赖, 如JUnit;provided
: 表示别的设施(Web Container)会提供, 所以不会被打包; 其他和 compile 等同runtime
: 依赖对于测试, 运行有效, 而在编译代码时不参与典型如:jdbc驱动实现(主代码编译只需要jdk提供的jdbc接口, 只有在执行测试和运行时才需要jdbc的具体实现)
另外runtime的依赖通常和optional搭配使用,optional为true 表示不会传递。我可以用A实现,也可以用B实现
system
: 和provided完全一致, 但是使用system时, 必须显式的指定依赖文件的路径(通过systemPath元素, systemPath可以使用环境变量如${JAVA_HOME}), 用于引入本地的不在maven仓库中的依赖, 由于不可移植,要慎用;import
: 导入依赖的范围, 不会对3种classpath产生实际的影响, 参见 dependencyMangement元素
18.3. 传递性依赖
下面说说 传递性依赖
以案例中的 email模块为例, 项目依赖spring-core, 而spring-core又依赖 common-logging, email模块有一个'compile'范围的spring-core依赖, spring-core有一个'compile'范围的common-logging依赖, 那么email就会有一个'compile'范围的common-logging依赖(或者说commons-logging是email的一个传递性依赖), 看下图:
有了传递性依赖机制, 使用spring framework就不需要考虑它依赖了什么; 第一直接依赖和第二直接依赖决定了传递性依赖, 关系如下图:
可以这么理解:
- 当 '第二直接依赖' 范围 = 'compile' 时, '传递性依赖'范围 = '第一直接依赖'范围
- 当 '第二直接依赖' 范围 = 'runtime' 时, '传递性依赖'范围 = '第一直接依赖'范围, 特例: 第一依赖范围 = 'compile', 第二依赖范围 = 'runtime', 则传递性依赖 = 'runtime'
- 当 '第二直接依赖' 范围 = 'test' 时, 依赖不会传递
- 当 '第二直接依赖' 范围 = 'provided' 时, '传递性依赖' 范围 = 'provided', 但是 '第一直接依赖' = 'provided'时, '传递性依赖' 才会传递
18.4. Maven的依赖调解特性
传递性依赖问题---Maven的 依赖调解特性
问题1: 有这样的依赖 a -> b -> c -> x(version: 1.0), a -> d -> x(version: 2.0); 此时有2个版本的x, 前者路径长度为3, 后者长度为2, 选择哪个呢?
Maven依赖调解第一原则: 路径最近者优先
; 根据这个原则 x(version: 2.0) 会被Maven选中引入
问题2: 有这样的依赖 a-> b -> x(version: 1.0), a -> c -> x(version: 2.0); 此时2个版本的x, 路径长度都一样, 但是b在pom中声明的位置比c靠前, 选择哪个版本呢?
Maven依赖调解第二原则: 第一声明者优先
; 根据这个原则, x(version: 1.0)会被选中
18.5. 可选依赖
考虑这个场景: a -> b -> x(optional: true), a -> b -> y(optional: true); b模块实现了x, y两个特性, 这两个特性互斥(用户不能同时使用两个特性, 比如b是一个持久层隔离工具包, 支持MySQL, Oracle), 在构建的时候, 需要需要这两种数据库的驱动程序, 但在使用的时候只会依赖其中一个;
如果x, y被指定为可选依赖, 此时, x, y 只会对 b 产生影响, 将不会对 a 产生任何影响, 如果a需要使用需要显式的声明;
通过基本元素里的 optional
元素指定依赖为可选依赖 (默认 optional 为 false)
不过不推荐使用optional, 因为这违背了解耦的原则, 实际上,可以为两个数据库分别建立 Maven项目, 分配统一的groupId, 不同的artifactId, 在各自的pom中声明对应的jdbc依赖, 用户根据需要引入两个项目之一, 这样就避免了使用 optional;
18.6. 排除依赖
考虑这个场景: a -> b -> x(version: 1.0-snapshoot), snapshoot不稳定, 我们需要替换这个不稳定版本的x, 然后在a中声明一个稳定版本的x; 如何替换呢?
需要注意的是: 声明exclusion时, 只需要groupId, artifactId, 而不需要version;
18.7. 依赖的优化
mvn dependency:list
: 查看已解析依赖列表mvn dependency:tree
: 查看依赖树mvn dependency:tree
简要的列出项目的依赖树mvn dependency:tree -D verbose
详细列出项目的依赖树mvn dependency:tree -D includes=groupid:artifactId excludes=groupId:artifactId
根据自己的喜好列出依赖树
mvn dependency:analyze
: 分析项目依赖; 比如a -> b -> c, 同时 a -> c, 这时去掉a中的c也不报错, 因为b中含有c, 但是如果去掉a总显式声明的c, 一旦b版本变化, b中的c版本也会发生变化, 给项目带来失败的风险, 而且不容易查出来; 因此要显式声明项目中直接使用的依赖;
18.8. 引入外部依赖
https://blog.csdn.net/qq_30938705/article/details/79245820
19. 案例:账户注册服务
19.1. 需求
先看需求用例:
界面原型类似这样
19.2. 简单的设计
接口可能有:
- 生成验证码图片(String generateCaptchaKey(), void generateCaptchaImage(String key))
- 处理注册请求(void signUp(SignUpRequest rqt))
- 激活账户(boolean activate(String activationNumber))
- 处理登录(boolean login(String id, String pwd))
验证码部分是这样: 通过 generateCaptchaKey() 生成一个 key, 然后通过 generateCaptchaImage(String key)生成image, key和image传递给client, user肉眼识别image, client传回key和image的值, server端通过key查到正确的image值, 和client传回的image值对比;
singUp方法创建一个未被激活的账户, 同时发送一封带有链接的激活邮件, active方法接受一个激活码, 查找对应的账户激活;
麻雀虽小, 五脏俱全, 模块也需要划分一下:
- web: 包含所有和web 相关的内容, JSP, Servlet, web.xml...直接依赖service模块, 使用其提供的服务;
- service: 系统的核心, 封装了所有细节, 对外暴露简单的接口(Facade模式);
- persist: 处理账户信息的持久化;
- captcha: 处理验证码key的生成, image生成, 验证;
- email: 激活邮件的编写发送;
19.3. account-email模块
目录结构如下:
19.3.1. parent-pom
首先看看 parent pom配置:
<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>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging> <!-- 作为parent项目, 这里必须为pom, 否则无法构建 -->
<name>Account Parent</name>
<modules>
<module>../account-email</module>
<module>../account-persist</module>
<module>../account-captcha</module>
<module>../account-service</module>
</modules>
<properties>
<springframework.version>4.3.12.RELEASE</springframework.version>
<junit.version>4.12</junit.version>
<lombok.version>1.16.18</lombok.version>
<mail.version>1.4.7</mail.version>
<greenmail.version>1.4.1</greenmail.version>
<dom4j.version>1.6.1</dom4j.version>
<kaptcha.version>2.3.2</kaptcha.version>
<maven-compiler-plugin.version>3.6.0</maven-compiler-plugin.version>
<maven-resource-plugin.version>3.0.1</maven-resource-plugin.version>
<maven-site-plugin.version>3.6</maven-site-plugin.version>
<!-- 文件拷贝时的编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- 编译时的编码 -->
<maven.compiler.encoding>UTF-8</maven.compiler.encoding>
</properties>
<dependencyManagement>
<dependencies>
<!-- <dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-email</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-persist</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-captcha</artifactId>
<version>${project.version}</version>
</dependency> -->
<!-- https://mvnrepository.com/artifact/com.github.penggle/kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!-- **************spring start******************************* -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
<!-- ****************spring end******************** -->
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.mail/mail -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>${mail.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.icegreen/greenmail -->
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>${greenmail.version}</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>${dom4j.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resource-plugin</artifactId>
<version>${maven-resource-plugin.version}</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>${maven-site-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
<!-- <inherited>false</inherited>
<configuration>
</configuration> --><!-- 注释掉, 不然源码jar包无法生成, mvn clean verify -->
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<!-- 同一个插件目标绑定到了不同的生命周期阶段 -->
<execution>
<id>ant-validate</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>绑定到了"validate"阶段</echo>
</tasks>
</configuration>
</execution>
<execution>
<id>ant-verify</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>绑定到了"verify"阶段</echo>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<!-- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
</plugin> -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
19.3.2. email-pom
email-pom配置
<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>com.xy.mvnbook.account</groupId> --> <!-- 可以去掉, parent项目已经定义的groupId了 -->
<artifactId>account-email</artifactId> <!-- 以groupId "account"做前缀 , 方便区分其他模块-->
<packaging>jar</packaging>
<name>Account Email</name>
<parent>
<groupId>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath><!-- 父pom文件相对路径, 默认是 [../pom.xml] -->
</parent>
<dependencies>
<!-- **************spring*** 实现DI所必须的spring组件(spring-core, spring-context, spring-context-support, spring-beans)******************************* -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context-support -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- ****************spring end************************************************* -->
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.mail/mail
实现邮件发送必须 -->
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.icegreen/greenmail
邮件服务测试套件 -->
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<!-- 如果没有在parent pom.xml中配置comiler插件, 这里还要补上该插件, 指定编译版本 -->
</project>
19.3.3. 主代码&测试
/**
* sendMail interface
*
* @version 0.1
* @author xy
* @date 2018年3月24日 下午5:29:20
*/
public interface AccountEmailService {
/**
* send email
*
* @param to 接受地址
* @param subject 主题
* @param htmlText 正文
* @throws AccountEmailException 邮件发送错误
*/
void sendMail(String to, String subject, String htmlText) throws AccountEmailException;
}
// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.email.impl;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import com.xy.mvnbook.account.email.AccountEmailService;
import com.xy.mvnbook.account.email.exception.AccountEmailException;
import lombok.Data;
/**
* 'sendMail' implements
*
* @version 0.1
* @author xy
* @date 2018年3月24日 下午5:30:37
*/
@Data
public class AccountEmailServiceImpl implements AccountEmailService {
/**
* sender, spring提供的简化邮件发送的util
*/
private JavaMailSender sender;
/**
* 系统邮箱
*/
private String systemEmail;
public void sendMail(String to, String subject, String htmlText) throws AccountEmailException {
MimeMessage msg = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
try {
helper.setFrom(systemEmail);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(htmlText);
sender.send(msg);
} catch (MessagingException e) {
throw new AccountEmailException("Faild to send email.", e);
}
}
}
package com.xy.mvnbook.account.email;
import static org.junit.Assert.assertEquals;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetup;
import com.xy.mvnbook.account.email.exception.AccountEmailException;
/**
* 测试
*
* @version 0.1
* @author xy
* @date 2018年3月24日 下午6:00:36
*/
public class AccountEmailServiceTest {
private GreenMail green;
@Before
public void startGreen() {
// 基于smtp协议初始化greenmail
green = new GreenMail(ServerSetup.SMTP);
green.setUser("test@xiaoyu.com", "123456");// 创建账户
green.start(); // 默认监听25端口
}
@Test
public void testSendMail() {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("account-email.xml");
AccountEmailService service = (AccountEmailService) ctx.getBean("accountEmailService");
try {
service.sendMail("test@xiaoyu.com", "Test subject", "<h3>Test</h3>");
} catch (AccountEmailException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
green.waitForIncomingEmail(2000, 1); // 接受一封邮件, 最多等待2s
MimeMessage[] msgs = green.getReceivedMessages();
assertEquals(1, msgs.length);
try {
assertEquals("Test subject", msgs[0].getSubject());
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
assertEquals("<h3>Test</h3>", GreenMailUtil.getBody(msgs[0]).trim());
ctx.close();
}
@After
public void stopGreen() {
green.stop();
}
}
19.3.4. 测试资源-主资源读取顺序
这里需要注意: 测试类会到src/test/reources下找service.properties, 如果没有找到, 再到src/main/resources下找, 不必担心classpath下有2个service.properties;
19.3.5. spring注入配置
当然还有必要的注入配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:property-placeholder location="classpath:service.properties"/>
<bean id="javaMailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="protocol" value="${email.protocol}"></property>
<property name="host" value="${email.host}"></property>
<property name="port" value="${email.port}"></property>
<property name="username" value="${email.username}"></property>
<property name="password" value="${email.password}"></property>
<property name="javaMailProperties">
<props>
<prop key="mail.${email.protocol}.auth">${email.auth}</prop>
</props>
</property>
</bean>
<bean id="accountEmailService" class="com.xy.mvnbook.account.email.impl.AccountEmailServiceImpl">
<property name="sender" ref="javaMailSender"></property>
<property name="systemEmail" value="${email.systemEmail}"></property>
</bean>
</beans>
以及邮件发送相关props
email.protocol=smtp
email.host=localhost
email.port=25
email.username=test@xiaoyu.com
email.password=123456
email.auth=true
email.systemEmail=775000738sdfs@qq.com
mvn clean test
, 输出如下:
p1 | p2 |
---|
19.4. account-persist模块
负责数据的持久化, 以xml保存数据
目录结构
在parent-pom文件存在的情况下, 添加本模块的pom
19.4.1. account-persist的pom
<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>
<artifactId>account-persist</artifactId>
<name>Account Persist</name>
<packaging>jar</packaging>
<parent>
<groupId>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath><!-- 父pom文件相对路径, 默认是 [../pom.xml] -->
</parent>
<dependencies>
<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
</dependency>
<!-- *************************************************spring依赖 start, 主要用来支持依赖注入****************************************** -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- *************************************************spring依赖 end, 主要用来支持依赖注入****************************************** -->
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<!-- 开启测试资源目录过滤 -->
<testResources>
<testResource>
<directory>src/test/resources</directory><!-- 这里是否加${project.basedir}均可 -->
<filtering>true</filtering>
</testResource>
</testResources>
<!-- 开启主资源目录过滤 -->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId><!-- 支持java8 -->
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resource-plugin</artifactId><!-- 使用utf-8处理资源处理 -->
</plugin>
</plugins>
</build>
</project>
19.4.2. 主代码&测试
package com.xy.mvnbook.account.persist;
import com.xy.mvnbook.account.persist.dto.Account;
import com.xy.mvnbook.account.persist.exception.AccountPersistException;
/**
* 持久化 interface, crud方法
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:08:15
*/
public interface AccountPersistService {
Account createAccount(Account account) throws AccountPersistException;
boolean deleteAccount(String id) throws AccountPersistException;
Account updateAccount(Account account) throws AccountPersistException;
Account readAccount(String id) throws AccountPersistException;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.persist.exception;
/**
* 自定义异常
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:12:51
*/
public class AccountPersistException extends Exception {
public AccountPersistException(String msg, Exception e) {
super(msg, e);
}
private static final long serialVersionUID = -7205545969802653298L;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* 常量(自定义的标签元素名称)
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:13:34
*/
public interface AccountPersist {
String ELEMENT_ROOT = "account-persist";
String ELEMENT_ACCOUNTS = "accounts";
String ELEMENT_ACCOUNT = "account";
String ELEMENT_ACCOUNT_ID = "id";
String ELEMENT_ACCOUNT_NAME = "name";
String ELEMENT_ACCOUNT_PASSWORD = "password";
String ELEMENT_ACCOUNT_EMAIL = "email";
String ELEMENT_ACCOUNT_ACTIVATED = "activated";
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.persist.dto;
import lombok.Data;
/**
* dto
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:15:11
*/
@Data
public class Account {
private String id;
private String name;
private String password;
private String email;
private boolean activated;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.persist.impl;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Iterator;
import java.util.List;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;
import com.xy.mvnbook.account.persist.AccountPersistService;
import com.xy.mvnbook.account.persist.constants.AccountPersist;
import com.xy.mvnbook.account.persist.dto.Account;
import com.xy.mvnbook.account.persist.exception.AccountPersistException;
import lombok.Data;
/**
* 持久化 实现
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:09:54
*/
@Data
public class AccountPersistServiceImpl implements AccountPersistService {
/**
* 文件存储地址
*/
private String fileUrl;
/**
* dom4j reader
*/
private SAXReader reader = new SAXReader();
public Account createAccount(Account account) throws AccountPersistException {
Document doc = this.readDocument();
Element accountsEle = doc.getRootElement().element(AccountPersist.ELEMENT_ACCOUNTS);
Element accountEle = accountsEle.addElement(AccountPersist.ELEMENT_ACCOUNT);
accountEle.addElement(AccountPersist.ELEMENT_ACCOUNT_ID).addText(account.getId());
accountEle.addElement(AccountPersist.ELEMENT_ACCOUNT_NAME).addText(account.getName());
accountEle.addElement(AccountPersist.ELEMENT_ACCOUNT_PASSWORD).addText(account.getPassword());
accountEle.addElement(AccountPersist.ELEMENT_ACCOUNT_EMAIL).addText(account.getEmail());
accountEle.addElement(AccountPersist.ELEMENT_ACCOUNT_ACTIVATED).addText(account.isActivated() ? "true" : "false");
this.writeDocument(doc);
return account;
}
public boolean deleteAccount(String id) throws AccountPersistException {
Document doc = this.readDocument();
Element accountsEle = doc.getRootElement().element(AccountPersist.ELEMENT_ACCOUNTS);
@SuppressWarnings("unchecked")
Iterator<Element> ei = accountsEle.elementIterator();
while(ei.hasNext()) {
Element next = ei.next();
if (next.elementText(AccountPersist.ELEMENT_ACCOUNT_ID).equals(id)) {
accountsEle.remove(next);
return true;
}
}
return false;
}
public Account updateAccount(Account account) throws AccountPersistException {
Document doc = this.readDocument();
Element accountsEle = doc.getRootElement().element(AccountPersist.ELEMENT_ACCOUNTS);
@SuppressWarnings("unchecked")
Iterator<Element> ei = accountsEle.elementIterator();
Account old = null;
while(ei.hasNext()) {
Element next = ei.next();
if (next.elementText(AccountPersist.ELEMENT_ACCOUNT_ID).equals(account.getId())) {
old = this.buildAccount(next);
if (account.getEmail() != null && !"".equals(account.getEmail())) {
next.element(AccountPersist.ELEMENT_ACCOUNT_EMAIL).addText(account.getEmail());
}
if (account.getName() != null && !"".equals(account.getName())) {
next.element(AccountPersist.ELEMENT_ACCOUNT_NAME).addText(account.getName());
}
if (account.getPassword() != null && !"".equals(account.getPassword())) {
next.element(AccountPersist.ELEMENT_ACCOUNT_PASSWORD).addText(account.getPassword());
}
if (account.isActivated()) {
next.element(AccountPersist.ELEMENT_ACCOUNT_ACTIVATED).addText("true");
}
}
}
return old;
}
public Account readAccount(String id) throws AccountPersistException {
Document doc = readDocument();
Element accounts = doc.getRootElement().element(AccountPersist.ELEMENT_ACCOUNTS);
@SuppressWarnings("unchecked")
List<Element> elements = accounts.elements();
for (Element ele : elements) {
if (ele.elementText(AccountPersist.ELEMENT_ACCOUNT_ID).equals(id)) {
return buildAccount(ele);
}
}
return null;
}
private Account buildAccount(Element ele) {
Account account = new Account();
account.setActivated("true".equals(ele.elementText(AccountPersist.ELEMENT_ACCOUNT_ACTIVATED)) ? true : false);
account.setEmail(ele.elementText(AccountPersist.ELEMENT_ACCOUNT_EMAIL));
account.setId(ele.elementText(AccountPersist.ELEMENT_ACCOUNT_ID));
account.setName(ele.elementText(AccountPersist.ELEMENT_ACCOUNT_NAME));
account.setPassword(ele.elementText(AccountPersist.ELEMENT_ACCOUNT_PASSWORD));
return account;
}
/**
* read document
*
* @return
* @throws AccountPersistException
*/
private Document readDocument() throws AccountPersistException {
File file = new File(fileUrl);
// if the file doesn't exits, create it. contain basic element
if (!file.exists()) {
File parentFile = file.getParentFile();
parentFile.mkdirs();
Document doc = DocumentFactory.getInstance().createDocument();
doc.addElement(AccountPersist.ELEMENT_ROOT).addElement(AccountPersist.ELEMENT_ACCOUNTS);
writeDocument(doc);
}
try {
return reader.read(new File(fileUrl));
} catch (DocumentException e) {
throw new AccountPersistException("unable to read data.xml", e);
}
}
private void writeDocument(Document doc) throws AccountPersistException {
OutputStreamWriter osw = null;
try {
osw = new OutputStreamWriter(new FileOutputStream(fileUrl), "utf-8");
XMLWriter xmlWriter = new XMLWriter(osw, OutputFormat.createPrettyPrint());
xmlWriter.write(doc);
} catch (IOException e) {
throw new AccountPersistException("unable to write data.xml", e);
} finally {
if (osw != null) {
try {
osw.close();
} catch (IOException e) {
throw new AccountPersistException("unable to close data.xml", e);
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.persist;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.xy.mvnbook.account.persist.dto.Account;
/**
* 测试
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午6:16:14
*/
public class AccountPersistServiceTest {
private AccountPersistService service;
@Before
public void pre() throws Exception {
File file = new File("target/test-classes/persist-data.xml");
if (file.exists()) {
file.delete();
}
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("account-persist.xml");
service = (AccountPersistService) context.getBean("accountPersistService");
Account account = new Account();
account.setId("id");
account.setName("name");
account.setPassword("password");
account.setEmail("email");
account.setActivated(true);
service.createAccount(account);
}
@Test
public void testReadAccount() throws Exception {
Account account = service.readAccount("id");
assertNotNull(account);
assertEquals("id", account.getId());
assertEquals("name", account.getName());
assertEquals("password", account.getPassword());
assertEquals("email", account.getEmail());
assertTrue(account.isActivated());
}
}
19.4.3. 注入配置
当然还有一些资源文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- <context:property-placeholder location="classpath:service.properties"/> -->
<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:persist.properties"></property>
</bean>
<bean id="accountPersistService" class="com.xy.mvnbook.account.persist.impl.AccountPersistServiceImpl">
<property name="fileUrl" value="${persist.fileUrl}"></property>
</bean>
</beans>
properties文件:
persist.fileUrl=${project.build.outputDirectory}/persist-data.xml
(这里包含了一个maven属性 ${project.build.outputDirectory}
maven的主输出目录, 详细看maven属性 )
19.5. account-captcha模块
19.5.1. captcha模块的pom
<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>
<parent>
<groupId>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath><!-- 父pom文件相对路径, 默认是 [../pom.xml] -->
</parent>
<artifactId>account-captcha</artifactId>
<name>Account Captcha</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<!-- *************************************************spring依赖 start, 主要用来支持依赖注入****************************************** -->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<!-- *************************************************spring依赖 end, 主要用来支持依赖注入****************************************** -->
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
19.5.2. 主代码&测试
package com.xy.mvnbook.account.captcha;
import java.util.List;
import com.xy.mvnbook.account.captcha.exception.AccountCaptchaException;
/**
* 验证码服务 interface
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午10:18:02
*/
public interface AccountCaptchaService {
/**
* 获取 key
* @return
* @throws AccountCaptchaException
*/
String generageCaptchaKey() throws AccountCaptchaException;
/**
* 根据key获取img
* @param key
* @return
* @throws AccountCaptchaException
*/
byte[] genarateCaptchaImage(String key) throws AccountCaptchaException;
/**
* 验证
* @param key
* @param value
* @return
* @throws AccountCaptchaException
*/
boolean validate(String key, String value) throws AccountCaptchaException;
/**
* 获取预定义的验证码内容, 方便测试
* @return
*/
List<String> getPreText();
void setPreText(List<String> preText);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.captcha.impl;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.imageio.ImageIO;
import org.springframework.beans.factory.InitializingBean;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import com.xy.mvnbook.account.captcha.AccountCaptchaService;
import com.xy.mvnbook.account.captcha.exception.AccountCaptchaException;
import com.xy.mvnbook.account.captcha.util.RandomGenerator;
/**
* 验证码服务 实现
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午10:22:10
*/
public class AccountCaptchaServiceImpl implements AccountCaptchaService, InitializingBean {
/**
* 验证码生成器
*/
private DefaultKaptcha producer;
/**
* InitializingBean接口中的方法, 会在spring初始化bean时调用; 这里用来初始化 producer
*/
public void afterPropertiesSet() throws Exception {
producer = new DefaultKaptcha();
producer.setConfig(new Config(new Properties()));
}
/**
* 存储key和验证码正确的值
*/
private Map<String, String> captchaMap = new HashMap<String, String>();
/**
* 预定义字符串, 方便测试
*/
private List<String> preText;
public String generageCaptchaKey() throws AccountCaptchaException {
String key = RandomGenerator.getRandomString();
String value = this.getCaptchaText();
captchaMap.put(key, value);
return key;
}
public byte[] genarateCaptchaImage(String key) throws AccountCaptchaException {
String value = captchaMap.get(key);
if (null == value) {
throw new AccountCaptchaException("Captcha key: " + key + "not found.");
}
BufferedImage img = producer.createImage(value);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
ImageIO.write(img, "jpg", out);
} catch (IOException e) {
throw new AccountCaptchaException("Faild to write captcha stream.", e);
}
return out.toByteArray();
}
public boolean validate(String key, String value) throws AccountCaptchaException {
String text = captchaMap.get(key);
if (null == text) {
throw new AccountCaptchaException("Captcha key: " + key + "not found.");
}
if (value.equals(text)) {
captchaMap.remove(key);
return true;
}
else {
return false;
}
}
public List<String> getPreText() {
return this.getPreText();
}
public void setPreText(List<String> preText) {
this.preText = preText;
}
private int count = 0;
private String getCaptchaText() {
// 如果 预定义验证码文本 != null, 则顺序循环该字符串列表读取值, 否则, 随机创建一个串
if (preText != null && !preText.isEmpty()) {
String text = preText.get(count);
count = (count + 1) % preText.size();
return text;
} else {
return producer.createText();
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.captcha.util;
import java.util.Random;
/**
* 生成随机串 工具类
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午11:06:06
*/
public class RandomGenerator {
/**
* 字符范围
*/
private static final String RANGE = "0123456789abcdefghijklmnopqrstuvwxyz";
/**
* 生成随机的8位字符串
* @return
*/
public static synchronized String getRandomString() {
Random random = new Random();
StringBuilder result = new StringBuilder();
for (int i = 0; i < 8; i++) {
result.append(RANGE.charAt(random.nextInt(RANGE.length())));
}
return result.toString();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.captcha;
import static org.junit.Assert.*;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.xy.mvnbook.account.captcha.util.RandomGenerator;
/**
* 测试
*
* @version 0.1
* @author xy
* @date 2018年3月25日 下午11:04:24
*/
public class AccountCaptchaServiceTest {
private AccountCaptchaService service;
@Before
public void pre() throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("account-captcha.xml");
service = (AccountCaptchaService)context.getBean("accountCaptchaService");
}
@Test
public void testRandomString() throws Exception{
HashSet<String> set = new HashSet<String>(100);
for (int i=0; i<100; i++) {
String random = RandomGenerator.getRandomString();
assertFalse(set.contains(random));
set.add(random);
}
}
@Test
public void testGenerateCaptcha() throws Exception {
String key = service.generageCaptchaKey();
assertNotNull(key);
byte[] imgArray = service.genarateCaptchaImage(key);
assertTrue(imgArray.length > 0);
File imgFile = new File("target/" + key + ".jpg");
FileOutputStream out = new FileOutputStream(imgFile);
out.write(imgArray);
if (out != null) {
out.close();
}
assertTrue(imgFile.exists() && imgFile.length() > 0);
}
@Test
public void testValidateCorrect() throws Exception{
ArrayList<String> preDefinedText = new ArrayList<String>();
preDefinedText.add("12345");
preDefinedText.add("abcde");
service.setPreText(preDefinedText);
String key = service.generageCaptchaKey();
service.genarateCaptchaImage(key);
assertTrue(service.validate(key, "12345"));// 顺序读取preDefinedText的元素
String key1 = service.generageCaptchaKey();
service.genarateCaptchaImage(key1);
assertTrue(service.validate(key1, "abcde"));
}
@Test
public void testValidateIncorrect() throws Exception {
ArrayList<String> preDefinedText = new ArrayList<String>();
preDefinedText.add("12345");
service.setPreText(preDefinedText);
String key = service.generageCaptchaKey();
service.genarateCaptchaImage(key);
assertFalse(service.validate(key, "1234"));
}
}
19.5.3. 注入配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- <context:property-placeholder location="classpath:service.properties"/> -->
<!-- <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:persist.properties"></property>
</bean> -->
<bean id="accountCaptchaService" class="com.xy.mvnbook.account.captcha.impl.AccountCaptchaServiceImpl">
</bean>
</beans>
19.6. account-service模块
web项目打包成war包, 部署到web容器中的war包一般有如下的目录结构
本模块用来封装前三个模块的细节
19.6.1. service-pom配置
<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>
<parent>
<groupId>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath><!-- 父pom文件相对路径, 默认是 [../pom.xml] -->
</parent>
<artifactId>account-service</artifactId>
<name>Account Service</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-email</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-persist</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-captcha</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
19.6.2. 主代码
package com.xy.mvnbook.account.service.bean;
import lombok.Data;
/**
* 请求对象
*
* @version 0.1
* @author xy
* @date 2018年4月11日 下午7:49:19
*/
@Data
public class SignUpRequest {
private String id;
private String email;
private String username;
private String pwd;
private String confirmPwd;
private String captchaKey;
private String captchaValue;
private String activateServiceUrl;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.service;
import com.xy.mvnbook.account.service.bean.SignUpRequest;
import com.xy.mvnbook.account.service.exception.AccountServiceException;
/**
* 封装后暴露的接口
*
* @version 0.1
* @author xy
* @date 2018年4月11日 下午7:50:44
*/
public interface AccountService {
String generateCaptchaKey() throws AccountServiceException;
byte[] generateCaptchaImage(String captchaKey) throws AccountServiceException;
void activate(String activateNum) throws AccountServiceException;
void login(String id, String pwd) throws AccountServiceException;
void singUp(SignUpRequest rqt) throws AccountServiceException;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
package com.xy.mvnbook.account.service.impl;
import java.util.HashMap;
import java.util.Map;
import com.xy.mvnbook.account.captcha.AccountCaptchaService;
import com.xy.mvnbook.account.captcha.exception.AccountCaptchaException;
import com.xy.mvnbook.account.captcha.util.RandomGenerator;
import com.xy.mvnbook.account.email.AccountEmailService;
import com.xy.mvnbook.account.email.exception.AccountEmailException;
import com.xy.mvnbook.account.persist.AccountPersistService;
import com.xy.mvnbook.account.persist.dto.Account;
import com.xy.mvnbook.account.persist.exception.AccountPersistException;
import com.xy.mvnbook.account.service.AccountService;
import com.xy.mvnbook.account.service.bean.SignUpRequest;
import com.xy.mvnbook.account.service.exception.AccountServiceException;
import lombok.Data;
/**
* 接口实现
*
* @version 0.1
* @author xy
* @date 2018年4月11日 下午7:51:32
*/
@Data
public class AccountServiceImpl implements AccountService {
private AccountCaptchaService accountCaptchaService;
private AccountEmailService accountEmailService;
private AccountPersistService accountPersistService;
private Map<String, String> activateMap = new HashMap<String, String>();
public String generateCaptchaKey() throws AccountServiceException {
try {
return accountCaptchaService.generageCaptchaKey();
} catch (AccountCaptchaException e) {
throw new AccountServiceException("无法创建验证码key。", e);
}
}
public byte[] generateCaptchaImage(String captchaKey) throws AccountServiceException {
try {
return accountCaptchaService.genarateCaptchaImage(captchaKey);
} catch (AccountCaptchaException e) {
throw new AccountServiceException("无法创建验证码image", e);
}
}
public void activate(String activateNum) throws AccountServiceException {
String accountId = this.getActivateMap().get(activateNum);
if (accountId == null) {
throw new AccountServiceException("无效的激活码。");
}
try {
Account account = accountPersistService.readAccount(accountId);
account.setActivated(true);
accountPersistService.updateAccount(account);
} catch (AccountPersistException e) {
throw new AccountServiceException("无法激活。");
}
}
public void login(String id, String pwd) throws AccountServiceException {
try {
Account account = accountPersistService.readAccount(id);
if (account == null) {
throw new AccountServiceException("account不存在");
}
if (!account.isActivated()) {
throw new AccountServiceException("account未激活");
}
if (!account.getPassword().equals(pwd)) {
throw new AccountServiceException("密码错误");
}
} catch (AccountPersistException e) {
throw new AccountServiceException("无法登录.", e);
}
}
public void singUp(SignUpRequest rqt) throws AccountServiceException {
if (!rqt.getPwd().equals(rqt.getConfirmPwd())) {
throw new AccountServiceException("两次输入的密码不一致。");
}
boolean validate;
try {
validate = accountCaptchaService.validate(rqt.getCaptchaKey(), rqt.getCaptchaValue());
} catch (AccountCaptchaException e) {
throw new AccountServiceException("验证码验证动作失败。", e);
}
if (!validate) {
throw new AccountServiceException("验证码输入错误。");
}
Account account = new Account();
account.setActivated(false);
account.setEmail(rqt.getEmail());
account.setId(rqt.getId());
account.setName(rqt.getUsername());
account.setPassword(rqt.getPwd());
try {
accountPersistService.createAccount(account);
} catch (AccountPersistException e) {
throw new AccountServiceException("保存account失败", e);
}
String activateId = RandomGenerator.getRandomString();
activateMap.put(activateId, account.getId());
try {
accountEmailService.sendMail(account.getEmail(), "请激活。。。", rqt.getActivateServiceUrl().endsWith("/") ?
rqt.getActivateServiceUrl() + activateId : rqt.getActivateServiceUrl() + "/" + activateId);
} catch (AccountEmailException e) {
throw new AccountServiceException("发送邮件失败。", e);
}
}
}
19.6.3. spring配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<!-- <context:property-placeholder location="classpath:service.properties"/> -->
<!-- <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:persist.properties"></property>
</bean> -->
<import resource="classpath*:/account-captcha.xml"/>
<import resource="classpath*:/account-email.xml"/>
<import resource="classpath*:/account-persist.xml"/>
<bean id="accountService" class="com.xy.mvnbook.account.service.impl.AccountServiceImpl">
<property name="accountCaptchaService" ref="accountCaptchaService"></property><!-- 这里小心ref错写成value -->
<property name="accountEmailService" ref="accountEmailService"></property>
<property name="accountPersistService" ref="accountPersistService"></property>
</bean>
</beans>
19.6.4. 安装account-service模块注意
需要先安装parent的pom, 否则安装service项目时会报错, 因为service的pom中引用了parent 的pom信息
进入parent项目目录 mvn -N clean install
, -N
表示不要构建子项目
19.7. account-web模块
包括servlet和前端页面
19.7.1. web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/account-persist.xml
classpath:/account-captcha.xml
classpath:/account-email.xml
classpath:/account-service.xml
</param-value>
</context-param>
<listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener>
<servlet>
<servlet-name>CaptchaImageServlet</servlet-name>
<display-name>CaptchaImageServlet</display-name>
<description>return captcha image to jsp</description>
<servlet-class>com.xy.mvnbook.account.web.CaptchaImageServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>SignUpServlet</servlet-name>
<display-name>SignUpServlet</display-name>
<description></description>
<servlet-class>com.xy.mvnbook.account.web.SignUpServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CaptchaImageServlet</servlet-name>
<url-pattern>/captcha_image</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SignUpServlet</servlet-name>
<url-pattern>/sign_up</url-pattern>
</servlet-mapping>
</web-app>
19.7.2. pom.xml
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xy.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>account-web</artifactId>
<packaging>war</packaging>
<name>account-web Maven Webapp</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- <dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
</dependency> -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>account-service</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
<build>
<finalName>account</finalName><!-- 最终生成的jar名字; 默认为${project.artifactId}-${project.version} -->
<plugins>
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.5.v20170502</version>
<configuration>
<scanIntervalSeconds>5</scanIntervalSeconds>
<webAppConfig>
<contextPath>/${project.artifactId}</contextPath>
</webAppConfig>
<connectors>
<connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector">
<port>8081</port>
</connector>
</connectors>
</configuration>
</plugin>
</plugins>
</build>
</project>
20. Maven中的测试
20.1. maven-surefire-plugin
Maven使用maven-surefire-plugin
调用JUnit执行测试, mvn test
即可, 如果某个测试方法希望忽略, 使用@Ignore
标注
默认下, 该插件会自动执行符合如下规范的测试用例
20.2. 如何跳过测试
方法1: mvn package -D skipTests
(有无空格都可), 不执行测试用例,但编译测试类生成相应的class文件至target/test-classes下 (idea 跳过默认是这种)
方法2: mvn package -D maven.test.skip=true
(有无空格都可) 跳过测试, 测试代码也不编译, 但是不推荐这么做
方法 3:
<plugin>
<groupId>org.apache.maven.plugin</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
20.3. 动态指定运行的测试类
如果-D test 没有找到测试用例, 会报错, 如果希望这时不报错,
20.4. 包含&排除测试用例
20.5. 测试报告&测试覆盖率报告
20.6. 改用TestNG代替Junit
TestNG一大优点就是支持测试组
的概念, 对测试可以达到方法级别的归类, 而JUnit只能到类级别的归类;
将JUnit依赖替换成TestNG, mvn test
即可
需要配置testng.xml来配置需要运行的测试集合
20.7. 打包测试代码
默认测试代码不会打包, 如下配置才会打包测试代码
21. 生命周期
Maven拥有3套独立的生命周期, 每个周期又有自己的阶段, 这些阶段有序, 后面阶段依赖于前面的阶段
- clean
- pre-clean 这个阶段执行一些清理前需要完成的工作
- clean 清理上次构建生成的文件
- post-clean 执行一次清理后需要完成的工作
- site
- pre-site
- site
- post-site
- site-deploy 将站点发布到服务器
- default(定义了构建的所有步骤, 最全)
- validate
- initialize
- generate-sources
- process-sources: 处理项目主资源文件, 对src/main/resources下的文件进行变量替换, 然后复制到输出的主classpath目录中
- generate-resources
- process-resources
- compile : 编译项目主代码到主输出classpath目录
- process-classes
- generate-test-sources
- process-test-sources : 处理项目测试资源文件
- generate-test-resources
- process-test-resources
- test-compile
- process-test-classes
- test 使用单元测试框架测试,
- prepare-package
- package
- pre-integration-test
- integration-test
- post-integration-test
- verify
- install
- deploy
mvn命令实际上就是调用三套生命周期的不同阶段进行组合
22. 插件
22.1. jib-maven-plugin 构建 docker 镜像
无需安装 Docker 环境
<!--
build提供了创建镜像并推送到远程仓库功能。
buildTar提供创建一个包含镜像的tar文件功能。(docker load命令将tar文件的镜像加载到本地镜像仓库)
dockerBuild提供创建docker镜像到本地功能。
exportDockerContext提供创建dockerfile功能
由于编译构建是在没有docker环境的情况下构建,所以使用build命令和dockerBuild命令并不能制作出镜像,只能使用buildTar命令制作出一个包含镜像的tar文件
-->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<from>
<!--base image-->
<image>amazoncorretto:20.0.2-alpine3.18</image>
</from>
<!--generated image configuration-->
<to>
<!-- 只有这个是必须的-->
<image>xiaoyureed/${project.artifactId}:v${project.version}</image>
<tags>
<tag>v${project.version}</tag>
</tags>
<!-- <auth>-->
<!-- <username></username>-->
<!-- <password></password>-->
<!-- </auth>-->
</to>
<allowInsecureRegistries>true</allowInsecureRegistries>
<container>
<mainClass>io.github.xiaoyureed.raincloud.example.springbootmybatismysql.SpringBootMybatisMysqlApp</mainClass>
<!-- 镜像创建时间-->
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
<!-- <jvmFlags>-->
<!-- <jvmFlag>-Xms4g</jvmFlag>-->
<!-- <jvmFlag>-Xmx4g</jvmFlag>-->
<!-- </jvmFlags>-->
<ports>
<port>8102</port>
</ports>
</container>
</configuration>
<executions>
<!-- 将 jib buil 绑定到 package 阶段-->
<!-- <execution>-->
<!-- <id>jib-mvn-plugin</id>-->
<!-- <phase>package</phase>-->
<!-- <goals>-->
<!-- <goal>build</goal>-->
<!-- </goals>-->
<!-- </execution>-->
</executions>
</plugin>
22.2. 查看插件帮助文档
mvn help:describe -Dplugin=<plugin_name> -Dgoal=<goal> -Ddetail=true
22.3. 插件绑定
插件绑定(生命周期的阶段和插件目标相互绑定)
Maven内置绑定(为了开箱即用, Maven为核心的一些生命周期阶段绑定了很多插件目标, 当user运行命令调用生命周期阶段的时候, 对应的插件目标就会执行对应任务, 这有点像设计模式中的模板方法模式)
内置绑定的例子看下图
default生命周期还有很多阶段, 这里只是列出了内置绑定了插件的阶段, 其他没有绑定插件的阶段就没有实际行为;
22.4. 插件的自定义绑定
比如创建源码jar包, 内置绑定中并没有这一项任务, 我们可以自定义绑定关系, 生成源码jar要使用maven-source-plugin
插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id> <!-- id名称随意 -->
<phase>verify</phase> <!-- 生命周期阶段 -->
<goals>
<goal>jar-no-fork</goal><!-- 插件目标 -->
</goals>
<!-- <inherited>false</inherited>
<configuration>
</configuration> --><!-- 注释掉, 不然源码jar包无法生成, mvn clean verify -->
</execution>
</executions>
</plugin>
运行 mvn clean verify
会在打包同时生成源码jar包
这里考虑一个问题: 如果有多个插件目标被绑定到同一个阶段, 目标的执行顺序是怎样的呢? ------插件声明的先后顺序决定了目标的执行顺序
22.5. 在命令行给插件设置参数
maven-surefire-plugin 提供一个参数 maven.test.skip ,如果为 true, 会跳过测试, 于是希望跳过测试可以这么做:
mvn clean install -Dmaven.test.skip=true
22.6. 在pom中设置插件的全局参数
还是生成源代码jar包的那个例子
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id> <!-- id名称随意 -->
<phase>verify</phase> <!-- 生命周期阶段 -->
<goals>
<goal>jar-no-fork</goal><!-- 插件目标 -->
</goals>
<configuration>
<!-- 在这里配置参数 -->
</configuration>
</execution>
</executions>
</plugin>
22.7. 在pom中给插件任务配置个性化参数
以maven-antrun-plugin
为例, 它可以执行ant任务
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<!-- 同一个插件目标绑定到了不同的生命周期阶段 -->
<execution>
<id>ant-validate</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>绑定到了"validate"阶段</echo>
</tasks>
</configuration>
</execution>
<execution>
<id>ant-verify</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>绑定到了"verify"阶段</echo>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
运行 mvn clean verify -D maven.test.skip
, 会跳过测试, 输出如下:
p1 | p2 | p3 |
---|
22.8. 常用的插件
22.8.1. 打可执行包插件
https://www.baeldung.com/executable-jar-with-maven
22.8.1.1. maven-shade-plugin
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<shadedArtifactAttached>true</shadedArtifactAttached>
<transformers>
<transformer implementation=
"org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>
jetty.Main
</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
22.8.1.2. maven-assembly-plugin
根据不同环境打包成tar.gz或者zip
使用 descriptorRefs(官方提供的定制化打包方式),官方提供的 descriptorRef 有 bin, jar-with-dependencies, src, project。[不建议使用]
使用 descriptors,指定打包文件 src/assembly/src.xml,在该配置文件内指定打包操作。
<project>
[...]
<build>
[...]
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<descriptors>
<descriptor>src/assembly/assembly.xml</descriptor><!-- 指定插件配置文件路径 -->
</descriptors>
<!--这样配置后,mvn deploy不会把assembly打的zip包上传到nexus-->
<attach>false</attach>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase><!--绑定到package-->
<goals>
<goal>single</goal><!--只运行一次-->
</goals>
</execution>
</executions>
[...]
</project>再看插件自身的配置文件 assembly.xml
<?xml version="1.0" encoding="UTF-8"?>
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>mini</id>
<formats>
<format>tar.gz</format><!-- 打包的文件格式, 可以有 zip、tar、tar.gz (or tgz)、tar.bz2 (or tbz2)、jar、dir、war,可以同时指定多个打包格式 -->
<!-- <format>dir</format> -->
</formats>
<includeBaseDirectory>false</includeBaseDirectory><!-- 是否将工程名作为压缩包的根路径 -->
<fileSets><!-- 管理一组文件的存放位置 -->
<fileSet>
<directory>src/main/webapp</directory><!-- 需要打包的目录 -->
<excludes>
<exclude>**/*config.yml</exclude>
<exclude>**/*.data</exclude><!-- admin user & role -->
<exclude>**/admin_log/**</exclude>
<exclude>**/docs/**</exclude><!-- 有中文打包会出乱码 -->
</excludes>
<outputDirectory>/</outputDirectory><!-- 打包后输出目录 -->
</fileSet>
<fileSet>
<directory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</directory>
<includes>
<include>${project.build.finalName}*.jar</include>
<include>sodo-*.jar</include>
</includes>
<outputDirectory>/WEB-INF/lib</outputDirectory>
</fileSet>
</fileSets>
<files><!-- 可以指定目的文件名到指定目录,其他和 fileSets 相同 -->
<file>
<source>README.txt</source><!-- 源文件 -->
<outputDirectory>/</outputDirectory><!-- 输出目录 -->
</file>
</files>
<dependencySets><!-- 用来定制工程依赖 jar 包的打包方式 -->
<dependencySet>
<!--是否把本项目添加到依赖文件夹下-->
<useProjectArtifact>true</useProjectArtifact>
<outputDirectory>lib</outputDirectory><!-- 指定包依赖目录,该目录是相对于根目录 -->
<!--将scope为runtime的依赖包打包-->
<scope>runtime</scope>
<!--此外还有两个元素: includes/include* List<String> 包含依赖; excludes/exclude* List<String> 排除依赖 -->
</dependencySet>
</dependencySets>
</assembly>
22.8.2. maven-war-plugin 打 war 包
打war包插件, 打包时对web资源开启资源过滤, 使得能够使用Maven属性. 见开启web资源过滤
另外, 如果使用 servlet 3.0, 需要配置 'failOnMissingWebXml'
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.4</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
22.8.3. maven-eclipse-plugin
生成.classpath和.project文件,并且配置Eclispe将Maven作为External工具
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
运行:mvn eclipse:eclipse 生成.classpath和.project文件
22.8.4. 执行脚本命令
22.8.4.1. exec-maven-plugin
可以执行命令行, or 执行 shell
通过命令行 执行java main
mvn exec:java -Dexec.mainClass="com.demo.HelloWorld"
并不打可执行包
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>com.demo.config.DocMapper</mainClass>
<arguments>
<argument>${project.build.outputDirectory}\doc-path-map.txt</argument>
<argument>${basedir}\src</argument>
<argument>**/resource/*.java</argument>
</arguments>
</configuration>
</plugin>
or 简单点的
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
或者, 直接执行 shell 脚本:
<build>
<plugins>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>uncompress</id>
<phase>install</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${basedir}/uncompress.sh</executable>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
22.8.4.2. bash-maven-plugin 脚本内容直接卸载 pom 文件中
执行 bash 脚本
<plugin>
<!-- Run with:
mvn bash:run
mvn install
-->
<groupId>com.atlassian.maven.plugins</groupId>
<artifactId>bash-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<executions>
<execution>
<id>test</id>
<phase>integration-test</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
<configuration>
<script>
# Here you can execute shell commands
echo "Tomcat will start"
/opt/apache-tomcat/bin/startup.sh
</script>
</configuration>
</plugin>
22.8.4.3. maven-antrun-plugin 更强大
执行脚本
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<id>generateSources</id>
<phase>generate-sources</phase>
<configuration>
<!-- 提供各种不同 task 类型的标签, echo, replaceregexp... -->
<tasks>
<exec executable="/bin/bash">
<arg value="myFirst.sh" />
<arg value="inputOne" />
</exec>
<exec executable="/bin/bash">
<arg value="src/mySecond.sh" />
<arg value="inputTwo" />
</exec>
<!-- 替换util模块Global.PRODUCT_VERSION版本号 -->
<replaceregexp file="datagear-util/src/main/java/org/datagear/util/Global.java" encoding="UTF-8" match="VERSION = "\d+(\.\d+){1,2}(\-\w+){0,1}"" replace="VERSION = "${project.version}""/>
<!-- 为management模块的datagear.sql添加版本号行 -->
<echo file="datagear-management/src/main/resources/org/datagear/management/ddl/datagear.sql" encoding="UTF-8" append="true">
${line.separator}
-----------------------------------------
--version[${project.version}], DO NOT EDIT THIS LINE!
-----------------------------------------
${line.separator}
</echo>
</tasks>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
22.8.5. versions-maven-plugin 管理子模块版本
统一更新子模块版本号为父模块版本号插件
<!-- mvn -N versions:update-child-modules -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.3</version>
<configuration>
<generateBackupPoms>false</generateBackupPoms>
</configuration>
</plugin>
22.8.6. build-helper-maven-plugin 自定义 build目录结构
codehaus提供了build-helper-maven-plugin插件来支持自定义的项目目录结构(相对于Maven默认目录结构来说)。
比较全的配置:
<!-- 设置多个源文件夹 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.8</version>
<executions>
<!-- 添加主资源文件目录 -->
<execution>
<!--自定义名称,不可重复-->
<id>add-resource</id>
<!--指定绑定到生命周期-->
<phase>initialize</phase>
<!--指定指定的目标,可添加多个-->
<goals>
<goal>add-resource</goal>
</goals>
<configuration>
<resources>
<!--资源文件目录,可添加多个-->
<resource>
<directory>${basedir}/src/main/one</directory>
<!--是否启用变量过滤-->
<filtering>true</filtering>
<!--排除的文件,可添加多个-->
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>${basedir}/src/main/two</directory>
<filtering>true</filtering>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
<!-- 添加主源码目录 -->
<execution>
<id>add-source</id>
<phase>initialize</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/main/three</source>
<source>${basedir}/src/main/four</source>
</sources>
</configuration>
</execution>
<!-- 添加测试源码目录 -->
<execution>
<id>add-test-source</id>
<phase>initialize</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/main/five</source>
<source>${basedir}/src/main/six</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
用的多点的是这个这个简单点的配置, 源码在多个目录:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>1.12</version>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/common</source><!-- 添加规范外的源码目录 -->
</sources>
</configuration>
</execution>
</executions>
</plugin>
22.8.7. maven-dependency-plugin 管理依赖库
依赖项插件, 提供了处理工件的功能。它可以将本地或远程存储库中的工件(或构件中的文件)复制和/或解包到指定的位置。
参考: https://www.cnblogs.com/lianshan/p/7350614.html
比如, 部署的时候, 希望将依赖都打包到一个文件夹下
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>install</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
22.8.8. jetty和tomcat
jetty:
相关参考: 多模块项目, 希望 启动的 web-module 可以随时反应 core-module 的修改
Maven jetty plugin - automatic reload using a multi-module project
mvn jetty:run
<plugin>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-maven-plugin -->
<!-- 默认会去加载:
resources in ${project.basedir}/src/main/webapp
classes in ${project.build.outputDirectory}
web.xml in ${project.basedir}/src/main/webapp/WEB-INF/
-->
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>9.4.9.v20180320</version>
<configuration>
<scanIntervalSeconds>3</scanIntervalSeconds>
<httpConnector>
<port>8082</port>
</httpConnector>
<webApp>
<contextPath>/${project.artifactId}</contextPath>
</webApp>
</configuration>
</plugin>
tomcat:
mvn tomcat7:run
<!-- https://tomcat.apache.org/maven-plugin-2.2/
(参考: https://www.cnblogs.com/llguanli/p/7131253.html)
-->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/taobao</path>
<port>9090</port>
<uriEncoding>UTF-8</uriEncoding>
</configuration>
</plugin>
22.8.9. maven-source-plugin 打包源码
程序打包时候, 同时打包源码
<!-- 打包时添加源码 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
22.8.10. native-maven-plugin 打二进制包
需要和 springboot 3 合作, 默认设置了版本
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
22.8.11. jib-maven-plugin 打 docker 镜像
<!-- ./mvnw compile jib:build -Dimage=xxx -->
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.2</version>
</plugin>
22.8.12. maven-resources-plugins 处理资源替换
springboot 默认提供, 用于 maven 打包时资源文件的复制, 占位符的替换
处理 spring boot profile 和 maven profile 集成
在 spring boot 配置中, 使用 @xxxx@
来引用 maven 的 profile, 同时要打开 maven resource filter
不要复制某些文件, 在分环境打包时可以减少包体积:
<resources>
<resource>
<directory>src/main/resources</directory>
<!--①-->
<excludes>
<exclude>application*.properties</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<!--②-->
<filtering>true</filtering>
<includes>
<include>application.properties</include>
<include>application-${profile.active}.properties</include>
</includes>
</resource>
</resources>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<executions>
<execution>
<id>position-react-build</id>
<goals>
<goal>copy-resources</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<outputDirectory>${project.build.outputDirectory}/static</outputDirectory>
<resources>
<resource>
<directory>${frontend.dir}/out</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
①中,我们通过 excludes 来将所有的 application*.properties 排除在外,这样 maven 在打包时就不会复制这些文件。毕竟我们不希望把 application-dev.properties 也包含在 prod 的 jar 包里。
②中,通过开启 filtering,maven 会将文件中的 @XX@ 替换 profile 中定义的 XX 变量/属性。另外,我们还通过 includes 来告诉 maven 根据 自定义的 maven profile 来复制对应的 properties 文件。
22.8.13. maven-compiler-plugin 编译
编译插件, 可以设置 Java compile version
<plugin>
<!-- 指定maven编译的jdk版本,如果不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<!-- 一般而言,target与source是保持一致的,但是,有时候为了让程序能在其他版本的jdk中运行(对于低版本目标jdk,源代码中不能使用低版本jdk中不支持的语法),会存在target不同于source的情况 -->
<source>1.8</source> <!-- 源代码使用的JDK版本 -->
<target>1.8</target> <!-- 需要生成的目标class文件的编译版本 -->
<encoding>UTF-8</encoding><!-- 字符集编码 -->
<skipTests>true</skipTests><!-- 跳过测试 -->
<verbose>true</verbose>
<showWarnings>true</showWarnings>
<fork>true</fork><!-- 要使compilerVersion标签生效,还需要将fork设为true,用于明确表示编译版本配置的可用 -->
<executable><!-- path-to-javac --></executable><!-- 使用指定的javac命令,例如:<executable>${JAVA_1_4_HOME}/bin/javac</executable> -->
<compilerVersion>1.3</compilerVersion><!-- 指定插件将使用的编译器的版本 -->
<meminitial>128m</meminitial><!-- 编译器使用的初始内存 -->
<maxmem>512m</maxmem><!-- 编译器使用的最大内存 -->
<compilerArgument>-verbose -bootclasspath ${java.home}\lib\rt.jar</compilerArgument><!-- 这个选项用来传递编译器自身不包含但是却支持的参数选项 -->
<parameters>true</parameters>//插件的配置就可以从用户属性中获取, 即可以在 properties 中配置 java.version, maven.compiler.source, maven.compiler.target; springboot 已经配置了为 true, 所以只需引入插件即可在 properties 中设置 java.version...
</configuration>
<executions>
<execution>
<id>compile-tests</id>
<phase>process-test-sources</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<source>${maven.compile.source}</source>
<target>${maven.compile.target}</target>
</configuration>
</execution>
</executions>
</plugin>
22.8.14. proguard-maven-plugin
https://github.com/wvengen/proguard-maven-plugin 代码混淆
22.8.15. spring-boot-maven-plugin
为Spring Boot应用提供了执行Maven操作的可能。允许你打包可执行文件和war文件,并且就地运行。
spring-boot:repackage, 绑定到 package 阶段. 也就是在mvn package之后,再次打包可执行的jar/war,同时保留mvn package生成的jar/war为.origin (需要自己绑定)
spring-boot:run,运行Spring Boot应用, 指定 spring boot profile 启动 mvn spring-boot:run -Dspring-boot.run.profiles=dev
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 临时使其失效 -->
<configuration>
<skip>true</skip>
</configuration>
</plugin>
对于多模块, mvn spring-boot:run -pl [groupId:]<artifactId>
22.8.16. maven-install-plugin
本地依赖加入本地仓库
22.8.17. frontend-maven-plugin 管理前端环境
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.3</version>
<executions>
<!-- check if nodejs/npm installed -->
<execution>
<id>install-frontend-tools</id>
<goals>
<goal>install-node-and-yarn</goal>
</goals>
<!-- optional, also the default value -->
<phase>generate-resources</phase>
</execution>
<!-- install dependencies -->
<execution>
<id>yarn-install</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>install --registry=https://registry.npm.taobao.org</arguments>
<!-- <arguments>install</arguments>-->
</configuration>
</execution>
<execution>
<id>yarn-build-and-export</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>export</arguments>
</configuration>
</execution>
</executions>
<configuration>
<workingDirectory>${frontend.dir}</workingDirectory>
<installDirectory>${project.build.directory}</installDirectory>
<nodeVersion>v18.14.0</nodeVersion>
<yarnVersion>v1.22.19</yarnVersion>
<!-- optional, just for projects in China main land -->
<!-- <downloadRoot>http://npm.taobao.org/mirrors/node/</downloadRoot> -->
</configuration>
</plugin>
22.9. 编写maven插件
https://github.com/acanda/spring-banner-plugin 实例
TODO 代码生成器插件https://zhuanlan.zhihu.com/p/69935918
处理文件复制, 比如灾备文件
插件前缀: 可以让你更方便地输入指令,因为在命令行里,一般调用插件目标,需要完整输入“groupId”、“artifactId”、“version”等等定位到某个插件,然后通过“:”写下插件目标,就可以运行插件的目标了。
maven-${prefix}-plugin,这个方式是官网插件使用的
${prefix}-maven-plugin,这个方式是第三方插件(包括我们自己写的)使用的
可以自己在 pom 中指定前缀:
<plugin>
...
<configuration>
...
<globalPrefix>xxxx
23. maven属性
有6中类型的maven属性
内置属性, 如 ${basedir}表示项目根目录(pom所在目录), ${version}项目版本
pom属性, pom中对应元素的值。例如${project.artifactId}对应了
<project>下的<artifactId>
元素的值- ${project.build.sourceDirectory}:项目的主源码目录,默认为src/main/java/.
- ${project.build.testSourceDirectory}:项目的测试源码目录,默认为/src/test/java/.
- ${project.build.directory}:项目构建输出目录,默认为target/.
- ${project.outputDirectory}:项目主代码编译输出目录,默认为target/classes/.
- ${project.testOutputDirectory}:项目测试代码编译输出目录,默认为target/testclasses/.
- ${project.build.finalName}:项目打包输出文件的名称,默认为${project.artifactId}${project.version}.
Settings属性:与POM属性同理。如${settings.localRepository}指向用户本地仓库的地址
Java系统属性:所有Java系统属性都可以使用Maven属性引用,例如${user.home}指向了用户目录。可以通过命令行mvn help:system查看所有的Java系统属性
环境变量属性:所有环境变量都可以使用以env.开头的Maven属性引用。例如${env.JAVA_HOME}指代了JAVA_HOME环境变量的值。也可以通过命令行mvn help:system查看所有环境变量。
自定义属性
<project>
<properties>
<my.prop>hello</my.prop>
</properties>
</project>
在pom中其他地方使用${my.prop}
会被替换为hello
24. 开启资源文件过滤
通过 maven-resources-plugin 实现, 在打包时, 会有资源文件的复制, 在复制时, 替换 资源文件中的 @xxx@
占位符
默认情况下,Maven属性只有在POM中才会被解析。资源过滤就是指让Maven属性在资源文件(src/main/resources、src/test/resources)中也能被解析。
<build>
<!-- 可选 -->
<resources>
<resource>
<!-- ${project.basedir}可以省略 -->
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
</build>
从上面的配置中可以看出,我们其实可以配置多个主资源目录和多个测试资源目录。
25. 开启web资源过滤
普通资源位于[src/main/reousrces]下; web资源位于[src/main/webapp]下;
借助插件maven-war-plugin
include标签指定要过滤的文件, 这里表示过滤所有的css和js文件
26. maven profile 多环境配置
26.1. profile 基本使用
需要首先 开启资源文件过滤
mvn help:active-profiles
查看哪些profile被激活了, mvn help:all-profiles
列出所有的profile;
profile可以声明在三个文件中:
pom.xml
:很显然,这里声明的profile只对当前项目有效(此时profile中的properties和直接在pom的properties标签下定义效果是一样的);用户settings.xml
:.m2/settings.xml中的profile对该用户的Maven项目有效(比全局的优先级高);全局settings.xml
:conf/settings.xml,对本机上所有Maven项目有效
profile在pom.xml中可声明的元素, 和在settings.xml中可声明的元素是不一样的, 在 pom 中可以声明:
<project>
<repositories></repositories>
<pluginRepositories></pluginRepositories>
<distributionManagement></distributionManagement>
<dependencies></dependencies>
<dependencyManagement></dependencyManagement>
<modules></modules>
<properties></properties>
<reporting></reporting>
<build>
<plugins></plugins>
<defaultGoal></defaultGoal>
<resources></resources>
<testResources></testResources>
<finalName></finalName>
</build>
</project>
而setttings中只能声明这些元素
<project>
<repositories></repositories>
<pluginRepositories></pluginRepositories>
<properties></properties>
</project>
编写profile,在pom中的project元素下:
<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>prd</id>
<properties>
<env>prd</env>
</properties>
</profile>
</profiles>
如何激活profile呢?
命令行方式: 比如有两个profile, id为 devx, devy, 那么激活命令为 mvn clean install -P devx,devy
(-P 后面有无空格都可)
系统属性激活,用户可以配置当某系统属性存在或其值等于期望值时激活profile,如
<profiles>
<profile>
<activation>
<property>
<name>actProp</name>
<value>x</value>
</property>
</activation>
</profile>
</profiles>
上面配置表示只有系统属性 actProp 为 x 时激活 profile, 可以在命令行声明系统属性 mvn clean install -DactProp=x
, 这也是一种变相的从命令行激活profile的方法,而且多个profile完全可以使用同一个系统属性来激活
。系统属性可以通过mvn help:system
来查看 (-P, -D 区别: 前者用来制定 profile id, 后者用来指定 activation property)
操作系统环境激活
<profiles>
<profile>
<activation>
<os>
<name>Windows XP</name>
<family>Windows</family>
<arch>x86</arch>
<version>5.1.2600</version>
</os>
</activation>
</profile>
</profiles>
这里的family值包括Window、UNIX和Mac等,而其他几项对应系统属性的os.name、os.arch、os.version
文件存在与否激活
<profiles>
<profile>
<activation>
<file>
<missing>x.properties</missing>
<exists>y.properties</exists>
</file>
</activation>
</profile>
</profiles>
settings文件显式激活 (不实用):
<settings>
...
<activeProfiles>
<activeProfile>devx</activeProfile>
<activeProfile>devy</activeProfile>
</activeProfiles>
...
</settings>
默认激活(如果有其他profile被激活了, 默认激活就自动失效了)
26.2. profile build
在 profile 中定义 配置, 比如定义 build 配置, 使得不同 profile 打包不同的 配置文件
26.3. 配置默认 jdk 编译版本
<profiles>
<profile>
<id>jdk1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compailerVersion>1.8</...>
</properties>
</profile>
27. 模块的聚合&继承
maven的聚合为这种场景服务: 我们一次希望构建两个项目/模块, 而不是到两个模块目录下分别构建
为了实现聚合, 需要有额外一个项目充当parent, 可以查看 parent-pom, 作为parent项目, 该项目只有一个pom文件吗没有代码和资源文件, packaging 为 pom 类型
现在使用 mvn clean install
maven会先按照一定顺序构建一个反应堆, 依次构建各个模块
接下来看看继承.
继承主要是为了解决pom文件的重复配置问题; 重点是要正确设置relativePath
元素的值, 默认值为../pom.xml
; (默认值似乎无效, 需要手动指定)
子模块可以省略groupId和version
, 会从parent继承;
父pom中有哪些元素可以被继承呢? (group, version, dependencies, dependencyManagement, plugins, pluginManagement, properties ...)
聚合&继承实际上经常同时使用;
28. dependencyMangement元素
一般在parent pom中使用, 提供依赖管理功能, 并不会真正引入. 如果需要引入, 还需要在dependency中声明, 这时只需要groupId和artifactId元素就可以唯一标识一个依赖了;
这时可以介绍scope: import 了, 如果一个构件声明为import范围, 那么它只能在dependencyMangement元素中声明才有效, 该构件通常指向一个pom, 作用是将该pom中的dependencyMangement元素内容导入当前pom的dependencyMangement元素下; 只是对依赖进行管理(如版本号), 并不会引入到 classpath
29. pluginManagement元素
和dependencyMangement元素类似;
30. 反应堆
反应堆构件顺序: Maven按照顺序读取A的pom, 如果该A的pom没有依赖任何模块, 直接构建该项目A, 否则就先构建其依赖的模块B, 如果B还依赖构建C, 则进一步先构建模块C;
剪切反应堆: 希望只构件parent的某几个模块而不希望全部构建, maven提供这些命令
# 指定构建模块
# -am --also-make 同时构建指定模块的依赖模块;
# -amd -also-make-dependents 同时构建依赖指定模块的模块;
# -pl --projects <arg> 构建制定的模块,模块间用逗号分隔;
# -rf -resume-from <arg> 从指定的模块恢复反应堆。
mvn clean package -pl <指定模块工程路径> -am
31. 历史项目改造为maven项目
这时可能需要修改maven项目的规范, 比如修改源码目录
如果需要包含or排除命名不规范的测试用例 ,参见[包含&排除测试用例]
mvn -s <自定义位置的setttings> xx:xx
-s表示用户settings, -gs表示设定全局settingsmvn -f <自定义位置的pom>
32. 检查依赖冲突
mvn dependency:tree
简要的列出项目的依赖树mvn dependency:tree -D verbose
详细列出项目的依赖树mvn dependency:tree -D includes=groupid:artifactId excludes=groupId:artifactId
根据自己的喜好列出依赖树
33. Maven项目的持续集成
34. 编写Maven模板archetype
34.1. archetype组成元素
- archetype特性是通过插件
maven-archetype-plugin
实现的 mvn archetype:generate
会进入生成的交互界面, 有多个archetype供选择, 他们来自于archetype-catalog.xml, 详细使用见使用archetype生成项目骨架- 在自动化shell脚本中
mvn archetype:generate -B
, -B表示不要进入交互式界面.
怎么自定义?
一个archetype项目包括这几个部分
- pom.xml 模版自身的pom; 有基本的坐标, 默认jar打包方式即可
- src/main/resources/archetype-resources/* 下面的数据用于生成新的项目;包括pom原型, 主代码原型;
- src/main/resources/META-INF/maven/archetype.xml 模版的描述符文件; 声明哪些文件会包含在模版中, 使用了哪些属性参数
下面创建一个简单的archetype
34.2. archetype-demo
34.2.1. archetype自身pom
首先是archetype的pom:
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xiaoyu.archetype</groupId>
<artifactId>archetype-learn</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Archetype - archetype-learn</name>
</project>
34.2.2. 模版数据
接着是模板数据, 包括pom和代码
package $com.xiaoyu.archetype.learn;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
package $com.xiaoyu.archetype.learn;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 设置artifactId和groupId作为变量 ( ${artifactId} / ${groupId} )。这两个变量都将在archetype:generate从命令行运行时被初始化 -->
<groupId>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${version}</version>
<packaging>jar</packaging>
<name>${artifactId}</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
34.2.3. 描述符文件
最重要的一步: 编辑描述符信息
<archetype xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype/1.0.0 http://maven.apache.org/xsd/archetype-1.0.0.xsd">
<id>archetype-learn</id><!-- 应该和archetype的artifactId相同 -->
<!--
<sources> = src/main/java
<resources> = src/main/resources
<testSources> = src/test/java
<testResources> = src/test/resources
<siteResources> = src/site
1. <sources> 和<testSources>都能包含<source>元素来指定源文件。
2. <resources>和<testResources>和<siteResources>能包含<resource>元素来指定资源文件
3. 比如src/main/webapp目录放在<resource>标签中
-->
<sources>
<source>src/main/java/App.java</source>
</sources>
<testSources>
<source>src/test/java/AppTest.java</source>
</testSources>
<!-- <allowPartial>true</allowPartial>标签是可选的,它使得archetype:generate可以在一个已存在的工程中运行 -->
<!-- <allowPartial>true</allowPartial> -->
</archetype>
34.2.4. 使用
mvn install
安装这个archetype, mvn archetype:generate -D archetypeGroupId=com.xiaoyu.archetype -D archetypeArtifactId=archetype-learn -D archetypeVersion=0.0.1-SNAPSHOT -D groupId=com.xiaoyu.archetype.generate -D artifactId=demo
使用这个archetype
如果直接使用mvn archetype:generate
而不带参数, 回出来一个列表供选择, 如何让自定义的archetype出现在这个列表呢? 见archetype-catalog
34.3. archetype-catalog
参考: http://maven.apache.org/archetype/maven-archetype-plugin/specification/archetype-catalog.html
maven-archetype-plugin会从这些位置读取catalog:
internal
: 插件内置的cataloglocal
(插件默认): 本地的默认位置, 位于~/.m2/archetype-catalog.xml
(默认不存在)remote
(插件默认): maven中央仓库的file://
: 指定本地任意位置的cataloghttp://
: 远程的catalog
如何读取这些catalog?
mvn archetype:generate -D archetypeCatalog=file:// /xxx/xxx/archetype-catalog.xml,local
指定多个catalog
典型的archetype-catalog.xml
长这样:
<?xml version="1.0" encoding="UTF-8"?>
<archetype-catalog xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0 http://maven.apache.org/xsd/archetype-catalog-1.0.0.xsd">
<archetypes>
<archetype>
<groupId>org.appfuse.archetypes</groupId><!-- 必须 -->
<artifactId>appfuse-basic-jsf</artifactId><!-- 必须 -->
<version>2.0</version><!-- 必须 -->
<repository>http://static.appfuse.org/releases</repository><!-- 可选, 如果省略, 会在archetype-catalog.xml所在的仓库找archetype -->
<description>AppFuse archetype for creating a web application with Hibernate, Spring and JSF</description><!-- 可选 -->
</archetype>
...
</archetypes>
</archetype-catalog>