【Maven教程】(十一):使用 Maven 构建 Web应用 —— 使用 jetty-maven-plugin 进行测试、使用 Cargo 实现自动化部署~

到目前为止,讨论的只有打包类型为 JAR 或者 POM 的 Maven 项目。但在现今的互联网时代,我们创建的大部分应用程序都是Web 应用,在Java 的世界中, Web 项目的标准打包方式是WAR 。 因此本文介绍一个WAR 模块 ——account-web。在介绍该模块之前,本文会先实现 account-service 。 此外,还介绍如何借助 jetty-maven-plugin 来快速开发和测试Web 模块,以及使用Cargo 实现 Web 项目的自动化部署。

在这里插入图片描述


1️⃣ Web 项目的目录结构

我们都知道,基于Java 的 Web 应用,其标准的打包方式是WAR 。WAR 与 JAR 类似, 只不过它可以包含更多的内容,如JSP 文件、Servlet、Java 类、web.xml 配置文件、依赖 JAR 包、静态 web 资源 ( 如 HTML、CSS、JavaScript 文件)等。一个典型的 WAR 文件会有如下目录结构:

- war/
	+ META-INF/
	+ WEB-INF/
	| + classes/
	| | + ServletA.class
	| | + config.properties
	| | + ...
	| | 
	| + lib/
	| | + dom4j-1.4.1.jar
	| | + mail-1.4.1.jar
	| | + ...
	| |
	| + web.xml
	|
	+ img/
	|
	+ css/
	|
	+ index.html
	+ sample.jsp

一个WAR 包下至少包含两个子目录: META-INF 和 WEB-INF 。前者包含了一些打包元数据信息,我们一般不去关心;后者是WAR 包 的核心,WEB-INF 下必须包含一个Web 资源表述文件 web.xml, 它的子目录 classes 包含所有该Web 项目的类,而另一个子目录 lib 则包含所有该Web 项目的依赖JAR包 ,classes和 lib目录都会在运行的时候被加入到 Classpath 中 。 除了META-INF 和 WEB-INF 外,一般的 WAR 包都会包含很多 Web 资源,例如你往往可以在 WAR 包的根目录下看到很多html 或者 jsp 文件。此外,还能看到一些文件夹如 img、css和js, 它们会包含对应的文件供页面使用。

同任何其他 Maven项目一样,Maven 对 Web 项目的布局结构也有一个通用的约定。不过首先要记住的是,用户必须为Web 项目显式指定打包方式为 war, 如代码所示。

<project>
	<groupId>com.xiaoshan.mvnbook</groupId>
	<artifactId>sample-war</artifactId>
	<packaging>war</packaging>
	<version>1.0-SNAPSHOT</version>
</project>

如果不显式地指定packaging,Maven 会使用默认的 jar 打包方式,从而导致无法正确打包 Web 项目,Web项目的类及资源文件同一般JAR 项目一样,默认位置都是 src/main/java/src/main/resources, 测试类及测试资源文件的默认位置是 src/test/java/src/test/resources/ 。 Web 项目比较特殊的地方在于:它还有一个 Web 资源目录,其默认位置是 src/main/webapp/ 。一个典型的 Web 项目的 Maven 目录结构如下:

+ project
	|
	+ pom.xml
	|
	+ src/
		+ main/
		| + java/
		| | + ServletA.java
		| | + ...
		| |
		| + resources/
		| | + config.properties
		| | + ...
		| |
		| + webapp/
		| 	+ WEB-INF/
		|		| + web.xml
		|		|
		|		+ img/
		|		|
		|		+ css/
		|		|
		|		+ js/
		|		+
		|		+ index.html
		| 	+ sample.jsp
		|		
		+ test/
			+ java/
			+ resources/

在 src/main/webapp/目录下,必须包含一个子目录 WEB-INF, 该子目录还必须要包含 web.xml 文件 。src/main/webapp 目录下的其他文件和目录包括 html、jsp、css、JavaScript 等,它们与WAR 包中的Web 资源完全一致。

在使用Maven 创建Web 项目之前,必须首先理解这种Maven 项目结构和WAR 包结构的对应关系。有一点需要注意的是,WAR 包中有一个 lib 目录包含所有依赖JAR 包,但Maven项目结构中没有这样一个目录,这是因为依赖都配置在POM中,Maven 在用WAR方式打包的时候会根据 POM 的配置从本地仓库复制相应的JAR 文件。


2️⃣ account-service

