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

Maven · 使用 Maven 构建 Web应用

  • 1️⃣ Web 项目的目录结构
  • 2️⃣ account-service
      • 2.1 account-service的 POM
      • 2.2 account-service 的主代码
  • 3️⃣ account-web
      • 3.1 account-web 的POM
      • 3.2 account-web 的主代码
  • 4️⃣ 使用 jetty-maven-plugin 进行测试
  • 5️⃣ 使用 Cargo 实现自动化部署
      • 5.1 部署至本地 Web 容器
      • 5.2 部署至远程 Web 容器
  • 🌾 总结

到目前为止,讨论的只有打包类型为 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的安装到任务创建 ~》

继续阅读下一篇(点击跳转)
《》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/153750.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【文件上传】01ctfer 文件上传获取flag

1.1漏洞描述 漏洞名称01ctfer 文件上传漏洞类型文件上传漏洞等级⭐⭐⭐漏洞环境docker攻击方式 1.2漏洞等级 高危 1.3影响版本 暂无 1.4漏洞复现 1.4.1.基础环境 靶场docker工具BurpSuite 1.4.2.环境搭建 1.创建docker-compose.yml文件 version: 3.2 services: upload: …

Banana Pi BPI-W3之RK3588安装Qt+opencv+采集摄像头画面.

场景&#xff1a;在Banana Pi BPI-W3 RK3588上做qt开发工作RK3588安装Qtopencv采集摄像头画面 2. 环境介绍 硬件环境&#xff1a; Banana Pi BPI-W3RK3588开发板、MIPI-CSI摄像头( ArmSoM官方配件 )软件版本&#xff1a; OS&#xff1a;ArmSoM-W3 Debian11 QT&#xff1a;QT5…

Compose学习之绘制速度表盘

内心想法XX compose已经发布好久了&#xff0c;还没有用过compose写过UI&#xff0c;之前只是在官网上了解过&#xff0c;看着这可组合函数嵌套&#xff0c;我就脑袋大&#xff0c;更Flutter一个德行&#xff0c;我的内心是抵触的&#xff0c;还是觉得用XML写香&#xff0c;抱…

广州华锐互动:办税服务厅税务登记VR仿真体验让税务办理更加灵活高效

在数字化世界的今天&#xff0c;我们正在见证各种业务过程的转型&#xff0c;而税务办理也不例外。最近&#xff0c;一种全新的交互方式正在改变我们处理税务的方式&#xff1a;虚拟现实&#xff08;VR&#xff09;。 首先&#xff0c;用户需要戴上虚拟现实头显&#xff0c;然后…

Antv/G2 柱状图添加自定义点击事件

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>柱状图点击事件</title></head><body><div id"container" /><script src"https://gw.alipayobjects.com/os/lib/antv/g2/4.2.8/…

每日一练 | 华为认证真题练习Day131

1、某台路由器输出信息如下&#xff0c;下列说法正确的有&#xff1f;&#xff08;多选&#xff09; A. 本路由器是DR B. 路由器Router ID为10.0.1.1 C. 路由器Router ID为10.0.2.2 D. 本路由器的接口地址为10.0.12.2 2、以下那条命令可以开启路由器接口的DHCP中继功能&…

TS7031: Binding element ‘role‘ implicitly has an ‘any‘ type.

文章 前言错误场景问题分析解决方案后言 前言 ✨✨ 他们是天生勇敢的开发者&#xff0c;我们创造bug&#xff0c;传播bug&#xff0c;毫不留情地消灭bug&#xff0c;在这个过程中我们创造了很多bug以供娱乐。 前端bug这里是博主总结的一些前端的bug以及解决方案&#xff0c;感兴…

就近值 reduce用法 时间戳与时间点对比循环查找

后台接口返回的13为时间戳 需要与数据data的time做对比&#xff0c;查找出最近的值 data的数据结构如下&#xff1a; 将&#xff1a;改为空格&#xff0c;变成数字之间的对比 //查找最近的时间getNearestTime(timestamp, data) {let date new Date(timestamp)let h date.ge…

windows c++开发

一 安装 离线MSDN MSDN:microsoft developer network ,微软向开发人员提供的一套帮助系统。 运行vs 2017 -》运行 vs studio installer ->点击修改-》单个组件-》代码工具-》help viewer-> 安装完后&#xff0c;启动vs 在“帮助”菜单&#xff0c;“设置帮助首选项…

使用Kohya_ss训练Stable Diffusion Lora

