下面是一个账号注册服务的account-persist模块。该模块负责账号数据的持久化,以XML的形式保存账户数据,并支持账户的创建、读取、更新、删除等操作。
1.模块的pom.xml
<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.juvenxu.mvnbook.account</groupId>
<artifactId>account-persist</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>Account Persist</name>
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
该POM中配置了一些依赖。其中dom4j用来支持XML操作,之后是spring-framwork的依赖,主要用来支持依赖注入。最后是测试范围的junit依赖,用来支持单元测试。
build元素中包含了一个testResources子元素,为了开启资源过滤。build元素下还包含maven的两个插件配置。MyEclipse在产生项目时会选择编译版本,正是在artifactId为maven-compiler-plugin中定义的。maven的插件可以不用用写groupId,可以在setting.xml中配置<setting><pluginGroups><pluginGroup>使Maven检查其它groupId上的插件仓库元数据。另外这里的resources插件是为了使用UTF-8编码处理资源文件。
2.主代码部分
Acocunt类定义了账户的简单模型,包含一些字段柄提供getter和setter方法
//Account.jaca
package com.juvenxu.mvnbook.account.persist;
public class Account {
private String id;
private String name;
private String email;
private String password;
private boolean activated;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public boolean isActivated() {
return activated;
}
public void setActivated(boolean activated) {
this.activated = activated;
}
}
AccountPersistException是Exception的一个子类,该模块出错时均抛出此错误。
//AccountPersistException.java
package com.juvenxu.mvnbook.account.persist;
@SuppressWarnings("serial")
public class AccountPersistException extends Exception {
public AccountPersistException(String message) {
super(message);
}
public AccountPersistException(String message, Throwable throwable) {
super(message, throwable);
}
}
//AccountPersistService.java
package com.juvenxu.mvnbook.account.persist;
public interface AccountPersistService {
Account createAccount(Account account) throws AccountPersistException;
Account readAccount(String id) throws AccountPersistException;
Account updateAccount(Account account) throws AccountPersistException;
void deleteAccount(String id) throws AccountPersistException;
}
AccountPersistService对应的实现为AccountPersistServiceImpl类,它通过操作XML文件实现账户数据的持久化。首先该类包含两个私有方法:readDocumt()和writeDocument(),然后包含了对接口的实现。
//AccountPersistServiceImpl.java
package com.juvenxu.mvnbook.account.persist;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
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;
public class AccountPersistServiceImpl implements AccountPersistService {
private static final String ELEMENT_ROOT = "account-persist";
private static final String ELEMENT_ACCOUNTS = "accounts";
private static final String ELEMENT_ACCOUNT = "account";
private static final String ELEMENT_ACCOUNT_ID = "id";
private static final String ELEMENT_ACCOUNT_NAME = "name";
private static final String ELEMENT_ACCOUNT_EMAIL = "email";
private static final String ELEMENT_ACCOUNT_PASSWORD = "password";
private static final String ELEMENT_ACCOUNT_ACTIVATED = "activated";
private String file;
private SAXReader reader = new SAXReader();
public String getFile() {
return file;
}
public void setFile(String file) {
this.file = file;
}
public Account createAccount(Account account)
throws AccountPersistException {
Document doc = readDocument();
Element accountsEle = doc.getRootElement().element(ELEMENT_ACCOUNTS);
accountsEle.add(buildAccountElement(account));
writeDocument(doc);
return account;
}
@SuppressWarnings("unchecked")
public void deleteAccount(String id) throws AccountPersistException {
Document doc = readDocument();
Element accountsEle = doc.getRootElement().element(ELEMENT_ACCOUNTS);
for (Element accountEle : (List<Element>) accountsEle.elements()) {
if (accountEle.elementText(ELEMENT_ACCOUNT_ID).equals(id)) {
accountEle.detach();
writeDocument(doc);
return;
}
}
}
@SuppressWarnings("unchecked")
public Account readAccount(String id) throws AccountPersistException {
Document doc = readDocument();
Element accountsEle = doc.getRootElement().element(ELEMENT_ACCOUNTS);
for (Element accountEle : (List<Element>) accountsEle.elements()) {
if (accountEle.elementText(ELEMENT_ACCOUNT_ID).equals(id)) {
return buildAccount(accountEle);
}
}
return null;
}
public Account updateAccount(Account account)
throws AccountPersistException {
if (readAccount(account.getId()) != null) {
deleteAccount(account.getId());
return createAccount(account);
}
return null;
}
private Account buildAccount(Element element) {
Account account = new Account();
account.setId(element.elementText(ELEMENT_ACCOUNT_ID));
account.setName(element.elementText(ELEMENT_ACCOUNT_NAME));
account.setEmail(element.elementText(ELEMENT_ACCOUNT_EMAIL));
account.setPassword(element.elementText(ELEMENT_ACCOUNT_PASSWORD));
account.setActivated(("true".equals(element
.elementText(ELEMENT_ACCOUNT_ACTIVATED)) ? true : false));
return account;
}
private Element buildAccountElement(Account account) {
Element element = DocumentFactory.getInstance().createElement(
ELEMENT_ACCOUNT);
element.addElement(ELEMENT_ACCOUNT_ID).setText(account.getId());
element.addElement(ELEMENT_ACCOUNT_NAME).setText(account.getName());
element.addElement(ELEMENT_ACCOUNT_EMAIL).setText(account.getEmail());
element.addElement(ELEMENT_ACCOUNT_PASSWORD).setText(
account.getPassword());
element.addElement(ELEMENT_ACCOUNT_ACTIVATED).setText(
account.isActivated() ? "true" : "false");
return element;
}
private Document readDocument() throws AccountPersistException {
File dataFile = new File(file);
if (!dataFile.exists()) {
dataFile.getParentFile().mkdirs();
Document doc = DocumentFactory.getInstance().createDocument();
Element rootEle = doc.addElement(ELEMENT_ROOT);
rootEle.addElement(ELEMENT_ACCOUNTS);
writeDocument(doc);
}
try {
return reader.read(new File(file));
} catch (DocumentException e) {
throw new AccountPersistException(
"Unable to read persist data xml", e);
}
}
private void writeDocument(Document doc) throws AccountPersistException {
Writer out = null;
try {
out = new OutputStreamWriter(new FileOutputStream(file), "utf-8");
XMLWriter writer = new XMLWriter(out,
OutputFormat.createPrettyPrint());
writer.write(doc);
} catch (IOException e) {
throw new AccountPersistException(
"Unable to write persist data xml", e);
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
throw new AccountPersistException(
"Unable to close persist data xml writer", e);
}
}
}
}
看看这里的writeDocument()方法。该方法首先使用变量file构建一个文件输入流,file是AccountPersistServiceImpl的一个私有变量,它的值通过SpringFramework注入。得到输入流后,该方法再使用DOM4J创建一个XMLWriter,这里的OutputFomat.createPrettyPrint()用来创建一个带缩进及换行的友好格式。得到XMLWriter后,就调用其write方法,将Document写入到文件中。该方法的其他代码用作处理流的关闭及异常处理。
readDocument()方法与writeDocument()方法对应,它负责从文件中读取XML数据,也就是Document对象。借助DocumentFactory创建一个Document对象,接着添加XML元素,再把这个不包含任何账户数据的XML文档写入到文件中。如果文件已经被初始化,则该方法使用SAXReader读取文件至Document对象。
用来存储账户数据的XML文件结构很简单,如下是一个包含一个账户数据的文件示例,并不需要在项目中包含
<?xml version="1.0" encoding="UTF-8"?>
<account-persist>
<accounts>
<account>
<id>zachary</id>
<name>Zachary Chang</name>
<email>ZacharyChang.zc@gmail.com</email>
<password>this_is_encrypted</password>
<activated>false</activated>
</account>
</accounts>
</account-persist>
这个XML文件的根元素是account-persist,其下是accounts元素,可以包含多个account元素,每个account元素代表一个账户,其子元素表示该账户的id、姓名、电子邮件、密码以及是否被激活等信息。这时可以返回上面看一下增删改查的实现过程。
另外,还需要一个Spring框架的配置文件,将它放在src/main/resources目录下
//account-persists.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:account-service.properties" />
</bean>
<bean id="accountPersistService"
class="com.juvenxu.mvnbook.account.persist.AccountPersistServiceImpl">
<property name="file" value="${persist.file}" />
</bean>
</beans>
该配置文件首先配置了一个id为peopertyConfigurer的bean,其实现类为PropertyPlaceholerConfigurer,作用是从项目classpath载入名为account-service.properties的配置文件。随后的bean是accountPersistService,实现为AccountPersistServiceImpl,同时这里使用属性persist.file配置其file字段的值。简而言之,XML数据文档的位置是由项目下account-service.properties文件中persist.file属性的值配置的。
3.测试代码部分
测试代码位于src/test/java/目录下,测试资源文件位于src/test/resources/目录下。在以上Spring框架中定义要求项目classpath下有一个名为account-service.properties的文件,且该文件需包含一个persist.file属性,以定义文件存储位置。为了能够测试账户数据的持久化,在测试资源目录下创建文件
//account-service.properties
persist.file=${project.build.testOutputDirectory}/persist-data.xml
该文件只包含一个persist.file属性,其中${project.build.ttestOutputDirectory}部分是一个Maven属性,该属性表示Maven的测试输出目录,其默认地址为项目根目录下的target/test-classes文件夹。即在测试中使用测试输出目录下的ppersist-data.xml文件存储账户数据。
然后编写测试用例AccountPersistService。为了避免冗余,这里只测试readAccount()方法
//AccountPersistServiceTest.java
package com.juvenxu.mvnbook.account.persist;
import static org.junit.Assert.*;
import java.io.File;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class AccountPersistServiceTest
{
private AccountPersistService service;
@Before
public void prepare()
throws Exception
{
File persistDataFile = new File ( "target/test-classes/persist-data.xml" );
if ( persistDataFile.exists() )
{
persistDataFile.delete();
}
ApplicationContext ctx = new ClassPathXmlApplicationContext( "account-persist.xml" );
service = (AccountPersistService) ctx.getBean( "accountPersistService" );
Account account = new Account();
account.setId("juven");
account.setName("Juven Xu");
account.setEmail("juven@changeme.com");
account.setPassword("this_should_be_encrypted");
account.setActivated(true);
service.createAccount(account);
}
@Test
public void testReadAccount()
throws Exception
{
Account account = service.readAccount( "juven" );
assertNotNull( account );
assertEquals( "juven", account.getId() );
assertEquals( "Juven Xu", account.getName() );
assertEquals( "juven@changeme.com", account.getEmail() );
assertEquals( "this_should_be_encrypted", account.getPassword() );
assertTrue( account.isActivated() );
}
@Test
public void testDeleteAccount()
throws Exception
{
assertNotNull( service.readAccount( "juven" ) );
service.deleteAccount( "juven" );
assertNull( service.readAccount( "juven" ) );
}
@Test
public void testCreateAccount()
throws Exception
{
assertNull( service.readAccount( "mike" ) );
Account account = new Account();
account.setId("mike");
account.setName("Mike");
account.setEmail("mike@changeme.com");
account.setPassword("this_should_be_encrypted");
account.setActivated(true);
service.createAccount(account);
assertNotNull( service.readAccount( "mike" ));
}
@Test
public void testUpdateAccount()
throws Exception
{
Account account = service.readAccount( "juven" );
account.setName("Juven Xu 1");
account.setEmail("juven1@changeme.com");
account.setPassword("this_still_should_be_encrypted");
account.setActivated(false);
service.updateAccount( account );
account = service.readAccount( "juven" );
assertEquals( "Juven Xu 1", account.getName() );
assertEquals( "juven1@changeme.com", account.getEmail() );
assertEquals( "this_still_should_be_encrypted", account.getPassword() );
assertFalse( account.isActivated() );
}
}
该测试用例使用与AccountPersistService一致的包名,它有两个表示方法:prepare()与testReadAccount()。其中prepare()方法使用@Before标注,表示在测试用例之前执行该方法。它首先检查数据文件是否存在,如果存在则将其删除以得到干净的测试环境,接着使用account-persist.xml配置文件初始化SpringFramework的IoC容器,再从容器中获取要测试的AccountPersistService对象。最后,prepare()方法 创建一个Account对象,设置对象的字段之后,使用AccountPersistService的createAccount()方法将其持久化。
使用@Test标注的testReadAccount()方法就是要测试的方法。该方法根据id使用AccountPersistService读取Accoutn对象,然互检查该对象不为空,并且每个字段的值必须与刚才插入的对象的值完全一致。
该测试用例遵循了测试接口而不测试实现这一原则也就是说,测试代码不能应用实现类,由于测试是从接口用户的角度编写的,这样就能保证接口的用户无须知晓接口的实现细节,既保证了代码的解耦,也促进了代码的设计。
参考:Maven实战(第8章)——许晓斌著