本节将完成背景案例项目,读者可以回顾前面的文章,除了之前实现的 account-email 、account-persist 和 account-captcha 之外,该项目还包括 account-service 和 account-web 两个模块。 其中, account-service 用来封装底层三个模块的细节,并对外提供简单的接口,而 account-web仅包含一些涉及Web 的相关内容,如 Servlet 和 JSP 等。

2.1 account-service的 POM

account-service 用来封装 account-email 、account-persist 和 account-captcha 三个模块的细节,因此它肯定需要依赖这三个模块。 account-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/maven-v4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>com.xiaoshan.mvnbook.account
		<artifactId>account-parent</artifactId>
		<version>1.0.0-SNAPSHOT</version>
	</parent>

	<artifactId>account-service</artifactId>
	<name>Account Service</name>
	<properties>
		<greenmail.version>1.3.1b</greenmail.version>
	</properties>
	
	<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>
			<version>${greenmail.version}</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	
	<build>
		<testResources>
			<testResource>
				<directory>src/test/resources</directory>
				<filtering>true</filtering>
			</testResource>
		</testResources>
	</build>
</project>

与其他模块一样,account-service 继承自 account-parent, 它依赖于 account-email 、account-pernsist 和account-captcha三个模块。由于是同一项目中的其他模块, groupld 和 version都完全一致,因此可以使用Maven属性 ${project.groupId} 和 ${project.vertion} 进行替换,这样可以在升级项目版本的时候减少更改的数量。项目的其他配置如 junit 和 greenmail 依赖,以及测试资源目录过滤配置,都是为了单元测试。前面的文章已经介绍过,这里不再赞述。

2.2 account-service 的主代码

account-service 的目的是封装下层细节,对外暴露尽可能简单的接口。先看一下这个接 口是怎样的,见代码:

package com.xiaoshan.mvnbook.account.service;

public interface AccountService
{
	String generateCaptchaKey() throws AccountServiceException;

 	byte[] generateCaptchaImage(String captchaKey) throws AccountServiceException;
 	
	void signup(SignUpRequest signUpRequest) throws AccountServiceException;
	
	void activate(String activationNumber) throws AccountServiceException;
	
	void login(String id, string password) throws AccountServiceException;
}

正如介绍的那样,该接口提供5个方法。generateCaptchaKey() 用来生成一个验证码的唯一标识符。generateCaptchalmage() 根据这个标识符生成验证码图片,图片以字节流的方式返回。用户需要使用signUp() 方法进行注册,注册信息使用SignUpRequest 进行封装,这个SignUpRequest 类是一个简单的 POJO, 它包含了注册 ID、email、用户名、密码、验证码标识、验证码值等信息。注册成功之后,用户会得到一个激活链接,该链接包含了一个激活码,这个时候用户需要使用 activate() 方法并传入激活码以激活账户。最后,login() 方法用来登录。

下面来看一下该接口的实现类 AccountServicelmpl.java 。 首先它需要使用3个底层模块的服务,如代码所示。

public class AccountServiceImpl implements AccountService {
	private AccountPersistService accountPersistService;
	private AccountEmailService accountEmailService;
	private AccountCaptchaService accountCaptchaService;
	
	public AccountPersistService getAccountPersistService()
	{
		return accountPersistService;
	}
	
	public void setAccountPersistService(AccountPersistService accountPersistService)
	{
		this.accountPersistService=accountPersistService;
	}
	...
}

三个私有变量来自 account-persist 、account-email 和 account-captcha 模块,它们都有各自的 get() 和 set() 方法,并且通过 Spring 注入。
AccountServicelmpl.java 借助 accountCaptchaService 实现验证码的标识符生成及验证码图片生成,如代码清单所示。

public byte[] generateCaptchaImage(String captchaKey) throws AccountServiceException
{	
	try{
		return accountCaptchaService.generateCaptchaImage(captchaKey);
	}
	catch(AccountCaptchaException e)
	{
		throw new AccountServiceException("Unable to generate Captcha Image.",e); 	
	}
}

public String generateCaptchaKey() throws AccountServiceException
{
	try
	{
		return accountCaptchaService.generateCaptchaKey();
	}
	catch(AccountCaptchaException e)
	{
		throw new AccountServiceException("Unable to generate Captcha key.",e);
	}
}	

稍微复杂一点的是 signUp() 方法的实现,见代码:

private Map<String,String> activationMap = new HashMap<String,String>();

