Wednesday, June 06, 2007

Spring Configuration

โดยปกติ คนที่ใช้ spring มักจะบ่นเรื่อง Configuration file ที่เต็มไปด้วย xml
ถึงกับมี solution จำนวนหนึ่งออกมาทดแทน เช่นไปใช้ groovy หรือไปใช้ annotation

เนื่องจากผมก็มีความเบื่อหน่ายใน xml file เหมือนกัน
ดังนั้นตอนนี้ จึงเริ่มมองหาวิธีที่จะมาทดแทน
สำหรับวิธีการ config สำหรับ spring ที่ผมสนใจตอนนี้ ก็มี
  • Annotation-based configuration
    วิธีนี้จะ bundle มาใน spring 2.1
  • Java Configuration
    แยกเป็น project มาต่างหาก โดยอยู่ใน project JavaConfig

ประเด็นหลักๆที่ต่างกันของ 2 วิธี ก็คือ แนวคิดเรื่องการแยกข้อมูลการ configuration ออกจาก source code
ซึ่งวิธีที่สอง จะดูดีกว่าในแง่นี้ เนื่องจากไม่มี code เกี่ยวกับ configuration ปนอยู่ใน bean เลย

ผมทดลองใช้โจทย์ ใน post ที่เคยเขียนเรื่อง Tapestry 5 IoC
มาลอง config ทั้ง 2 วิธีดู
เริ่มด้วยวิธีแรกก่อน
@Component("indexService")
public class IndexServiceImpl implements IndexService {

public void index(Document document, Long key) {
...
}

}

@Repository
public class RepositoryServiceImpl implements RepositoryService {

public Long persistent(Document document) {
...
return id;
}

}

@Component("uploadService")
public class UploadServiceImpl implements UploadService {

private IndexService indexService;
private RepositoryService repositoryService;
private Map<String, Parser> parserMap = new HashMap<String, Parser>();

public void upload(InputStream input, String type) {
Parser parser = parserMap.get(type);
Document document = parser.parse(input);
Long key = repositoryService.persistent(document);
indexService.index(document, key);
}

@Autowired
public void setIndexService(IndexService indexService) {
this.indexService = indexService;
}

@Autowired
public void setRepositoryService(RepositoryService repositoryService) {
this.repositoryService = repositoryService;
}

@Resource(name="parserMap")
public void setParserMap(Map<String, Parser> parserMap) {
this.parserMap = parserMap;
}
}

จะเห็นมี annotaion อยู่ดังนี้
  • @Component -> เข้าข่ายพวกที่เราเคยประกาศ bean id=".." ใน xml file
  • @Repository -> ต่างกับ Component ตรงที่จะใช้กับพวกที่เป็น Data access layer
    ,เข้าใจว่าตั้งชื่อให้ต่างกัน component เพื่อที่จะได้นำไปใช้อย่างอื่นได้ด้วยเช่น
    เอาไป implement พวก automatic translation of exception
  • @Autowired -> เป็นการ inject โดยใช้ type
  • @Resource -> ใช้ annotation ของ JSR-250 มา inject bean พวกที่ declare ไว้ใน xml file แบบเดิม
    หรือแบบที่ซับซ้อนกว่านั้นเช่น lookup จาก jndi

การใช้วิธีแรกนี้ ยังมีปัญหาไม่สามารถ config พวก map property ผ่านทาง annotation ได้
จึงต้องใช้ 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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.1.xsd"
>

<context:component-scan base-package="anno"/>

<bean id="parserMap" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="pdf">
<bean class="anno.impl.PdfParser"/>
</entry>
</map>
</constructor-arg>
</bean>

</beans>

จากข้างบน จะเห็นว่า เราใช้ component-scan เข้ามากำหนดว่า
เพื่อที่จะได้ข้อมูล bean ที่ทำ annotation ไว้, spring ต้อง scan กลุ่ม package ใดบ้าง

เห็นแล้วก็ยังไม่ชอบใจนัก
ลองดูวิธีที่ 2 บ้าง
ในวิธี Java Configuration นี้ เราจะ implement class ขึ้นมาแทน xml file
โดยการกำหนด @Configuration ไว้ที่ class,
ซึ่ง spring ก็จะใช้ class ที่มี annotation นี้ในการ config beans
@Configuration
public class MyConfiguration {

@Bean
public IndexService indexService() {
return new IndexServiceImpl();
}

@Bean
public RepositoryService repositoryService() {
return new RepositoryServiceImpl();
}

@Bean
private Map<String, Parser> parserMap() {
HashMap<String, Parser> map = new HashMap<String, Parser>();
map.put("pdf", new PdfParser());
return map;
}

@Bean
public UploadService uploadService() {
UploadServiceImpl impl = new UploadServiceImpl();
impl.setIndexService(indexService());
impl.setRepositoryService(repositoryService());
impl.setParserMap(parserMap());
return impl;
}

}

