Chapter 17. Web Applications with Spring MVC * 개요 1. MVC구조 : model1 과 model2 를포함한 MVC구조에대해서언급한다. 2. Spring MVC : MVC구조의컴포넌트를 Spring웹애플리케이션내에서구현하는방법에대해언급한다. 3. controller : 컨트롤러를 Spring내에서구현하는방법과특정요청을다루는컨트롤러를구분하는방법을언급한다. 4. 테마 (Themes) 와로케일 : 국제화와테마를설정하는방법을언급한다. * MVC 는무엇인가.? MVC 는 Model View Controller 의줄임말이다. 이패턴의목적은사용자요청에작동하고데이터를 조작하고표현할필요가있는애플리케이션의구현을단순화하는데있다. 이패턴에는다음과같은 3 가지의구별된컴포넌트가존재한다. + Model : 사용자가보게되는데이터를표현한다. 대개의경우, Model은자바빈으로구성된다. + View : model를표현한다. 웹애플리케이션의경우대개 HTML로표현이된다. + Controller : 사용자요청에반응하고처리하며적당한 model을빌드하고표현하기위한 view로이것을전달하는로직의일부이다. 대부분의자바웹애플리케이션의경우 controller는 servlet이다. 다음의그림은 MVC 의 type1 을나타내는그림이다.
이타입에서 JSP페이지는애플리케이션의핵심이된다. 그것은컨트롤로직과표현 (presentation) 모두를포함한다. 클라이언트는 JSP로요청을보내고, 페이지내로직은그다음빌드되고 model을표현한다. presentation레이어와 control레이어의구분이명확하지않다. JSP가수행할필요가있는로직의양이많기때문에 type1 의방식은조만간없어지게될것이다. model 2 는좀더큰애플리케이션내에서도관리가가능하도록해준다. model1 에서는 JSP페이지가 view도되고 controller도되지만, model2 에서는 controller를위한 JSP를분리한다. 사용자의요청을중간에서처리하는 controller는 model을준비하고, 표현을위한 view로 model을전달한다. JSP페이지는요청을처리하기위한로직을더이상포함하지않는다. 이것은단순히 controller에의해준비된 model을표시할뿐이다. 우리는 model1 에서는 view와 controller로 JSP를사용하고 model2 에서는 view로 JSP를사용한다. 하지만이것은 view가 JSP페이지에만제한되지는않기때문에올바르지않다. Spring MVC구조는 model2 MVC의구현이다. 게다가 view는모델을표현하고클라이언트로결과물을반환할수있는어떠한것도될수가있다. 다음의그림은 MVC 의 type2 를나타내는그림이다. * Introducing Spring MVC Spring MVC 는 MVC model2 패턴을사용하는유연한애플리케이션을빌드하도록지원한다. model 은 데이터를가지는간단한 Map 이다. View 는데이터를표현하는인터페이스이고, controller 는 Controller 인터페이스의구현물이다. 소개와개요 웹애플리케이션을위한 MVC 구조의 Spring 구현물은 DispatcherServlet 에기반을두고있다. 이 서블릿은요청을처리하고요청을다루기위한적당한 controller 를호출한다.
DispatcherServlet은들어오는요청을받아서어떤 Controller가요청을다룰지판단한다. Spring controller는핸들링메소드로부터 ModelAndView클래스를반환한다. ModelAndView인스턴스는 view와 model에대한참조를가진다. model은 view가표현할자바빈즈를가지는간단한 Map인스턴스이다. View는인터페이스이고, 클라이언트가해석할수있는어떠한것이라도될수있도록해준다. 구현 우리가 Spring 을사용한웹애플리케이션을생성하기를원한다면, 다음처럼 web.xml 에 DispatcherServlet 을명시하고명시된 url-pattern 을위한맵핑을셋팅해야만한다. <?xml version="1.0" encoding="iso-8859-1"?> <!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>pro Spring Chapter 17 Sample application</display-name> <description>dtto</description> <servlet> <servlet-name>ch171819</servlet-name> <servlet-class> org.springframework.web.servlet.dispatcherservlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ch17</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name> ch171819</servlet-name> <url-pattern>*.tile</url-pattern> </servlet-mapping> </web-app>
이 web.xml 파일은 *.html 이나 *.tile 로들어오는모든요청을위해맵핑된 DispatcherServlet 의 ch171819 서블릿을정의한다. * 핸들러맵핑사용하기 Spring내에서 URL맵핑이어떻게작동을하는지알아보자. 당신은특정요청을위해호출될 controller가무엇인지 Spring에게알리기위한 URL맵핑을알아볼필요가있다. Spring은호출하는 controller를알아내기위해 HandlerMapping구현물을사용하고그구현물은아래와같다. HandlerMapping BeanNameUrlHandlerMapping 상세설명 bean이름은 URL에의해인식된다. 만약 URL이 /product/index.html 이라면, controller id 는 /product/index.html 로 셋팅될맵핑을다룬다. 이맵핑은요청내와일드카드를지원하지 않기때문에다소작은규모의애플리케이션에적합하다. SimpleUrlHandlerMapping 이핸들러는 controller 가요청을다루도록명시하는요청 ( 전체이름과 와일드카드를사용하여 ) 을명시하도록허용한다. * 핸들러 Interceptors 사용하기 interceptors는각각의맵핑을위해호출되는 interceptor의목록을명시할수있기때문에맵핑과관련이있다. HandlerIntereptor 구현물은적당한 controller이처리를하기전이나후에각각의요청을처리할수있다. 당신은 HandlerInterceptor 인터페이스를구현할수도있고모든 HandlerInterceptor메소드를위한디폴트구현물을제공하는 HandlerInterceptorAdapter를확장할수도있다. 다음은그예이다. import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.modelandview; import org.springframework.web.servlet.handler.handlerinterceptoradapter; public class BigBrotherHandlerInterceptor extends HandlerInterceptorAdapter {
public void posthandle(httpservletrequest request, HttpServletResponse response, Object handler, ModelAndView modelandview) throws Exception { // process the request 이러한 interceptor 의실질적인구현물은요청파라미터를처리하고그결과를로그에남길것이다. interceptor 를사용하기위해서, 우리는다음처럼 Spring 애플리케이션컨텍스트파일에 URL 맵핑과 interceptor bean 정의를생성한다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="bigbrotherhandlerinterceptor" class="bigbrotherhandlerinterceptor" /> <bean id="publicurlmapping" class="org.springframework.web.servlet.handler.simpleurlhandlermapping"> <property name="interceptors"> <list> <ref local="bigbrotherhandlerinterceptor" /> </list> <property name="mappings"> <props> <prop key="/index.html">indexcontroller</prop> <prop key="/product/index.html">productcontroller</prop> <prop key="/product/view.html">productcontroller</prop> <prop key="/product/edit.html">productformcontroller</prop> </props> </bean> </beans>
당신은당신이하고싶은만큼많은수의 HandlerMapping 과 HandlerInterceptor bean 들을명시할수 있다. * controller 와작동하기 controller는요청을처리하고, 요청에기반하는 model을빌드하고표현을위한 view로 model을전달하는모든작업을수행한다. Spring의 dispatcherservlet은클라이언트로부터요청을받아서더많은처리를위해요청을위임하는 HandlerAdapter구현물을사용한다. 당신은전달해야만하는요청의묶음을변경하기위해 HandlerAdapter를구현할수있다. DispatcherServlet은사용하길바라는 HandlerAdapter을명시하도록하는 List handleradapter프라퍼티를가진다. HandlerAdapter구현물이순서대로호출되기위해, 당신은 HandlerAdapter사이의위치를표시하기위해당신의 HandlerAdapter내 Ordered 인터페이스를구현할수있다. DispatcherServlet의 handleradapters프라퍼티의값이 null이라면, DispatcherServlet은 SimpleControllerHandlerAdapter를사용한다. 우리는추가적인 HandlerAdapter구현물을제공하지않을것이기때문에, 우리의애플리케이션은 SimpleControllerHandlerAdapter을사용할것이다. SimpleControllerHandlerAdapter의 handle() 메소드는 ((Controller) handler).handlerequest(request, response), 를호출하기때문에, Spring handler bean은 Controller인터페이스를구현해야만하는 controller처럼작동한다. 이접근법은편리한수퍼클래스중하나를사용하여자신만의구현물을사용하는것을쉽게만든다. Controller인터페이스는당신의웹애플리케이션에서만사용될수있다는의미로 HttpServletRequest와 HttpServletResponse에의존한다. Controller인터페이스의가장기본적인구현물을살펴보자. import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.modelandview; import org.springframework.web.servlet.mvc.controller; public class IndexController implements Controller { public ModelAndView handlerequest(httpservletrequest request, HttpServletResponse response) throws Exception { response.getwriter().println("hello, world");
return null; 우리가구현할필요가있는메소드는오직 ModelAndView handerrequest(httpservletrequest, HttpServletResponse) 이다. 우리는표현될 view가없고응답의출력을위해쓰여진출력은처분 (commit) 되었으며클라이언트에반환된다는의미로 ModelAndView로 null을반환한다. Spring은이미유용한많은수의수퍼클래스를제공하기때문에 Controller인터페이스를구현하는것은의미가없다. AbstractController AbstractController 는요청을처리하기위한 handlerequestinternal 메소드를구현하도록강요하는인터페이스를간단하게포장 (warpper) 한다. AbstractController 는요청과응답을제어하기위한추가적인프라퍼티를셋팅하는것을허용하는 WebContentGenerator 클래스를확장하기때문에, 부분적으로는사실이다. 추가적으로, WebContentGenerator 는 ApplicationContextAware 를구현하는 ApplicationObjectSupport 를확장하는 WebApplicationObjectSupport 를확장한다. 반면에, Controller 인터페이스를구현하는것보다 AbstractController 로부터당신의 controller 를확장하는것이직접적으로 ServletContext, WebApplicationContext, ApplicationContext, Log, 그리고 MessageSourceAccessor 에접근하도록해준다. 프라퍼티 설명 디폴트값 supportedmethods 지원되고허용되는 HTTP 메소드 GET, POST requiressession HttpSession 인스턴스가요청을처리하는데 요구되는지에대한명시 false useexpiresheader HTTP 1.0 expires 헤더를사용할지에대한명시 true usecachecontrolheader cacheseconds synchronizeonsession HTTP 1.1 cache-control 헤더를사용할지에대한명시명시된초 (second) 동안생성된결과물을캐시할지를클라이언트에지시 handlerequestinternal 를호출하기전에 HttpSession 의인스턴스를통기화할지에대한명시. true 1 false
프라퍼티 설명이것은같은클라이언트로부터요청핸들링에재진입하는데유용하다. 디폴트값 예를들면, 위값중에 cacheseconds 프라퍼티를 10 으로셋팅하고클라이언트에서페이지를새로고 침한다면결과로써, 우리는매번 10 마다서버로부터새로운결과물을가져온다. import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.modelandview; import org.springframework.web.servlet.mvc.abstractcontroller; public class IndexController extends AbstractController { protected ModelAndView handlerequestinternal(httpservletrequest request, HttpServletResponse response) throws Exception { setcacheseconds(10); response.getwriter().println("hello, world at " + System.currentTimeMillis()); return null; ParameterizableViewController 이것은 AbstractController 의간단한하위클래스이다. 이것은 viewname 프라퍼티내셋팅된이름을가진새로운 model 을반환하기위한 handlerequestinternal 메소드를구현한다. model 로삽입된데이터는없고, 이것의이름을사용하여 view 를간단히표시하기위해이 controller 을사용할것이다. 다음은 ParameterizableViewController 의기능을보여주기위해생성된 ParameterizableIndexController 를보여준다. import org.springframework.web.servlet.mvc.parameterizableviewcontroller; public class ParameterizableIndexController extends ParameterizableViewController {
다음의 xml 파일에서, 우리는애플리케이션컨텍스트파일로 parameterizableindexcontroller bean 을 추가하고, publicurlmapping bean 으로참조를추가하는것처럼 product-index 에 viewname 프라퍼티 를셋팅한다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="publicurlmapping" class="org.springframework.web.servlet.handler.simpleurlhandlermapping"> <property name="interceptors"> <list> <ref local="bigbrotherhandlerinterceptor" /> </list> <property name="mappings"> <props> <prop key="/index.html">indexcontroller</prop> <prop key="/pindex.html"> parameterizableindexcontroller </prop> <prop key="/product/index.html">productcontroller</prop> <prop key="/product/view.html">productcontroller</prop> <prop key="/product/edit.html"> productformcontroller </prop> <prop key="/product/image.html"> productimageformcontroller </prop> </props> </bean>
<bean id="parameterizableindexcontroller" class="parameterizableindexcontroller"> <property name="viewname"> <value>products-index</value> </bean> </beans> MultiActionController 이것또한모든프라퍼티와메소드에접근하는의미를가지는 AbstractController 의하위클래스이다. 이것은당신이필요한 public ModelAndView(HttpServletRequest, HttpServletResponse) throws Exception 의많은구현물을제공한다. 당신은 MultiActionController 의하위클래스내의메소드를구현하기위해선택할수있거나이러한메소드를구현하는위임객체를명시할수있다. 후자의경우, MultiActionController 는위임객체의메소드를호출한다. AbstractController 에추가적인두개의프라퍼티인 delete 와 methodnameresolver 는어떠한객체의메소드가각각의요청에대해호출되는지를 MultiActionController 에게알리기위해사용된다. 만약 delete 프라퍼티가 null 이라는디폴트값으로남겨진다면, 메소드는검색하고 (look up) MultiActionController 하위클래스의메소드를호출한다. 만약 delegate 가 null 이아니라면, 메소드를 delete 의것을호출한다. methodnameresolver 는 MethodNameResolver 의구현물로셋팅되어야만한다. MethodNameResolver 의 3 가지구현물은아래와같다. 구현물 설명 InternalPathMethodNameResolver 메소드이름은 path 의마지막부분으로부터확장자를제외하고얻어진다. 이 resolver path 가사용될때, /servlet/foo.html 은 public ModelAndView foo(httpservletrequest, HttpServletResponse) 처럼선언된메소드로맵핑된다. 이것은 MultiActionController 내사용되는디폴트구현물이다. ParameterMethodNameResolver 메소드이름은특정요청파라미터로부터얻어진다. 디폴트 파라미터이름은 action 이다. 당신은컨텍스트파일에서파라미터 이름을변경할수있다. PropertiesMethodNameResolver 메소드이름은외부프라퍼티파일로부터해석된다. 당신은 /test.html=handletest 처럼정확하게맵핑을명시할수도있고, /*= handleallts 처럼와일드카드를사용할수도있다.
당신의애플리케이션에위의것중에적합한것이없다면, 당신은자신만의 MethodNameResolver 를 구현할수있다. 다음의소스는 MultiActionController 하위클래스의가장간단한구현물을보여준다. import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.modelandview; import org.springframework.web.servlet.mvc.multiaction.multiactioncontroller; public class ProductController extends MultiActionController { throws Exception { public ModelAndView view(httpservletrequest request, HttpServletResponse response) request.getparameter("productid")); response.getoutputstream().print("viewing product " + return null; 위 ProductController 는오직 view() 라는하나의메소드만을추가했다. 만약 path /product/* 가컨테이너에맵핑되고, 요청이 /product/view.html?productid=10 이라면, 브라어저내출력은 Viewing product 10 을보게될것이다. 디폴트에의해 MultiActionController 는 methodnameresolver 처럼 InternalPathMethodNameResolver 를사용하고, delegate 프라퍼티는 null 이된다. 왜냐하면 view() 메소드는 ProductController 의메소드가호출되기때문이다. 다른 methodnameresolver 가설정되는방법을보자. 디폴트에의해, ParameterMethodNameResolver 는메소드이름을얻기위한것으로부터파라미터이름같은 action 을사용한다. 우리는아래와같이 paramname 프라퍼티를셋팅하여변경할수있다. 우리는 paramname 파라미터가호출될메소드의이름을위해 defaultmethodname 프라퍼티를셋팅하여요청내존재하지않을때호출되는메소드이름을명시할수있다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <!-- other beans --> <bean id="productcontroller" class="productcontroller"> <property name="methodnameresolver"> <ref local="productmethodnameresolver" /> </bean> <bean id="productmethodnameresolver" class="org.springframework.web.servlet.mvc.multiaction.parametermethodnameresolver"> <property name="paramname"> <value>method</value> <property name="defaultmethodname"> <value>view</value> </bean> </beans> 만약우리가 /product/a.html 로요청을보내고, 메소드파라미터를명시하지않는다면, ProductController.view 가호출된다. 우리는 /product/a.html?method=view 로요청을보낸다면같은행위를하게될것이다. 만약우리가 /product/a.html?method=foo 로요청을보낸다면, 우리는 public ModelAndView foo(httpservletrequest, HttpServletResponse) 메소드가 ProductController 내구현되지않아서에러메시지를보게될것이다. 마지막으로 PropertiesMethodNameResolver 는 InternalPathMethodNameResolver 와는달리요청 URI 에의존한다. 우리는 Spring 컨텍스트파일에메소드이름을명시할수있다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans>
<!-- other beans --> <bean id="productcontroller" class="product.productcontroller"> <property name="methodnameresolver"> <ref local="productmethodnameresolver" /> </bean> <bean id="productmethodnameresolver" class="org.springframework.web.servlet.mvc.multiaction.propertiesmethodnameresolver"> <property name="mappings"> <props> <prop key="/product/view.html">view</prop> <prop key="/product/v*.html">view</prop> </props> </bean> </beans> 위코드는 PropertiesMethodNameResolver 를사용하는방법을보여준다. 우리는 mappings 프라퍼티 를설정할필요가있고 mapping 의목록과그것들의핸들러메소드를추가할필요가있다. 위소스는 ProductController 내 public ModelAndView view(httpservletrequest, HttpServletResponse) 로 /product/v*.html 처럼 /product/view.html 을선언했다. 이 MethodNameResolver 의이득은맵핑문자열내와일드카드를사용할수있다. 이러한 controller 는매우유용하다. 하지만우리가사용자에의해서브밋된입력을처리한다면, 우리는서브밋된값을얻고에러메시지를처리하는데많은코드를작성해야만한다. Spring 은다양한 command controller 를제공하여이처리를단순화한다. * Views, Locales, and Themes Using Views Programmatically 이예제에서, 우리는 View 를구현하고 AbstractController.handleRequestInternal() 의결과인 ModelAndView 클래스로이구현물을반환한다. 우리의 view 는 View 인터페이스로부터오직하나 메소드인 render(map, HttpServletRequest, HttpServletResponse) 를구현해야만한다. 아래의코드
에서생성한 View 구현물은 model 로부터텍스트파일로모든데이터를출력하고클라이언트로표시 하기위한응답헤더를셋팅한다. import java.io.printwriter; import java.util.iterator; import java.util.map; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.servlet.view; public class PlainTextView implements View { throws Exception { public void render(map model, HttpServletRequest request, HttpServletResponse response) response.setcontenttype("text/plain"); response.addheader("content-disposition", "attachment; filename=output.txt"); PrintWriter writer = response.getwriter(); for (Iterator k = model.keyset().iterator(); k.hasnext();) { Object key = k.next(); writer.print(key); writer.println(" contains:"); writer.println(model.get(key)); 아래에서우리는사용자지정 view 를반환하기위해 IndexController 를변경한다. public class IndexController extends AbstractController {
protected ModelAndView handlerequestinternal(httpservletrequest request, HttpServletResponse response) throws Exception { setcacheseconds(10); Map model = new HashMap(); model.put("greeting", "Hello World"); model.put("server time", new Date()); return new ModelAndView(new PlainTextView(), model); /index.html 경로로요청을보내보자. IndexController.handleRequestInternal 메소드는호출되고이것은 PlainTextView 의인스턴스로셋팅되는 View 를가진 ModelAndView 의인스턴스를반환하며 model Map 는 Greeting 와 Server time 키를포함한다. PlainTextView 의 render() 메소드는헤더정보를셋팅한다.
이예제는하나의단점을가진다. IndexController 내코드는각각의요청에대해 PlainTextView 의인 스턴스를생성한다. view 는비상태유지객체이기때문에이것은다소불필요하다. 다음에서우리는 애플리케이션을향상시키고 PlainTextView Spring bean 을만든다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="plaintextview" class="plaintextview" /> <!-- other beans as usual --> </beans> 우리는각각의요청을위해 PlainTextView 의인스턴스를인스턴스화하는대신에 PlainTextView bean 을사용하기위해아래와같이 IndexController 의 handlerequestinternal() 메소드를변경할필요가 있다. public class IndexController extends AbstractController { protected ModelAndView handlerequestinternal(httpservletrequest request, HttpServletResponse response) throws Exception { setcacheseconds(10);
Map model = new HashMap(); model.put("greeting", "Hello World"); model.put("server time", new Date()); View view = (View) getapplicationcontext().getbean("plaintextview"); return new ModelAndView(view, model); 위소스는이전의것보다는좋다. 각각의요청은 PlainTextView bean 의같은인스턴스를얻게된다. 하지만여전히이상적이지는않다. 전형적인웹애플리케이션은굉장히많은수의 view 로구성이되고, 이러한방법으로 view 를설정하는것은불편하다. 게다가어떤 view 는더많은설정을요구한다. JSP view 를얻는것은예를들면, JSP 페이지의경로를필요로한다. 만약우리가 Spring bean 처럼손으로일일이모든 view 를설정한다면, 우리는분리된 bean 처럼각각의 JSP 페이지를설정해야만한다. 우리가 view 를설정하고 Spring 에대해작동하는모든것을위임하는좀더쉬운방법을가진다면이것은굉장히좋은상황이라고할수있다. 이러한요구사항을만족시키는것이 view resolver 이다. Using ViewResolver Implementations ViewResolver 는전략적인인터페이스이다. Spring 은이름과로케일에기반하여적당한 view 를찾고인스턴스화하기위해사용한다. 다양한 view resolver 는당신의애플리케이션을좀더유지보수를수비게하도록해주는 ViewResolver 인터페이스의하나의메소드인 View resolveviewname(string viewname, Locale locale) throws Exception 를모두구현한다. locale 파라미터는 ViewResolver 가다른클라이언트로케일을위한 view 를반환할수있도록해준다. 구현물 BeanNameViewResolver 상세설명애플리케이션컨텍스트내설정된 bean 처럼 view 를얻기위해시 도하는간단한 ViewResolver 구현물이다. 당신은 view 정의를고정하는다른파일을생성하는것을원하지않는좀더작은규모의애플리케이션을위해유용하다는것을알수있을것이다. 어쨌든, 이 resolver 는여러가지제한점을가진다. 가장골치아픈것은당신이애플리케이션컨텍스트내 Spring bean 처럼 view 를설정해야만한다는것이다. 또한이것은국제화 (internalization) 를지원하지
구현물 상세설명 않는다. ResourceBundleViewResolver 이것은좀더복잡한 resolver 이다. 이경우, view 정의는각각분리 된설정파일에서유지된다. 당신은애플리케이션컨텍스트파일내 view bean 을설정할필요는없다. 이 resolver 는국제화를지원한다. UrlBasedViewResolver 이 resolver 는 URL 에기반하여적당한 view 를인스턴스화한다. 당신은접두사나접미사를가지는 URL 을설정할수있다. 이 resolver 는당신에게 BeanNameViewResolver 보다좀더 view 에 대한제어를가능하게해준다. 하지만이것은좀더큰애플리케이 션을관리하는것을어렵게할수있고국제화를지원하지않는다. XmlViewResolver 이 view resolver 는 view 정의가분리된파일에서유지되기때문에 ResourceBundleViewResolver 와유사하다. 하지만국제화는지원 하지않는다. 우리는 Spring 내에서사용가능한 ViewResolvers 와그것들의각각의장점과단점을알아보았다. 가장 복잡한기능을제공하는 ResourceBundleViewResolver 에대해서알아보자. viewresolver bean 정의 를포함하는컨텍스트파일을변경하는것으로시작해보자. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="viewresolver" class="org.springframework.web.servlet.view.resourcebundleviewresolver"> <property name="basename"> <value>views</value> </bean> <!-- etc --> </beans> 이것은 Spring 이모든 view 이름을해석하기위해사용하는 viewresolver bean 을소개한다. 클래스 는 ResourceBundleViewResolver 이고, 이것의 basename 프라퍼티는 views 이다. 이것은
ViewResolver 가클래스패스에서 views_<lid>.properties 파일을찾는다는것을의미한다. 여기서 LID 는로케일구분자 (EN, CS 그리고기타등등 ) 를말한다. 만약 resolver 가 views_<lid>.properties 파일을찾지못한다면, 이것은 views.properties 파일을열것이다. 이 resolver 내국제화지원을보여주기위해, 우리는 views.properties 와 views_cs.properties 를생성한다. properties 파일의형식은아래와같이 viewname.class=class-name 와 viewname.url=view-url 이다. #index products-index.class=org.springframework.web.servlet.view.jstlview products-index.url=/web-inf/views/product/index.jsp 이파일을유지하는가장좋은방법은디렉토리구분자처럼 / 를사용하여애플리케이션의논리적인 구조를따라유지보수하는것이가장쉽다. #index user-edit.class=org.springframework.web.servlet.view.jstlview user-edit.url=/web-inf/views/user /edit.jsp 유사하게도우리는 /web-src/as-web/web-inf/views/product 내 index.jsp 와 index_cs.jsp 를생성 한다. 마지막으로우리는 Product 객체의목록을반환하기위해 ProductController 를변경하고 view 에서이목록을보여준다. public class ProductController extends MultiActionController { private List products; private Product createproduct(int productid, String name, Date expirationdate) { Product product = new Product(); product.setproductid(productid); product.setname(name); product.setexpirationdate(expirationdate); return product; public ProductController() { products = new ArrayList();
Date today = new Date(); products.add(createproduct(1, "test", today)); products.add(createproduct(2, "Pro Spring Appes", today)); products.add(createproduct(3, "Pro Velocity", today)); products.add(createproduct(4, "Pro VS.NET", today)); public ModelAndView index(httpservletrequest request, HttpServletResponse response) { return new ModelAndView("products-index", "products", products); // other methods omitted for clarity 당신이보는것처럼, 한가지의변경사항은 index() 메소드내에서 ModelAndView(View,...) 대신에 ModelAndView(String, String, Object) 가호출된다는것이다. 이애플리케이션을테스트하기위해, Czech 가아닌다른언어를브라우저에셋팅하라. Spring 은 URL 을 /WEB-INF/product/index.jsp 로셋팅하고결과물을표시하는 JstlView 타입의 product-index View 를생성한다. 결과물은다음과같다. 만약우리가 Czech 로언어를변한다면, 다음처럼 view resolver 는 JstlView 이고 URL 프라퍼티가 /WEB-INF/products/index_CS.jsp 를가리키는 index_cs 의인스턴스를생성한다.
view 를일일이수동으로인스턴스화하는것보다 view resolver 를사용하는것은설정파일을좀더단순화하는이득을준다. 하지만이것은애플리케이션의메모리를감소시킨다. 만약우리가 Sprinb bean 처럼각각의 view 를정의한다면, 이것은애플리케이션시작시인스턴스화된다. 만약우리가 view resolver 를사용한다면, view 는인스턴스화되고첫번째요청시캐시된다. Using Localized Messages 우리가 Sprnig 웹애플리케이션내로케일을이야기하기전에, 우리는 spring:message 태그를사용하 여표시될메시지를위한실질적인텍스트를해석하는방법을봐야만한다. Spring 이찾는디폴트 bean 이름은 messagesource 이다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="messagesource" class="org.springframework.context.support.resourcebundlemessagesource"> <property name="basename"> <value>messages</value> </bean> </beans>
로케일사용하기 Spring 은요청을받아서로케일을가져오거나셋팅하기위해이것의메소드를호출하기위해 LocaleResolver 인터페이스를사용한다. LocaleResolver 의다양한구현물이있다. 다음처럼각각은특 별한사용법과프라퍼티를가진다. 구현물 AcceptHeaderLocaleResolver 상세설명이로케일 resolver는애플리케이션을위해사용자에이전트에의 해보내어진 accept- language 헤더에기반하여로케일을반환한다. 만약이 resolver가사용된다면, 애플리케이션은사용자가선호하는언어를자동적으로나타낼것이다. 만약당신이다른언어로변경하기를원한다면브라우저셋팅을변경해야한다. CookieLocaleResolver 이로케일 resolver 는로케일을확인하기위해클라이언트머신의 쿠키를사용한다. 이것은브라우저셋팅의변경없이나타내고자하는언어를명시하도록해준다. 이로케일 resolver를사용하여, 우리는사용자의브라우저쿠키저장소를사용하여로케일셋팅을저장할수있다. FixedLocaleResolver 이것은설정된하나의로케일만을반환하는 LocaleResolver 의매 우간단한구현물이다. SessionLocaleResolver 이 resolver 는 CookieLocaleResolver 처럼작동한다. 하지만로케 일셋팅은쿠키에저장되지않고세션이종료되면사라진다. Using Themes 사용자언어로애플리케이션의 view를제공하는데추가적으로, 당신은사용자의경험을향상시키기위해테마를사용할수있다. 테마는스타일시트와내장된이미지의모음이다. Spring은 JSP페이지내지원되는테마가가능하도록하기위해사용할수있는태그라이브러리를제공한다. 테마의사용을보여주기위해사용될디렉토리구조는아래와같다.
당신이보는것처럼우리는테마디렉토리를추가하고두개의새로운프라퍼티파일 (cool.properties,default.properties) 을생성했다. 프라퍼티파일의내용은아래와같이정적인테마 자원의위치를명시한다. css=/themes/cool/main.css 프라퍼티파일내 key 는테마 resolver 에의해드러나는 key 를명시한다. 그리고프라퍼티의 value 는 테마자원의위치를명시한다. 우리는 Spring 태그라이브러리를사용하여 JSP 페이지내이정의를사 용할수있다. <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head>
<c:set var="css"> <spring:theme code="css" /> </c:set> <c:if test="${not empty css"> <link rel="stylesheet" href="<c:url value="${css" />" type="text/css" /> </c:if> </head> <body> This page lists all available products:<br> <c:foreach items="${products" var="product"> <c:out value="${product.name" /> <a href="view.html?productid=<c:out value="${product.productid" />">[View]</a> <a href="edit.html?productid=<c:out value="${product.productid" />">[Edit]</a> <br><hr> </c:foreach> <br> <a href="edit.html">[add]</a> </body> </html> 마지막으로, 우리는 Spring 애플리케이션컨텍스트를변경하고 themeresolver bean 을추가할필요가 있다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="themeresolver" class="org.springframework.web.servlet.theme.fixedthemeresolver"> <property name="defaultthemename"> <value>cool</value> </bean>
</beans> <!-- other beans as usual --> 애플리케이션컨텍스트파일은애플리케이션이 defaultthemename가 cool로셋팅되는 FixedThemeResolver를사용하는것을명시한다. 그러므로테마는클래스패스의가장상위의 cool.properties파일로부터로드된다. 테마는스타일시트가아닌이미지나영상과같은어떠한종류의정적인내용을포함할수있다. 이것은또한이미지가다른언어로번역될필요가있는텍스트를포함하기때문에국제화를지원해야만한다. 테마 resolver내국제화지원은 ResourceBundleViewResolver내국제화지원과완전히동일하게작동한다. 테마 resolver는 theme_<lid>.properties 를로드할려고시도한다. 여기서 LID는로케일구분자 (EN, CS 또는기타등등 ) 이다. 만일 LID를가진 properties파일이존재하지않는다면, resolver는 LID없는 properties파일을로드하기를시도할것이다. ViewResolvers 와 LocaleResolvers처럼, ThemeResolvers 에도다음처럼다양한구현물이있다. 테마 Resolver CookieThemeResolver 상세설명이것은사용자별로셋팅되는테마를허용하고클라이언트컴퓨터의 쿠키를저장하여테마참조를저장한다. FixedThemeResolver 이테마 resolver 는 bean 의 defaultthemename 프라퍼티에셋팅된 하나의고정된테마를반환한다. SessionThemeResolver 이것은사용자세션별로셋팅되는것을허용한다. 이테마는세션사 이에유지되지는않는다. * Command Controller 사용하기 전형적인애플리케이션은사용자로부터데이터를모으고처리한다. Spring은 controller로보내어진데이터를처리하는 command Controller를제공한다. 우리가다양한 command Controller에대해언급하기이전에 command Controller의개념을알아보자. command controller는 form 서브밋으로부터활성화되는 command 객체의프라퍼티를허용한다. command controller는데이터유효성체크를단순화하기위한 Spring 태그라이브러리로작동한다. command controller는모든비즈니스유효성체크를수행하기에는이상적이다. 유효성체크가서버에서발생할때, 사용자가유효성체크를무시하는것은불가능하지만, 당신은모든유효성체크를수행하기위해웹티어에의존하지않고비즈니스티어에서재입증할게될것이다. 기술적인측면에서, command controller구현물은대개도메인객체인호출되는 command객체를나타낸다. 우리의애플리케이션에서사용할수있는 command controller를살펴보자.
+ AbstractCommandController AbstractController 처럼, 이것은 Controller인터페이스를구현한다. 이클래스는실질적으로 HTML form 서브밋을다루기위해디자인된것은아니지만, 유효성체크와데이터바인딩을위한기본적인지원을제공한다. 당신은자신만의 command controller를구현하기위해이클래스를사용할수있다. + AbstractFormController AbstractFormController클래스는 AbstractCommandController를확장하고실질적으로 HTML form서브밋을다룰수있다. 반면에, 이 command controller는 HttpServletRequest내값을처리하고 controller 의 command객체를활성화한다. AbstractFormController 또한중복 form 서브밋을찾아내는능력을가진다. 그리고 Spring 컨텍스트파일보다는코드내표현되는 view를명시하도록허용한다. + SimpleFormController 이것은 HTML form을처리하기위해가장공통적으로사용되는 command controller이다. 이것은또한사용하기쉽게디자인되었다. 당신은 initial( 초기화 ) view와 success( 성공 ) view를표시하기위한 view를명시할수있다. 그리고당신은서브밋된데이터를활성화할필요가있는 command객체를셋팅할수있다. + AbstractWizardFormController 이름이제시하는것처럼, 이 command controller는페이지의마법사스타일세트를구현하기위해유용하다. 이것은또한당신이현재 HttpSession내 command객체를유지하고현재페이지의데이터가유효한지와마법사가다음페이지에도계속될수있는지를체크하기위한 validatepage() 메소드를구현할필요가있다는것을내포한다. 마지막으로 AbstractWizardFormController는마법사가처리하는마지막페이지를처리하는것을표시하기위한 processfinish() 메소드를수행한다. 그리고데이터는유효하고비즈니스티어로전달될수있다. Using Form Controllers 지금당신은선택해야할 form controller가무엇인지안다. form controller가어떻게사용되는지예제를통해서보자. 우리는가장간단한 form controller로시작하고유효성체크와사용자정의포맷터를추가한다. 가장간단한 controller 구현물은 onsubmit() 메소드를오버라이드하고디폴트생성자를제공하는 SimpleFormController를확장한다. public class ProductFormController extends SimpleFormController {
public ProductFormController() { super(); setcommandclass(product.class); setformview("products-edit"); protected ModelAndView onsubmit(httpservletrequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { System.out.println(command); return new ModelAndView("products-index-r"); ProductFromController의생성자는 command클래스가 Product.class임을정의한다. 이것은이 controller가생성하는객체가 Product의인스턴스임을의미한다. 다음으로, 우리는사용자가 form을서브밋할때호출되는 onsubmit() 메소드를오버라이드한다. command객체는이미유효성체크를하고애플리케이션의비즈니스티어로이것을안전하게전달한다. onsubmit() 메소드는 products/index.html 페이지로리다이렉트하는 RedirectView인 products-indexr view를반환한다. 우리는요청을다루기위한 ProductController.handleIndex() 메소드를원하기때문에이것이필요하다. 마지막으로, setformview() 에대한호출은 form을표시하기위해사용되는 view를명시한다. 우리의경우, JSP는다음과같다. <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <c:set var="css"><spring:theme code="css"/></c:set> <c:if test="${not empty css"> <link rel="stylesheet" href="<c:url value="${css"/>" type="text/css" /> </c:if>
</head> <body> <form action="edit.html" method="post"> <input type="hidden" name="productid" value="<c:out value="${command.productid"/>"> <table> <tr> <td>name</td> <td><spring:bind path="command.name"> <input name="name" value="<c:out value="${status.value"/>"> <span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td> </tr> <tr> <td>expiration Date</td> <td><spring:bind path="command.expirationdate"> <input name="expirationdate" value="<c:out value="${status.value"/>"> <span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td> </tr> <tr> <td></td> <td><input type="submit"></td> </tr> </table> </form> </body> </html> Spring이제공하는 spring:bind태그는가장간단한방법으로 form으로부터값을전달하도록해주고간단한유효성체크를제공한다. 첫번째, 우리는 path에 command객체이름인값을바인드하고필드에셋팅한다. spring:bind태크에서 Spring은 value필드가 spring:bind태그내정의된프라퍼티의값을표현하는객체상태를명시한다. status.errormessage는어떤유효성에러메시지를정의한다.
마지막으로우리는애플리케이션컨텍스트파일의 productformcontroller bean 을변경할필요가있고 /product/edit.html 에서 form controller 를맵핑할필요가있다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="publicurlmapping" class="org.springframework.web.servlet.handler.simpleurlhandlermapping"> <property name="mappings"> <props> <prop key="/index.html">indexcontroller</prop> <prop key="/product/index.html">productcontroller</prop> <prop key="/product/view.html">productcontroller</prop> <prop key="/product/edit.html"> productformcontroller </prop> </props> </bean> <!-- Product --> <bean id="productformcontroller" class="productformcontroller"> </bean> <!-- other beans as usual --> </beans> 당신이보는것처럼, Spring 애플리케이션컨텍스트파일내새로운정의를추가하는것은아무것도아니 다. 만약우리가 http://localhost:8080/ch17/product/edit.html 를본다면, 우리는다음처럼데이터를 넣기위한 for 을가진웹페이지를보게될것이다.
불행하게도, expirationdate 프라퍼티는 Date타입이고 Java 날짜포맷은조금사용하기어렵다. 당신은 Date값을위해 Sun Oct 24 19:20:00 BST 2004 형식을기대할수는없다. 사용자를위해좀더쉽도록하기위해서, 우리는최근바인더 (binder) 를가진사용자정의편집기를등록한다. 이것을하기위해서, 우리는 initbinder() 메소드를오버라이드할것이다. public class ProductFormController extends SimpleFormController { // other methods omitted for clarity protected void initbinder(httpservletrequest request, ServletRequestDataBinder binder) throws Exception { SimpleDateFormat dateformat = new SimpleDateFormat("dd/MM/yyyy"); dateformat.setlenient(false); binder.registercustomeditor(date.class, null, new CustomDateEditor(dateFormat, false));
새롭게등록된사용자정의편집기는모든 Date.class값이적용되고값은 dd/mm/yyyy 값으로파싱된다. 24/10/2004 는유효한 Date값처럼 Sun Oct 24 19:20:00 BST 2004 대신에적용된다. 여기엔다른중요한것인유효성체크라는것을놓쳤다. 우리는이름이없는 product를추가하는것을사용자에게허용하길원하지않는다. 유효성체크를구현하기위한가장멋진방법은아래와같이 Validator 인터페이스를구현하는것이다. Spring관리 bean처럼 ProductValidator를등록하고 productvalidator bean에 ProductFormController의 validator프라퍼티를셋팅한다. import org.springframework.validation.errors; import org.springframework.validation.validator; public class ProductValidator implements Validator { public boolean supports(class clazz) { return clazz.isassignablefrom(product.class); public void validate(object obj, Errors errors) { Product product = (Product) obj; if (product.getname() == null product.getname().length() == 0) { errors.rejectvalue("name", "required", ""); Validator구현물은요구되는 errorcode세트를가진유효성에러를추가한다. 이코드는 messagesource bean을사용하여해석될필요가있는메시지자원을구별한다. messagesource bean은외부화될메시지문자열을허용하고국제화를잘지원한다. 국제화된메시지를생성하기위한규칙은국제화된 view와테마를생성하기위한규칙과정확하게동일하다. 우리는마지막으로애플리케이션컨텍스트파일을보여준다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans>
<bean id="messagesource" class="org.springframework.context.support.resourcebundlemessagesource"> <property name="basename"> <value>messages</value> </bean> <bean id="productvalidator" class="productvalidator" /> <!-- Product --> <bean id="productformcontroller" class="productformcontroller"> <property name="validator"> <ref bean="productvalidator" /> </bean> <!-- other beans as usual --> </beans> 우리가애플리케이션을빌드하고다시배치할때, product/edit.html 페이지로가라. 그리고유효한 종료날짜를제공하지만이름은채우지않고 form 을서브밋을시도해보라. 우리는적당한언어로에 러메시지를보게될것이다.
당신은사용자로부터새로운데이터를얻는방법을안다. 하지만전형적인애플리케이션에서당신은편집을잘다룰것이다. 비즈니스티어로부터가져온데이터를포함하는 command객체를준비하기위한방법이어야만한다. 이것은객체의구분자를명시하는요청파라미터를포함하는 edit페이지에요청하는것을의미한다. 객체는다음비즈니스티어로호출하여로드되고사용자에게표시된다. 이것을하기위해 formbackingobject() 메소드를오버라이드하라. public class ProductFormController extends SimpleFormController { // other methods omitted for clarity protected Object formbackingobject(httpservletrequest request) throws Exception { Product command = new Product(); int productid = RequestUtils.getIntParameter(request, "productid", 0); if (productid!= 0) { // load the product command.setproductid(productid); command.setname("loaded");
return command; 요청파라미터 productid가 2인것을가지고 edit.html에요청을보낼때, command객체의이름프라퍼티는로드되기위해셋팅된다. 물론, controller내 Product 객체의인스턴스를생성하는대신에, 우리는 productid에의해구별되는객체를전달하기위한비즈니스티어를사용한다. 다른 controller는 form서브밋과유효성체크를처리하기위해같은규칙을따른다. 그래서당신은나중에그것들을서술할필요가없다. Spring샘플애플리케이션은다른 controller의사용을설명한다. Exploring the AbstractWizardFormController AbstractFormController의매우유용한하위클래스는페이지의마법사같은형식의시리즈를구현하도록허용하는 AbstractWizardFormController이다. 이 controller구현물을사용하는방법을보여주기위해, 우리는 JSP페이지의간단한세트 (step1.jsp, step2.jsp, finish.jsp) 를가지고시작한다. 이러한 JSP페이지의코드는위 edit.jsp페이지내에서사용된코드와크게다르지않다. // step1.jsp <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <c:set var="css"><spring:theme code="css"/></c:set> <c:if test="${not empty css"><link rel="stylesheet" href="<c:url value="${css"/>" type="text/css" /></c:if> </head> <body> <form action="wizard.html?_target1" method="post"> <input type="hidden" name="_page" value="0"> <table> <tr> <td>name</td> <td><spring:bind path="command.name"> <input name="name" value="<c:out value="${status.value"/>">
<span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td> </tr> <tr> <td></td> <td><input type="submit" value="next"></td> </tr> </table> </form> </body> </html> // step2.jsp <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <c:set var="css"><spring:theme code="css"/></c:set> <c:if test="${not empty css"><link rel="stylesheet" href="<c:url value="${css"/>" type="text/css" /></c:if> </head> <body> <form action="wizard.html?_target2" method="post"> <input type="hidden" name="_page" value="1"> <table> <tr> <td>expiration Date</td> <td><spring:bind path="command.expirationdate"> <input name="expirationdate" value="<c:out value="${status.value"/>"> <span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td>
</tr> <tr> <td></td> <td><input type="submit" value="next"></td> </tr> </table> </form> </body> </html> // finish.jsp <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <c:set var="css"><spring:theme code="css"/></c:set> <c:if test="${not empty css"><link rel="stylesheet" href="<c:url value="${css"/>" type="text/css" /></c:if> </head> <body> <form action="wizard.html?_finish" method="post"> <input type="hidden" name="_page" value="2"> <table> <tr> <td>register now?</td> <td><c:out value="${command"/></td> </tr> <tr> <td></td> <td><input type="submit" value="next"></td> </tr> </table> </form> </body> </html>
당신이보는것처럼 step1.jsp와 step2.jsp 페이지는 Product도메인객체의인스턴스인 command객체의 name과 expirationdate 프라퍼티를간단하게활성화한다. 지금우리는 JSP페이지를셋업하고, AbstractWizardFormController 가마법사의페이지흐름을제어하기위한요청파라미터를사용하는방법을봐야만한다. 이러한파라미터는아래와같다. 파라미터 _target<value> 상세설명 Value는현재페이지가서브밋되고유효할때이거나 allowdirtyforward또는 allowdirtyback 프라퍼티가 true 로셋팅될때 controller 가가는 pages[] 프라퍼티내 인덱스를명시하는숫자이다. _finish 이파라미터가명시되면, AbstractWizardFormController 는 processfinish() 메소드를 호출하고세션으로부터 command 객체를제거한다. _cancel 이파라미터가명시되면, AbstractWizardFormController 는 processcancel() 메소드를 호출하고만약오버라이드되지않는다면, 세션으로부터 command객체를제거한다. 만약이메소드를오버라이드하기로선택한다면, super() 메소드를호출하거나세션으로부터수동으로 command객체를제거하는것을잊지말라. _page 이파라미터 ( 언제나 <input type="hidden" name="_page" value=""> 처럼명시되는 ) 는 pages[] 프라퍼티내페이지의인덱스를명시한다. 지금우리는마법사단계로부터 JSP 페이지를가졌고, 우리는아래처럼 AbstractWizardFormController 의하위클래스처럼 RegistrationController 를구현할필요가있다. public class RegistrationController extends AbstractWizardFormController { public RegistrationController() { setpages(new String[] { "registration-step1", "registration-step2", "registrationfinish" ); setsessionform(true); setcommandclass(product.class); protected ModelAndView processfinish(httpservletrequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { Product product = (Product) command;
System.out.println("Register " + product); return null; protected void initbinder(httpservletrequest request, ServletRequestDataBinder binder) throws Exception { SimpleDateFormat dateformat = new SimpleDateFormat("dd/MM/yyyy"); dateformat.setlenient(false); binder.registercustomeditor(date.class, null, new CustomDateEditor(dateFormat, false)); protected void validatepage(object command, Errors errors, int page, boolean finish) { getvalidator().validate(command, errors); 위코드는 AbstractWizardFormController 하위클래스의가장간단한구현물을표현한다. 기술적으로, 우리가구현해야만하는모든것은 processfinish() 메소드이다. 하지만우리의경우, 우리는 Date클래스를위해사용자정의편집기를등록할필요가있다. 마지막으로, 우리는 Product.class를위한 commandclass프라퍼티를셋팅하길원한다. 우리는 bean정의에서 pages와 sessionform프라퍼티를셋팅할수있다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="publicurlmapping" class="org.springframework.web.servlet.handler.simpleurlhandlermapping"> <property name="interceptors"> <list> <ref local="bigbrotherhandlerinterceptor" /> </list>
<property name="mappings"> <props> <!-- other props omitted --> <prop key="/registration/wizard.html"> registrationcontroller </prop> </props> </bean> <bean id="registrationcontroller" class="registrationcontroller"> <property name="validator"> <ref bean="productvalidator" /> </bean> </beans> 우리는 step1.jsp, step2.jsp 그리고 finish.jsp페이지를위해맵핑을생성하지는않았다. 우리는 /registration/wizard.html 를위한하나의맵핑을생성하는대신에, registrationcontroller bean에의해다루어진다. 우리는또한 productvalidator bean을위한 registrationcontroller의 validator프라퍼티를셋팅한다. 우리는각각의페이지를체크할수있는것을보여주기위해 validatepage() 메소드내 validator프라퍼티를사용한다. 우리가선택한구현물은 AbstractWizardFormController내디폴트구현물과같지만, 만약우리가원한다면, 우리는다음페이지로이동하는것을사용자에게허용한다. AbstractWizardFormController는 processfinish() 메소드를호출하기전에유효성체크를수행한다. 우리가제공하는 AbstractWizardFormController의설명은매우간단하다. 하지만당신의애플리케이션내 AbstractWizardFormController 하위클래스를사용하기위해결정한다면시작점으로는매우좋다. File Upload Spring은 MultipartResolver 인터페이스의구현물을통해파일업로드를다룬다. 특히, Spring은 COS FileUpload와 Commons FileUpload를위한지원을가진다. 디폴트로명시되는 multipartresolver bean은없다. 그래서당신이 Commons나 COS구현물이나당신자신만의구현물을사용하길원한다면, 당신은다음처럼 Spring애플리케이션컨텍스트내 multipartresolver bean을선언해야만한다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans> <bean id="multipartresolver" class="org.springframework.web.multipart.commons.commonsmultipartresolver"> <property name="maxuploadsize"> <value>100000</value> </bean> <bean id="multipartresolver" class="org.springframework.web.multipart.cos.cosmultipartresolver"> <property name="maxuploadsize"> <value>100000</value> </bean> <!-- other beans as usual --> </beans> 당신은하나의 multipartresolver bean을선택할수있다는것을잊지말라. 이것은당신이 bean을선언할때사용할하나를선택해야만한다는것을의미한다. multipartresolver bean이설정되었을때, Spring은 form데이터를 byte[] 배열로변형하기위해 multipart/form-data 로인코딩된요청을다루는방법을안다. 새롭게설정된 multipartresolver 작업을보여주기위해, 우리는 ProductImageForm 과 ProductImageFormController 클래스를생성할것이다. 두번째것이이미지이름과내용을위한프라퍼티를포함하는동안첫번째것은 SimpleFormController를확장하고이미지업로드를다룬다. public class ProductImageForm { private String name; private byte[] contents; public byte[] getcontents() { return contents; public void setcontents(byte[] contents) {
this.contents = contents; public String getname() { return name; public void setname(string name) { this.name = name; 이것은 name 와 contents 프라퍼티를나타내는간단한자바빈이다. import java.net.bindexception; public class ProductImageFormController extends SimpleFormController { public ProductImageFormController() { super(); setcommandclass(productimageform.class); setformview("products-image"); protected ModelAndView onsubmit(httpservletrequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { ProductImageForm form = (ProductImageForm) command; System.out.println(form.getName()); byte[] contents = form.getcontents(); for (int i = 0; i < contents.length; i++) { System.out.print(contents[i]); return new ModelAndView("products-index-r");
protected void initbinder(httpservletrequest request, ServletRequestDataBinder binder) throws Exception { binder.registercustomeditor(byte[].class, new ByteArrayMultipartFileEditor()); ByteArrayMultipartResolver 클래스는멀티파트스트림 (multipart stream) 의내용을파싱하고이것을 byte[] 배열로반환하기위해애플리케이션컨텍스트파일로부터 multipartresolver bean을사용한다. 파일업로드를위한 JSP페이지를코딩할때는주의하라. 가장흔한에러는 form요소의 enctype 속성을잊어버리는것이다. <%@taglib prefix="c" uri="http://java.sun.com/jstl/core"%> <%@taglib prefix="spring" uri="http://www.springframework.org/tags"%> <html> <head> <c:set var="css"><spring:theme code="css"/></c:set> <c:if test="${not empty css"><link rel="stylesheet" href="<c:url value="${css"/>" type="text/css" /></c:if> </head> <body> <form action="image.html" method="post" enctype="multipart/form-data"> <table> <tr> <td>name</td> <td><spring:bind path="command.name"> <input name="name" value="<c:out value="${status.value"/>"> <span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td> </tr> <tr>
<td>image</td> <td><spring:bind path="command.contents"> <input name="contents" type="file"> <span class="error"><c:out value="${status.errormessage"/></span> </spring:bind> </td> </tr> <tr> <td></td> <td><input type="submit"></td> </tr> </table> </form> </body> </html> 당신이보는것처럼, JSP 페이지는 enctype 속성을제외하고는표준적인 HTML 페이지이다. 우리는 views.properties 파일내 view 처럼 JSP 페이지를명시하는것을잊어서는안된다. * Using Spring MVC in the Sample Application
우리는 URL 맵핑을언급하며시작할것이다. 요청 URL 을애플리케이션내 controller 로맵핑하기위해, 우리는하나의간단한 SimpleUrlHandlerMapping bean 을사용한다. 이 bean 은적당한 controller 를위 해모든 *.html 과 *.tile 요청을맵핑한다. bean 정의는아래와같다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="urlmapping" class="org.springframework.web.servlet.handler.simpleurlhandlermapping"> <property name="mappings"> <props> <!-- Index --> <prop key="/index.html">indexcontroller</prop> <!-- Entry --> <prop key="/entry/view.html">entrycontroller</prop> <prop key="/entry/delete.html">entrycontroller</prop> <prop key="/entry/edit.html">editentryformcontroller</prop> <!-- Comment --> <prop key="/comment/index.html">commentcontroller</prop> <prop key="/comment/delete.html">commentcontroller</prop> <prop key="/comment/view.html">commentcontroller</prop> <prop key="/comment/edit.html">editcommentformcontroller</prop> <!-- Audit --> <prop key="/admin/audit/*">auditcontroller</prop> <!-- Tiles --> <prop key="/tiles/menu.html">menutilecontroller</prop> <!-- Users --> <prop key="/admin/users/index.html">userscontroller</prop> <!-- Login --> <prop key="/security/login.html">logincontroller</prop> <!-- Attachments --> <prop key="/attachment/*">attachmentcontroller</prop> </props>
</bean> </beans> 지역화된유효성체크메시지와일반적인메시지를지원하기위해, 우리는 ResourceBundleMessageSource bean을설정한다. 우리는애플리케이션의생명주기내메시지를리로드할필요가없기때문에, ReloadableResourceBundleMessageSource 가아닌이구현물을선택한다. messagesource bean의정의는아래에서보여진다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="messagesource" class="org.springframework.context.support.resourcebundlemessagesource"> <property name="basename"> <value>messages</value> </bean> </beans> 여기서, 우리는두개의메시지프라퍼티파일을생성했다. 하나는 Czech언어를저장하는 messages_cs.properties이고, 다른하나는영어로된텍스트를포함하거나검색을실패한프라퍼티파일을표현하는 messages.properties이다. 그리고유효성체크를위해 CommentValidator 와 EntryValidator를구현한다. import org.springframework.validation.errors; import org.springframework.validation.validator; public class CommentValidator implements Validator { public boolean supports(class clazz) { return Comment.class.isAssignableFrom(clazz); // return clazz.isassignablefrom(comment.class); public void validate(object obj, Errors errors) {
Comment comment = (Comment) obj; if (comment.getsubject() == null comment.getsubject().length() == 0) { errors.rejectvalue("subject", "required", null, "required"); if (comment.getbody() == null comment.getbody().length() == 0) { errors.rejectvalue("body", "required", null, "required"); 우리는첨부의업로드를지원하기위한 Comment 도메인클래스의하위클래스를생성해야만하기때 문에 Comment.class.isAssignableForm(clazz) 를사용하여 supports() 를구현한다. 우리는대개의 Spring bean 처럼 validators 를정의한다. <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="commentvalidator" class="commentvalidator" /> <bean id="editcommentformcontroller" class="editcommentformcontroller"> <property name="blogmanager"> <ref bean="blogmanager" /> <property name="validator"> <ref local="commentvalidator" /> </bean> <bean id="entryvalidator" class="entryvalidator" /> <bean id="editentryformcontroller" class="editentryformcontroller"> <property name="blogmanager"> <ref bean="blogmanager" />
<property name="validator"> <ref local="entryvalidator" /> </bean> </beans> 여기서우리는 blogmanager프라퍼티에접근하는하위클래스를주는편리한클래스의세트를생성한다. 우리가구현하는편리한클래스는 AbstractBlogManagerController, AbstractBlogManagerFormController 그리고 AbstractBlogManagerMultiactionController 이다. 이러한추상클래스는 blogmanager프라퍼티가 BlogManager구현물의인스턴스로셋팅되도록하기위해 InitializingBean 인터페이스를구현한다. 게다가 AbstractBlogManagerController는 AbstractController 의하위클래스가아니다. 이것은 Controller 인터페이스만을구현하고그래서좀더가볍다. AbstractBlogManagerMultiactionController의사용법은다음과같다. import java.util.hashmap; import java.util.map; import javax.servlet.http.httpservletrequest; import javax.servlet.http.httpservletresponse; import org.springframework.web.bind.requestutils; import org.springframework.web.servlet.modelandview; public class EntryController extends AbstractBlogManagerMultiactionController { public ModelAndView handleview(httpservletrequest request, HttpServletResponse response) throws Exception { int entryid = RequestUtils.getRequiredIntParameter(request, "entryid"); Entry e = getblogmanager().getentry(entryid); Map model = new HashMap(); model.put("entry", e); return new ModelAndView("entry-view", model); throws Exception { public ModelAndView handledelete(httpservletrequest request, HttpServletResponse response)
boolean confirm = RequestUtils.getIntParameter(request, "confirm", 0) == 1; int entryid = RequestUtils.getRequiredIntParameter(request, "entryid"); if (confirm) { getblogmanager().deleteentry(entryid, SessionSecurityManager.getUser(request)); return new ModelAndView("entry-deleted"); else { Map model = new HashMap(); model.put("entry", getblogmanager().getentry(entryid)); return new ModelAndView("entry-delete", model); 요구되는프라퍼티가셋팅되는것을확인하기위해 InitializingBean 인터페이스를구현할필요는없다. 사실, afterpropertiesset() 이 final로구현되었다. 우리는표준적인웹애플리케이션내에서기대하는방법으로보안을구현하지는않았다. 대신우리는사용자확인을하기위해가장기본적인지원을수행하는정적메소드를포함하는 SessionSecurityManager 를생성한다. 보안코드를확장하기위해, 당신은 HandlerInterceptor 을구현할수있다. 사용자로그인없이접근가능한 URL중몇몇을허용하기위해, 당신은두개의 bean으로 urlmapping bean을분리했다. 하나는제안을가지지않는맵핑을포함하고, 다른것은 HandlerInterceptor 구현물과로그인한사용자를요구하는맵핑을포함한다.