public void signUp(signUpRequest signUpRequest) throws AccountServiceException
{
	try{
		if(!signUpRequest.getPassword().equals(signUpRequest.getConfirmPassword()))
		{
			throw new AccountServiceException("2 passwords do not match.");
		}
		if(!accountCaptchaService.validateCaptcha(signUpRequest.getCaptchaKey(), signUpRequest.getCaptchaValue()))
		{
			throw new AccountServiceException("Incorrect Captcha.");
		}
	
		Account account = new Account();
		account.setId(signUpRequest.getId());
		account.setEmail(signUpRequest.getEmail());
		account.setName(signUpRequest.getName());
		account.setPassword(signUpRequest.getPassword());
		account.setActivated(false);

		accountPersistService.createAccount(account);
		String activationId = RandomGenerator.getRandomString();
		activationMap.put(activationId ,account.getId());
		String link = signUpRequest.getActivateServiceUrl().endsWith("/") ? signUpRequest.getActivateServiceUrl() + activationId : signUpRequest.getActivateServiceUrl() + "?key=" + activationId;
		accountEmailService.sendMail(account.getEmail(),"Please Activate Your Account",link);
	}
	catch(AccountCaptchaException e){
		throw new AccountServiceException("Unable to validate captcha.",e); 
	}
	catch(AccountPersistException e){
		throw new AccountServiceException("Unable to create account.",e);
	}
	catch(AccountEmailException e){
		throw new AccountServiceException("Unable to send actiavtion mail.",e);
	}
}

signUp() 方法首先检查请求中的两个密码是否一致,接着使用 accountCaptchaService 检查验证码,下一步使用请求中的用户信息实例化一个 Account 对象,并使用 accountPersistService将用户信息保存。下一步是生成一个随机的激活码并保存在临时的 activateMap 中, 然后基于该激活码和请求中的服务器 URL创建一个激活链接,并使用 accountEmailService 将该链接发送给用户。如果其中任何一步发生异常, signUp() 方法会创建一个一致的 AccountServiceExepetion 对象,提供并抛出对应的异常提示信息。

最后再看一下相对简单的 activate() 和 login() 方法,见代码:

public void activate(String activationId) throws AccountServiceException
{
	string accountId = activationMap.get(activationId);
	if(accountId ==null){
		throw new AccountServiceException("Invalid account activation ID.");
	}
	try{
		Account account = accountPersistService.readAccount(accountId);
		account.setActivated(true);
		accountPersistService.updateAccount(account);
	}
	catch(AccountPersistException e){
		throw new AccountServiceExceptiont("Unable to activate account."); 
	}
}
public void login(String id,String password) throws AccountServiceException
{
	try{
		Account account=accountPersistService.readAccount(id);
		if(account == null)
		{
			throw new AccountServiceException("Account does not exist.");
		}
		if(!account.isActivated())
		{
			throw new AccountServiceException("Account is disabled.");
		}
		if(!account.getPassword().equals(password))
		{
			throw new AccountServiceException("Incorrect password.");
		}
	}
	catch(AccountPersistException e){
		throw new AccountServiceException("Unable to log in.",e);
	}
}	

activate() 方法仅仅是简单根据激活码从临时的 activationMap 中寻找对应的用户 ID, 如 果找到就更新账户状态为激活。login() 方法则是根据ID 读取用户信息,检查其是否为激 活,并比对密码,如果有任何错误则抛出异常。

除了上述代码之外, account-service 还包括一些 Spring 配置文件和单元测试代码,这里就不再详细介绍。

3️⃣ account-web

account-web 是本maven系列背景案例中唯一的 Web 模块,旨在用该模块来阐述如何使用 Maven 来构建一个Maven项目。由于account-service已经封装了所有下层细节,account-web 只需要在此基础上提供一些Web 页面,并使用简单Servlet 与后台实现交互控制。大家将会看到一个具体Web 项目的 POM 是怎样的,也将能体会到让Web 模块尽可能简洁带来的好处。

3.1 account-web 的POM

除了使用打包方式 war之外,Web 项目的POM 与一般项目并没多大的区别。account- web的 POM 代码见代码:

<?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"
	xmins="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.xiaoshan.mvnbook.account</groupId>
		<artifactId>account-parent</artifactId>
		<version>1.0.0-SNAPSHOT</version>
	</parent>
	<artifactId>account-web</artifactId>
	<packaging>war</packaging>
	<name>Account Web</name>

	<dependencies>
		<dependency>
			<groupId>${project.groupId}</groupId>
			<artifactId>account-service</artifactId>
			<version>${project.version}</version>
		</dependency>
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>servlet-api</artifactId>
			<version>2.4</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>javax.servlet.jsp</groupId>
			<artifactId>jsp-api</artifactId>
			<version>2.0</version>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
		</dependency>
	</dependencies>
</project>