Stable Diffusion模型微调方法 Stable Diffusion主要有 4 种方式&#xff1a;Dreambooth, LoRA, Textual Inversion, Hypernetworks。 Textual Inversion &#xff08;也称为 Embedding&#xff09;&#xff0c;它实际上并没有修改原始的 Diffusion 模型&#xff0c; 而是通过…

Sonar生成PDF错误Can‘t get Compute Engine task status.Retry..... HTTP error: 401

报错及修改&#xff1a; 报错&#xff1a;INFO: Can’t get Compute Engine task status.Retry… org.sonarqube.ws.connectors.ConnectionException: HTTP error: 401, msg: , query: org.apache.commons.httpclient.methods.GetMethod7a021f49 ERROR: Problem generating PD…

Python-pptx教程之二操作已有PPT模板文件

文章目录 简单的案例找到要修改的元素修改幻灯片中的文本代码使用示例 修改幻灯片的图片代码使用示例 删除幻灯片代码使用示例 获取PPT中所有的文本内容获取PPT中所有的图片总结 在上一篇中我们已经学会了如何从零开始生成PPT文件&#xff0c;从零开始生成较为复杂的PPT是非常消…

基于边缘智能网关的冬季管网智能监测应用

随着我国北方全面进入到冬季&#xff0c;多日以来严寒、降雪天气频发&#xff0c;民生基础设施也迎来冬季考验。尤其是民众生活仰赖的水、电、气管网&#xff0c;面临极端冰雪天气时易存在各种风险&#xff0c;包括管道水/气泄露损耗、低温冻裂、积雪压塌压损、冻结受阻等。 针…

前端常用的几种加密方法

文章目录 前端常用的几种加密方法md5 加密(不可逆)base64 位加密(可加密可解密)RSA 加密(公钥加密&#xff0c;私钥解密)AES 加密(需要密钥才能解密)CryptoJS 常用的加密方式--demo ✒️总结 前端常用的几种加密方法 在信息安全越来越受重视的今天&#xff0c;JS 安全一直是前端…

无需API实现MySQL与巨量引擎的对接

通过数环通&#xff0c;您可以使用不到几分钟的时间即可实现MySQL与巨量引擎的对接与集成&#xff0c;从而高效实现工作流程自动化&#xff0c;降本增效&#xff01; 1.产品介绍 巨量引擎是字节跳动旗下的营销服务品牌&#xff0c;它整合了字节跳动旗下的产品及海量内容&#…

asp.net购物网站源码-系统销售毕业设计

采用典型的三层架构进行开发&#xff0c;包含购物车、登陆注册、个人中心、留言板、新闻系统&#xff0c;前台页面、后台管理等主要技术&#xff1a;基于asp.net架构和sql server数据库 功能模块&#xff1a; 本源码是一个三层购物网站源码&#xff0c;功能齐全&#xff0c;界面…

【PyQt小知识 - 4】:QGroupBox分组框控件 - 边框和标题设置

QGroupBox QGroupBox 是 PyQt 中的一个小部件&#xff0c;用于创建一个带有标题的组框。 可以使用 QGroupBox 将相关控件分组并添加一个标题。 以下是一个使用 QGroupBox 的示例代码&#xff08;示例一&#xff09;&#xff1a; from PyQt5.QtWidgets import * import sysa…

Linux_虚拟机常用目录汇总

根目录&#xff08;cd /&#xff09;&#xff1a;/ 表示根目录&#xff0c;cd和 / 之间有个空格&#xff01; 用户目录&#xff08;cd ~&#xff09;&#xff1a;~ 表示用户目录&#xff0c;也称为家目录。cd 和 ~ 之间有个空格&#xff01; 当前路径&#xff1a;执行 pwd 指令…

linux运行java程序

这个帖子实现的是linux上运行java代码 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 事情发生的原因是博洋需要知道海外城市的数量&#xff0c;我一开始准备将全量数据拉取到本地&#xff0c;用代码遍历一遍。但是打包好全量数据&…

SystemVerilog学习 (9)——随机化

目录 一、概述 二、随机化 2.1、如何简单地产生一个随机数 2.1.1 利用系统函数产生随机数 2.1.2 urandom() 2.2、什么需要随机化 2.3、随机约束 2.3.1 rand 和 randc 2.3.2 随机约束的使用 2.3.3 约束块 三、总结 一、概述 随着设计变得越来越大,要产生一个完整的激…