Note: พวก bean RepositoryServiceImpl, IndexServiceImpl, ...
เขียนแบบ POJO แท้ๆ ไม่ต้องมี Annotation ใดๆ ใส่ไว้ให้รบกวนลูกตาเลย

Note: ให้สังเกตุว่า parserMap เป็น private method ซึ่งเท่ากับว่า bean
นี้ไม่สามารถ access จาก method context.getBean('parserMap') ได้

เราสามารถใช้วิธี Java Configuration ผสมกับ xml ได้เช่นกัน
อย่างสมมติว่า เราต้องการให้ parserMap ถูก config จากข้างนอก
ก็สามารถ เขียนส่วน Configuration ใหม่ดังนี้
        @ExternalBean
public Map<String, Parser> parserMap() {
return null;
}

และไปกำหนด xml ข้างนอกแบบนี้
<beans>

<bean id="parserMap" class="java.util.HashMap">
<constructor-arg>
<map>
<entry key="pdf">
<bean class="anno.impl.PdfParser"/>
</entry>
</map>
</constructor-arg>
</bean>

<bean class="anno.configuration.MyConfiguration"/>

<bean class="org.springframework.config.java.process.ConfigurationPostProcessor"/>

</beans>


ถึงตอนนี้แล้ว ผมออกจะชอบวิธีที่ 2 มากกว่า

Related link from Roti

Tuesday, June 05, 2007

Unit Testing in Tapestry 5

testing ใน tapestry 5 แบ่งออกเป็น 2 พวกคือ
1. พวก unit testing, สามารถใช้ได้ทั้ง testng หรือ junit
class ต่างๆที่ช่วยในการ test อยู่ใน package tapestry-core
2. พวก integrated testing, อันนี้เป็นการ test ผ่าน browser โดยใช้ selenium เข้ามาช่วย,
class ที่ช่วยในการ test อยู่ใน package tapestry-test

class ที่เป็นพระเอกในเรื่อง unit testing ก็คือ PageTester
ลองดูตัวอย่างวิธีใช้ง่ายๆ

สมมติเรามี start page หน้าตาแบบนี้
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd">
<head>
<title>Start Page</title>
</head>
<body>
<p id="greeting">${greeting}</p>
</body>
</html>

แล้วก็มี page class หน้าตาแบบนี้
public class Start
{
@Inject
private HelloService helloService;

public String getGreeting() {
return helloService.getGreeting();
}
}

ตัว Test case ก็จะเขียนอย่างนี้
public class TestStartPage extends Assert {
@Test
public void testPage() {
String appPackage = "demo";
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");

Document document = tester.renderPage("Start");
assertEquals("hi", document.getElementById("greeting").getChildText());
}
}

จาก code ที่เราเห็นใน class "Start" เราจะเห็นว่า
มันมีการ inject service ที่ชื่อ "HelloService" เข้ามา
ปัญหาก็คือ ในตอน run จริง, service นี้จะได้มาจาก springframework
แต่ในขั้น unit testing นี้เราไม่อยากลากเอา service ตัวจริงๆเข้ามา test ด้วย
ดังนั้น เราต้องทำการสร้าง stub ของ HelloService ขึ้นมาก่อน
public class HelloServiceStub implements HelloService {

public String getGreeting() {
return "hi";
}

}

การนำ stub ไปใช้ ก็ทำได้โดยผ่านการ configuration ด้วย Module class
package demo.services;

public class TestModule {

public static HelloService buildHelloService() {
return new HelloServiceStub();
}
}

โดยชื่อและ package ของ TestModule ต้องสัมพันธ์กับ parameter ที่เรา pass ให้ PageTester
String appPackage = "demo";        
// นำไป solve หา class โดยใช้ชื่อ class = appPackage + ".services." + appName + "Module"
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");


ส่วนการนำไปใช้จริง เราสามารถเขียนให้ซับซ้อน โดย simulate การ click
หรือการ submit ได้ด้วย ยกตัวอย่าง
        @Test
public void testPage() {
String appPackage = "demo.bid";
String appName = "Test";
PageTester tester = new PageTester(appPackage, appName, "src/main/webapp");

Document document = tester.renderPage("Start");

// ทดสอบ click action link
document = tester.clickLink(document.getElementById("plus"));
// assert ว่าค่าที่ render กลับมา เปลี่ยนไปอย่างที่ต้องการไหม
Node node = document.getElementById("c1").getChildren().get(1);
node = node.getChildren().get(5);
assertEquals("1", node.getChildText());
}

Related link from Roti