如上述代码所示,account-web 的 packaging 元素值为 war, 表示这是一个Web 项目,需要以 war 方式进行打包。 account-web 依赖于 servlet-api 和 jsp-api 这两个几乎所有 Web 项目都要依赖的包,它们为 servlet 和 jsp的编写提供支持。需要注意的是。这两个依赖的范围是 provided, 表示它们最终不会被打包至war 文件中,这是因为几乎所有 Web 容器都会提供这两个类库,如果 war包中重复出现,就会导致潜在的依赖冲突问题。 account-web 还依赖于 account-service 和 spring-web, 其中前者为 Web 应用提供底层支持,后者为 Web 应用提供 Spring 的集成支持。

在 一 些 Web 项目中可能会看到 finalName 元素的配置。该元素用来标识项目生成
的主构件的名称。该元素的默认值已在超级 POM 中设定值为 p r o j e c t a r t i f a c t l d − {project artifactld}- projectartifactld {project.version}。因此代码对应的主构件名称为 account-web-1.0.0-SNAPSHOT.war
。不过,这样的名称显然不利于部署,不管是测试环境还是最终产品环境,我们都不想在访问页面的时候输入冗长的地址,因此我们会需要名字更为简洁的 war 包。这时可以如下所示配置 finalName 元素:

<finalName>account</finalName>

经此配置后,项目生成的 war 包名称就会成为 account.war, 更方便部署。

3.2 account-web 的主代码

account-web 的主代码包含了2个JSP 页面和 4个Servlet, 它们分别为:

  • signup.jsp: 账户注册页面。
  • login.jsp: 账户登录页面。
  • CaptchalmageServlet: 用来生成验证码图片的Servlet。
  • LoginServlet: 处理账户注册请求的Servlet。
  • ActivateServlet: 处理账户激活的 Servlet。
  • LoginServlet: 处理账户登录的 Servlet。

Servlet 的配置可以从 web.xml 中获得,该文件位于项目的 src/main/webapp/WEB-INF/ 目录。其内容见代码:

<!DOCTYPE web-app PUBLIC
"-//Sun Microsvstems.Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_23.dtd">
<web-app>
	<display-name>Sample Maven Project:Account Service</display-name>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			classpath:/account-persist.xm1
			classpath:/account-captcha.xml
			classpath:/account-email.xml
			classpath:/account-service.xml
		</param-value>
	</context-param>
	<servlet>
		<servlet-name>CaptchaImageServlet</servlet-name>
		<gervlet-class >com.xiaoshan.mvnbook.account.web.CaptchaImageServlet </servlet-class >
	</servlet>
	<servlet>
		<servlet-name>SignUpServlet</servlet-name>
		<servlet-class>com.xiaoshan.mvnbook.account.web.SignUpServlet</servlet-class >
	</servlet>
	<servlet>
		<servlet-name>ActivateServlet</servlet-name>
		<servlet-class>com.xiaoshan.mvnbook.account.web.ActivateServlet</servlet-class>
	</servlet>
	<servlet>
		<servlet-name>LoginServlet</servlet-name>
		<servlet-class>com.xiaoshan.mvnbook.account.web.LoginServlet</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>/signup</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>ActivateServlet</servlet-name>
		<url-pattern>/activate</url-pattern>
	</servlet-mapping>
	<servlet-mapping>
		<servlet-name>LoginServlet</servlet-name>
		<url-pattern>/login</url-pattern>
	</servlet-mapping>
</web-app>                    

web.xml 首先配置了该 Web项目的显示名称,接着是一个名为 ContextLoaderListener 的ServletListener。该listener 来自 springweb, 它用来为 Web项目启动 Spring的 IoC容器,从而 实现Bean的注入。名为 contextConfigLocation 的 context-param 则用来指定 Spring 配置文件的位置。这里的值是四个模块的 Spring配置XML 文件,例如 classpath://account-persist.xml 表 示从classpath 的根路径读取名为 account-persist.xml 的文件。我们知道 account-persist.xml 文件在 account-persist 模块打包后的根路径下,这一 JAR 文件通过依赖的方式被引入到 accoumt-web 的 classpath下 。
weh.xml 中的其余部分是Servlet, 包括各个 Servlet 的名称、类名以及对应的 URL 模式。
下面来看一个位于 src/main/webapp/ 目录的 signup.jsp 文件,该文件用来呈现账户注册 页面。其内容如代码:

<% @ page contentType="text/html;charset=UTF-8" language="java" %>
<% @ page import="com.xiaoshan.mvnbook.account.service.*,
	org.springframework.context.ApplicationContext,
	org.springframework.web.context.support.WebApplicationContextUtils"%> <html>
<head>
<style type="text/css">
...
</tyle>
</head>

<body>
<% ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
	AccountService accountervice =(AccountService)context.getBean("accountService");
	String captchaKey = accountervice.generateCaptchaKey();
%>
<div class="text-field">
<h2>注册新账户</h2>
<form name="signup" action="signup" method="post">
	<label>账户ID:</label><input type="text" name="id"></input><br/>
	<label>Email:</label><input type="text" name="email"></input><br/>
	<label>显示名称:</label><input type="text" name="name"></input><br/>
	<label>密码:</label><input type="password" name="password"></input><br/>
	<label>确认密码:</label><input type="password" name="confirm_password"></input><br/>
	<label>验证码:</label><input type="text" name="captcha_value"></input>
	<input type="hidden" name="captcha_key" value="<% =captchaKey %>" />
	<img src="<% =request.getContextPath() %>/captcha_image?key=<% =captchaKey % >"/>
	<br/>
	<button>确认并提交</button>
</form>
</div>
</body>
</html>

该 JSP 的主题是一个 name 为 signup 的 HTML FORM, 其中包含了ID、Email、 名称、密码等字段,这与一般的 HTML内容并无差别。不同的地方在于,该JSP 文件引入了 Spring 的 ApplicationContext类,并且用此类加载后台的 accountService, 然后使用 accountService 先生成一个验证码的key, 再在 FORM 中使用该key 调用 captcha_image 对应的Servlet 生成其标识的验证码图片。需要注意的是,上述代码中略去了css 片段。

上述 JSP中使用到了/captcha_image 这一资源获取验证码图片。根据 web.xml, 我们知道该资源对应了 CaptchalmageServlet。 下面看一下它的代码,见代码:

package com.xiaoshan.mvnbook.account.web;
import java.io.IOException;
import ..

public class CaptchaImageServlet extends HttpServlet{
	private ApplicationContext context;
	private static final long serialVersionUID=5274323889605521606L;

	@override
	public void init() throws ServletException{
		super.init();
		context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
	}

	public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
		String key=request.getParameter("key");
		if(key==null || key.length()==0)
		{
			response.sendError(400,"No Captcha Key Found");
		}else{
			AccountService service=(AccountService)context.getBean("accountService");
			try{
				response.setContentType("image/jpeg");
				OutputStream out =response.getOutputStream();
				out.write(service.generateCaptchaImage(key));
				out.close();
			}catch(AccountServiceException e){
				response.sendError(404,e.getMessage());
			}
		}
	}
}

CaptchalmageServlet在 init() 方法中初始化 Spring的 ApplicationContext, 这一context用来获取SpringBean。Servlet的 doGet()方法中首先检查key参数,如果为空,则返回HTTP400 错误,标识客户端的请求不合法;如果不为空,则载入 AccountService实例。该类的 generateCaptchalmage() 方法能够产生一个验证码图片的字节流,我们将其设置成 image/jpeg 格式,并写入到 Servlet相应的输出流中,客户端就能得到验证码图片。

代码中FROM的提交目标是signup, 其对应了SignUpServlet。其内容如代码:

public class signUpServlet extends HttpServlet{
	private static final long serialVersionUID=4784742296013868199L;
	private ApplicationContext context;
	
	@Override
	public void init() throws ServletException{
		super.init();
		context = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
	}
	@Override
	protected void doPost(HttpServletRequest req,HttpServletResponse resp) throws ServletException, IOException
	{
		String id =req.getParameter("id");
		String email =req.getParameter("email");
		String name =req.getParameter("name");
		String password =req.getParameter("password");
		String confirmPassword=req.getParameter("confirm_password");
		String captchaValue =req.getParameter("captcha_value");
		if(id == null || id.length()==0 || email==null || email.length()==0 || name ==null || name.length()==0 || password ==null || password.length()==0 || confirmPassword.length()==0 || captchaKey == null || captchaKey.length()==0 || captchaValue ==null || captchaValue.length()==0)
		{
			resp.sendError(400,"Parameter Incomplete.");
		return;
		}
		AccountService service=(AccountService)context.getBean("accountService");
		SignUpRequest request =new SignUpRequest();
		request.setId(id);
		request.setEmail(email);
		request.setName(name);
		request.setPassword(password);
		request.setConfirmPassword(confirmPassword);
		request.setCaptchaKey(captchaKey);
		request.setCaptchaValue(captchaValue);
		request.setActivateServiceUrl(getServletContext().getRealPath("/")+"activate");
		try{
			service.signUp(request);
			resp.getWriter().print("Account is created,please check your mail box for activation link.");
		}catch(AccountServiceException e){
			resp.sendError(400,e.getMessage());
		return;
		}
	}
}

SignUpServlet 的 doPost() 接受客户端的 HTTP POST请求,首先它读取请求中的 id、name、email等参数,然后验证这些参数的值是否为空,如果验证正确,则初始化一个SignUpRequest 实例,其包含了注册账户所需要的各类数据。其中的 activateServiceUrl 表示服务应该基于什么地址发送账户激活链接邮件,这里的值是与signup 平行的 activate地址,这正是 ActivationServlet 的地址。SignUpServlet使用AccountService注册账户,所有的细节都已经封装在AccountService中,如果注册成功,服务器打印一条简单的提示信息。
上面介绍了一个JSP和两个Servlet,它们都非常简单。鉴于篇幅的原因,这里就不再详细解释另外几个JSP及Servlet。


4️⃣ 使用 jetty-maven-plugin 进行测试

在进行Web 开发的时候,我们总是无法避免打开浏览器对应用进行测试,比如为了验证程序功能、验证页面布局,尤其是一些与页面相关的特性,手动部署到Web 容器进行测 试似乎是唯一 的方法。近年来出现了很多自动化的Web 测试技术如 Selenium, 它能够录制 Web 操作,生成各种语言脚本,然后自动重复这些操作以进行测试。应该说,这类技术方法是未来的趋势,但无论如何,手动的、亲眼比对验证的测试是无法被完全替代的。测试 Web 页面的做法通常是将项日打包并部署到 Web 容器中,本节介绍如何使用jetty-maven-plugin, 以使这些步骤更为便捷。

在介绍 jetty-maven-plugin 之前,笔者要强调一点,虽然手动的 Web 页面测试是必不可 少的,但这种方法绝不应该被滥用。现实中常见的情况是,很多程序员即使修改了一些较底层的代码(如数据库访问、业务逻辑), 都会习惯性地打开浏览器测试整个应用,这往往是没有必要的。可以用单元测试覆盖的代码就不应该依赖于Web 页面测试,且不说页面测试更加耗时耗力,这种方式还无法自动化,更别提重复性了。因此 Web 页面测试应该仅限于页面的层次,例如JSP 、CSS 、JavaScript 的修改,其他代码修改(如数据访问),请编写单元测试。

传统的 Web 测试方法要求我们编译、测试、打包及部署,这往往会消耗数10秒至数分钟的时间,jetty-maven-plugin 能够帮助我们节省时间,它能够周期性地检查项目内容,发现变更后自动更新到内置的Jetty Weh 容器中。换句话说,它帮我们省去了打包和部署的步 骤 。jetty-maven-plugin 默认就很好地支持了Maven 的项目目录结构。在通常情况下,我们只需要直接在IDE 中修改源码, IDE 能够执行自动编译,jetty-maven-plugin 发现编译后的文件变化后,自动将其更新到 Jetty 容器,这时就可以直接测试Web 页面了。

使用jetty-maven-plugin 十分简单。指定该插件的坐标,并且稍加配置即可,见代码:

<plugin>
	<groupId>org.mortbay.jetty</groupId>
	<artifactId>jetty-maven-plugin</artifactId>
	<version>7.1.6.v20100715</version>
	<configuration>
		<scanIntervalSeconds>10</scanIntervalSeconds>
		<webAppConfig>
			<contextPath>/test</contextPath>
		</webAppConfig>
	</configuration>
</plugin>

jetty-maven-plugin 并不是官方的 Maven 插件,它的 groupld 是 org.mortbay.jetty, 上述 代码中使用了Jetty7 的最新版本。在该插件的配置中,scanlntervalSeconds 顾名思义表示该插件扫描项目变更的时间间隔,这里的配置是每隔10秒。需要注意的是,如果不进行配置, 该元素的默认值是0,表示不扫描,用户也就失去了所谓的自动化热部署的功能。上述代码中 webappConfig 元素下的 contextPath 表示项目部署后的 context path。例如这里的值为/test, 那么用户就可以通过http://hostname:port/test/ 访问该应用。

下一步启动 jetty-maven-plugin。不过在这之前需要对 settings.xml 做个微小的修改。前 面介绍过,默认情况下,只有 org.apache.maven.pluginsorg.codehaus.mojo 两个groupld 下的插件才支持简化的命令行调用,即可以运行 mvn help:system, 但 mvn jetty:run 就不行了。因为 maven-help-plugin 的 groupld 是 org.apache.maven.plugins, 而 jetty-maven-plugin 的 groupld 是 org.mortbay.jetty。为了能在命令行直接运行 mvn jetty:run, 用户需要配置 settings.xml 如下:

<settings>
	<pluginGroups>
		<pluginGroup>org.mortbay.jetty</pluginGroup>
	</pluginGroups>
</settings>

现在可以运行如下命令启动 jetty-maven-plugin:

$mvn jetty:run

jetty-maven-plugin 会启动 Jetty, 并且默认监听本地的8080 端口,并将当前项目部署到 容器中,同时它还会根据用户配置扫描代码改动。

如果希望使用其他端口,可以添加 jetty.port参数。例如:

$mvn jetty:run -Djetty.port=9999

现在就可以打开浏览器通过地址 http://localhost:9999/test/ 测试应用了。要停止Jetty, 只需要在命令行输人 Ctrl +C 即可。

启动 Jetty之后,用户可以在 IDE 中修改各类文件,如JSP、HTML、CSS、JavaScript 甚至是 Java 类。只要不是修改类名、方法名等较大的操作,jetty-maven-plugin 都能够扫描到变更并正确地将变化更新至 Web 容器中,这无疑在很大程度上帮助了用户实现快速开发和 测试。

上面的内容仅仅展示了jetty-maven-plugin 最核心的配置点,如果有需要,还可以自定义 web.xml 的位置、项目 class 文件的位置、web 资源目录的位置等信息。用户还能够以 WAR包的方式部署项目,甚至在 Maven的生命周期中嵌入 jetty-maven-plugin 。例如,先启动Jetty 容器并部署项目,然后执行一些集成测试,最后停止容器。有兴趣进一步研究的读者可以访问该页面: http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin

5️⃣ 使用 Cargo 实现自动化部署

Cargo是一组帮助用户操作 Web 容器的工具,它能够帮助用户实现自动化部署,而且它 几乎支持所有的Web 容器,如 Tomcat 、JBoss 、Jetty 和 Glassfish 等 。Cargo 通过cargo-maven2-plagin 提供了Maven集成 ,Maven 用户可以使用该插件将 Web项目部署到 Web 容器中。虽然 cargo-maven2-plugin 和 jetty-maven-plugin 的功能看起来很相似,但它们的目的是不同的, jetty-maven-plugin 主要用来帮助日常的快速开发和测试,而 cargo-maven2-plugin 主要服务于自动化部署。例如专门的测试人员只需要一条简单的 Maven 命令,就可以构建项目并部署到 Web 容器中,然后进行功能测试。本节以 Tomcat 6为例,介绍如何自动化地将 Web 应用部署至本地或远程 Web 容器中。

5.1 部署至本地 Web 容器

Cargo 支持两种本地部署的方式,分别为 standalone 模式和 existing 模式。在 standalone 模式中 ,Cargo会 从Web 容器的安装目录复制一份配置到用户指定的目录,然后在此基础上 部署应用,每次重新构建的时候,这个目录都会被清空,所有配置被重新生成。而在 existing模式中,用户需要指定现有的Web 容器配置目录,然后 Cargo 会直接使用这些配置并将 应用部署到其对应的位置。代码展示了standalone 模式的配置样例:

<plugin>
	<groupId>org.codehaus.cargo</groupId>
	<artifactId>cargo-maven2-plugin</artifactId>
	<version>1.0</version>
	<configuration>
		<container>
			<containerId>tomcat6x</containerId>
			<home>D:\cmd\apache-tomcat-6.0.29</home>
		</container>
		<configuration>
			<type>standalone</type>
			<home>${project.build.directory}/tomcat6x</home>
		</configuration>
	</configuration>
</plugin>

cargo-maven2-plugin 的 groupld 是 org.codehaus.cargo, 这不属于官方的两个 Maven 插件 groupld, 因此用户需要将其添加到 settings.xml的 pluginGroup 元素中以方便命令行调用。

上述 cargo-maven2-plugin的具体配置包括了 container 和configuration 两个元素,configuration的子元素 type 表示部署的模式(这里是 standalone) 。与之对应的,configuration 的 home 子元素表示复制容器配置到什么位置,这里的值为${project.build.directory}/tomcat6x, 表示构建输出目录,即 target/下的 tomcat6x子目录。container 元素下的 containerld 表 示容器的类型,home元素表示容器的安装目录。基于该配置,Cargo 会从 D:\cmd\apache-tomcat-6.0.29目录下复制配置到当前项目的target/tomcat6x/ 目录下。
现在,要让 Cargo 启动 Tomcat 并部署应用,只需要运行:

$mvn cargo:start

以 account-web 为例,现在就可以直接访问地址的账户注册页面了。
默认情况下 ,Cargo 会让 Web 容器监听8080 端口。可以通过修改 Cargo 的 cargo.servlet.port 属性来改变这一配置,如代码:

<plugin>
	<groupId>org.codehaus.cargo</groupId>
	<artifactId>cargo-maven2-plugin</artifactId>
	<version>1.0</version>
	<configuration>
		<container>
			<containerId>tomcat6x</containerId>
			<home>D:\cmd\apache-tomcat-6.0.29</home>
		</container>
		<configuration>
			<type>standalone</type>
			<home>${project.build.directory}/tomcat6x</home>
			<properties>
				<cargo.servlet.port>8081</cargo.servlet.port>
			</properties>
		</configuration>
	</canfiguration>
</plugin>

要将应用直接部署到现有的Web容器下.需要配置Cargo 使用existing 模式,如代码:

<plugin>
	<groupId>org.codehaus.cargo</groupId>
	<artifactId>cargo-maven2-plugin</artifactId>
	<version>1.0</version>
	<configuration>
		<container>
			<containerId>tomcat6x</containerId>
			<home>D:\cmd\apache-tomcat-6.0.29</home>
		</container>
		<configuration>
			<type>existing</lype>
			<home>D:\cmd\apache-tomcat-6.0.29</home>
		</conflguration>
	</configuration>
</plugin>

上述代码中 configuration元素的 type子元素的值为existing, 而对应的 home 子元素表示 现有的Weh容器目录,基于该配置运行 mvn cargo:start 之后,便能够在Tomcat 的 webapps 子目录看到被部署的Maven项目。

5.2 部署至远程 Web 容器

除了让Cargo直接管理本地Web容器然后部署应用之外,也可以让 Cargo 部署应用至远 程的正在运行的Web容器中。当然,前提是拥有该容器的相应管理员权限。相关配置如代码:

<plugin>
	<groupId>org.codehaus.cargo</groupId>
	<artifactId>cargo-maven2-plugin</artifactld>
	<version>1.0</version>
	<configuration>
		<container>
			<containerId>tomcat6x</containerid>
			<type>remote</type>
		</container>
		<configuration>
			<type>runtime</type>
			<properties>
				<cargo.remote.userrane>admin</cargo.remote.username>
				<cargo.remote.password>admin123</cargo.remote.password>
				<cargo.tomcat.manager.url>http://localhost:9080/manager</cargo.tomcat.manager.url>
			</properties>
		</configuration>
	</configuration>
</plugin>

对于远程部署的方式来说,container 元素的 type 子元素的值必须为 remote 。如果不显式指定,Cargo 会使用默认值 installed, 并寻找对应的容器安装目录或者安装包,对于远程部署方式来说,安装目录或者安装包是不需要的。上述代码中 configuration 的 type 子元素值为 runtime, 表示既不使用独立的容器配置,也不使用本地现有的容器配置,而是依赖于一个已运行的容器。properties 元素用来声明一些容器热部署相关的配置。例如,这里的Tomcat 6 就需要提供用户名、密码以及管理地址。需要注意的是,这部分配置元素对于所有容器来说不是一致的,读者需要查阅对应的Cargo 文档。

有了上述配置后,就可以让 Cargo 部署应用了。运行命令如下:

$mvn cargo:redeploy

如果容器中已经部署了当前应用, Cargo 会先将其卸载,然后再重新部署。
由于自动化部署本身就不是简单的事情,再加上Cargo 要兼容各种不同类型的Web 容器,因此 cargo-maven2-plugin 的相关配置会显得相对复杂,这个时候完善的文档就显得尤为重要。如果想进一步了解 Cargo, 可访问 http://cargo.codehaus.org/Maven2 +plugin


🌾 总结

本文介绍了 用 Maven 管理Web 项目,因此首先讨论了Web 项目的基本结构,然后分析实现了背景案例的最后两个模块: account-service 和 account-web, 其中后者是一个典 型的Web 模块。开发Web 项目的时候,大家往往会使用热部署来实现快速的开发和测试, jetty-maven-plugin 可以帮助实现这一 目标。最后讨论的是自动化部署,这一技术的主角是 Cargo, 有了它,可以让Maven 自动部署应用至本地和远程 Web 容器中。


温习回顾上一篇(点击跳转)
《【Maven教程】(十):使用 Hudson 进行持续集成—— 从Hudson的安装到任务创建 ~》

继续阅读下一篇(点击跳转)
《【Maven教程】(十二):版本管理 ——版本号定义约定及相关概念,自动化版本发布与创建分支,GPG签名 ~》

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小山code

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

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

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

打赏作者

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

抵扣说明:

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

余额充值