2017年12月26日 星期二

Java Spring - Rending Web View

  • Spring resolver
    • 將controller中請求處理的logic和view中render的implement decouple是Spring MVC的特性
    • 如果controller method直接產生HTML,很難在不影響request處理logic的前提下維護更新view
    • controller和view會在model的內容達成一致,這是最大關聯。除此之外,兩者應該保持距離
    • 而Spring resolver的重點功能就在於透過logic view name知道要使用哪過view render model
    • Spring MVE ViewResolver interface:
      • 給定resolveViewName後會回傳一個View instance
      • public interface ViewResolver {
          View resolveViewName(String viewName, Locale locale) throws Exception;
        }
        
      • View是另外一個interface,為了接受model及request&response,並將結果輸出到response
      • public interface View {
            String getContentType();
        
            void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
        }
        
    • Spring提供了一些13個out-of-the-box implementations,能夠適應大多情境 
      •  BeanNameViewResolver
      •  ContentNegotiatingViewResolver
      •  FreeMarkerViewResolver: FreeMarker
      •  InternalResourceViewResolver: JSP
      •  JasperReportsViewResolver
      •  ResourceBundleViewResolver
      •  TilesViewResolver: Apache Tiles view
      •  UrlBasedViewResolver
      •  VelocityLayoutViewResolver
      •  VelocityViewResolver: Velocity
      •  XmlViewResolver
      •  XsltViewResolver
  • Create JSP view
    • Spring提供兩個support JSP view的方式
      1. InternalResourceViewResolver會將view名稱解析為JSP,且能夠將view name解析為jstlView型式的JSP文件,將JSTL localize及resource bundle給JSTL formatting及message label
      2. Spring提供兩個JSP tag library,一個用在form到model的bundle,另一個提供通用的utility features
    • InternalResourceViewResolver將logic name對應為真實的physical path的方式是加上prefix及suffix
      • 例如如果將JSP都放在WEB-INF folder下,防止對他的直接access,則假設在WEB-INF/views/有個home.jsp,那麼logic view name: home,則能夠因為下面的設定:
        • prefix:WEB-INF/views/
        • suffix: jsp
      • 組出WEB-INF/views/home.jsp
      • 這樣的話,就可以很方便地將view model組織為目錄結構,而不是都放在固定目錄下
    • 使用@Bean annotation時,可以如下範例配置InternalResourceViewResolver
    • @Bean
      public ViewResolver viewResolver() {
       InternalResourceViewResolver resolver = new InternalResourceViewResolver();
       resolver.setPrefix("/WEB-INF/views/");
       resolver.setSuffix(".jsp");
       resolver.setExposeContextBeansAsAttributes(true);
       return resolver;
      }
      
    • 也可以使用XML
    • <bean id="viewResolver"
            class="org.springframework.web.servlet.view.InternalResourceViewResolver"
            p:prefix="/WEB-INF/views/" p:suffix=".jsp" />
      
  • JSTL view
    • 如果JSP使用JSTL tag來處理formatting及message的時候,會希望InternalResourceViewResolver能夠將view解析為JstlView
    • JSTL formatting label需要一個Locale object(設定data, currenct使用)
    • 將view解析為JstlView,而不是InternalResourceView,只需要設定viewClass property 
    • @Bean
      public ViewResolver viewResolver() {
       InternalResourceViewResolver resolver = new InternalResourceViewResolver();
       resolver.setPrefix("/WEB-INF/views/");
       resolver.setSuffix(".jsp");
       resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);
       return resolver;
      }
      
    • XML:
    •  <bean id="viewResolver"
                    class="org.springframework.web.servlet.view.InternalResourceViewResolver"
                    p:prefix="/WEB-INF/views/"
                    p:suffix=".jsp"
                    p:viewClass="org.springframework.web.servlet.view.JstlView" />
      
  • 使用Spring的JSP library
    • Binding forms to the model
    • 宣告:
    • 此處的prefix為sf,通常也會使用form prefix
      • <%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
        
    • 共14種tag,可render在HTML form上
      • <sf:checkbox>
      • <sf:checkboxes>
      • <sf:errors>
      • <sf:form>
      • <sf:hidden>
      • <sf:input>
      • <sf:label>
      • <sf:option>
      • <sf:options>
      • <sf:password>
      • <sf:radiobutton>
      • <sf:radiobuttons>
      • <sf:select>
      • <sf:textarea>
    • 使用範例:
    • <sf:form method="POST" commandName="spitter">
        First Name: <sf:input path="firstName" /><br/>// type將會設定為text
        Last Name: <sf:input path="lastName" /><br/> // path值就等同於name值
        Email: <sf:input path="email" /><br/>
        Username: <sf:input path="username" /><br/>
        Password: <sf:password path="password" /><br/> //不會顯示明文
        <input type="submit" value="Register" />
      </sf:form>
      
    • 也能夠設定context在model object(此處是 commandName,property是spitter)
    • 在其他form-binding tags會引用這個model object的property
    • 注意此處將commandName property設定為spitter,這表示在導往registerForm的model中必須要有一個spitter key,否則form將會有問題。
    • 因此:原本的controller處理registration:
    • @RequestMapping(value = "/register", method=GET)
      public String showRegistrationForm() {
       return "registerForm";
      }
      
    • 應修改為
    • @RequestMapping(value="/register", method=GET)
      public String showRegistrationForm(Model model) {
        model.addAttribute(new Spitter());
        return "registerForm";
      }
      
    • 最後的html其實會像這樣:
    • <form id="spitter" action="/spitter/spitter/register" method="POST">
          First Name:
          <input id="firstName" name="firstName" type="text" value="J" />
          <br/> Last Name:
          <input id="lastName" name="lastName" type="text" value="B" />
          <br/> Email:
          <input id="email" name="email" type="text" value="jack" />
          <br/> Username:
          <input id="username" name="username" type="text" value="jack" />
          <br/> Password:
          <input id="password" name="password" type="password" value="" />
          <br/>
          <input type="submit" value="Register" />
      </form>
      
    • 另外,Spring3.1後,<sf:input>能夠指定type property,除了可以選的type外,還能指定HTML5特定type,如date、range、email。如下:
    • Email: <sf:input path="email" type="email" /><br/>
      
    • 其實渲染在HTML就是
    • Email:
          <input id="email" name="email" type="email" value="jack"/><br/>
      
  • Display error
    • 如果存在validation error,request中會包含錯誤的訊息,這些message會和model放在一起,需要做的事情就是將data從model中取出
    • 使用<sf:errors>能夠讓任務變得簡單,可以也設定cssClass="error",使其明顯(需設定css color:red;)
      
      
    • <sf:form method="POST" commandName="spitter">
        First Name: <sf:input path="firstName" />
          <sf:errors path="firstName" cssClass="error"/><br/>
      ...
      </sf:form>
      
    • 渲染出的HTML如下:
    • First Name: <input id="firstName" name="firstName" type="text" value="J"/>
      <span id="firstName.errors">size must be between 2 and 30</span>
      
    • 這種方法好的地方在於很明顯,使用者可以知道問題在哪,但有可能會造成排版有問題
    • 解決:放在同一個地方進行顯示
      • 移除每個input上的<sf:errors> elements,並將其放到form的頂部
      • <sf:form method="POST" commandName="spitter" >
          <sf:errors path="*" element="div" cssClass="errors" />
        ...
        </sf:form>
        
      • path="*" 表示display所有attribute的錯誤
      • element="div"
        • 默認是span,只顯示一個錯誤還可以
        • 整個input field的錯誤,可能不只一個錯誤,最好使用div區塊元素
      • 設定css:(紅邊匡,淺紅背景)
      • div.errors {
          background-color: #ffcccc;
          border: 2px solid red;
        }
        
      • 設定需要修正的input field,可以設定<sf:label>並新增cssErrorClass
      • <sf:form method="POST" commandName="spitter" >
          <sf:label path="firstName" cssErrorClass="error">First Name</sf:label>:
            <sf:input path="firstName" cssErrorClass="error" /><br/>
        ...
        </sf:form>
        
      • 若沒有問題的話,則render的HTML如下
      • <label for="firstName">First Name</label>
        
      • 有error的話,Label class會被設定為error
      • <label for="firstName" class="error">First Name</label>
        
      • 除此之外可以設定error css:
      • label.error {
          color: red;
        }
        input.error {
          background-color: #ffcccc;
        }
        
    • 在Spitter class中,可以再validation annotation上設定message property。可以引用更friendly的error message。message能夠定義在property document中
    • @NotNull
      @Size(min=5, max=16, message="{username.size}")
      private String username;
      @NotNull
      @Size(min=5, max=25, message="{password.size}")
      private String password;
      @NotNull
      @Size(min=2, max=30, message="{firstName.size}")
      private String firstName;
      @NotNull
      @Size(min=2, max=30, message="{lastName.size}")
      private String lastName;
      @NotNull
      @Email(message="{email.valid}")
      private String email;
      
      • 注意使用的是大括號,指的是property
    • 新增一ValidationMessages.properties在root classpath下
    • firstName.size=
          First name must be between {min} and {max} characters long.
      lastName.size=
          Last name must be between {min} and {max} characters long.
      username.size=
          Username must be between {min} and {max} characters long.
      password.size=
          Password must be between {min} and {max} characters long.
      email.valid=The email address must be valid.
      
      • key值對應在annotation中的property placeholder的value
      • 沒有hardcode
      • 也能夠在創建一個ValidationMessages.properties是針對其他國語言的
  • Spring's general tag library
    • Spring通用的tag library,需要再page上進行declare(prefix可自訂)
      • <%@ taglib uri="http://www.springframework.org/tags" prefix="s"%>
    • 可使用以下10種JSP tag
      • <s:bind>: form相關,已被淘汰
      • <s:escapeBody>
      • <s:hasBindErrors>
      • <s:htmlEscape>
      • <s:message>: internationalization
      • <s:nestedPath>
      • <s:theme>
      • <s:transform>
      • <s:url>
      • <s:eval>
    • <s:message>
      • 例如今天要將<h1>Welcome to spittr!</h1>換成國際化,則可以使用之
      • <h1><s:message code="spittr.welcome" /></h1>
        
      • 上例,會根據key為spitter.welcome的message resource render text
      • Spring有多個message resource class,都implement MessageSource interface,其中比較常見及有用的是ResourceBundleMessageSource
      • @Bean
        public MessageSource messageSource() {
          ResourceBundleMessageSource messageSource =
                  new ResourceBundleMessageSource();
          messageSource.setBasename("messages"); // 設定property doc
          return messageSource;
        }
        
      • 會試著在root path的property document中解析message
      • 另外一個方式是使用ReloadableResourceBundleMessageSource,差別在於此class能夠重新reload而不用重新編譯或啟動application
      • @Bean
        public MessageSource messageSource() {
          ReloadableResourceBundleMessageSource messageSource =
              new ReloadableResourceBundleMessageSource();
          messageSource.setBasename("file:///etc/spittr/messages");
          messageSource.setCacheSeconds(10);
          return messageSource;
        }
        
      • Basename是從外部找,而非從classpath下找
        • prefix: classpath: 從classpath下找
        • prefix: file: 指定file下找
        • prefix: 沒有,代表從web application的root path下開始找 
      • 建立property
        • default名字為message.properties,內容可設定如下
        • default名字為message.properties,內容可設定如下
        • spittr.welcome=Welcome to Spittr!
        • 西班牙文可設定property名字為message_es.properties,內容可設定如下
        • spittr.welcome=Bienvenidos a Spittr!
    • 建立URL˙
      • <s:url>建立一個相對於Servlet context的URL
      • <a href="<s:url href="/spitter/register" />">Register</a>
        
      • 假設Servlet context為"spittr",則會render html如下
      • <a href="/spittr/spitter/register">Register</a>
        
      • 也可以變成一個變數,之後在template中使用,
      • default scope是page,透過scope property,可以讓<s:url>在session, application或request中使用
      • <s:url href="/spitter/register" var="registerUrl" scope="request"/>
        <a href="${registerUrl}">Register</a>
        
      • 也可以設定parameters
      • <s:url href="/spittles" var="spittlesUrl">
          <s:param name="max" value="60" />
          <s:param name="count" value="20" />
        </s:url>
        
      • 亦可以使用在path中有parameter的URL,這個變數會參考到下面的<s:param>指定的parameter
      • <s:url href="/spitter/{username}" var="spitterUrl">
          <s:param name="username" value="jbauer" />
        </s:url>
        
      • 若使用<s:url>不想使用到hyperlink,想要讓他直接呈現在畫面上,則可以使用htmlEscape
      • <s:url value="/spittles" htmlEscape="true">
          <s:param name="max" value="60" />
          <s:param name="count" value="20" />
        </s:url>
        
      • 則render出的HTML: /spitter/spittles?max=60&amp;count=20 
      • 如果希望在Javascript中使用URL,則可將javascriptEscpae設定為true
      • <s:url value="/spittles" var="spittlesJSUrl" javaScriptEscape="true">
          <s:param name="max" value="60" />
          <s:param name="count" value="20" />
        </s:url>
        <script>
          var spittlesUrl = "${spittlesJSUrl}"
        </script>
        
      • 則結果為 var spittlesUrl = "\/spitter\/spittles?max=60&count=20"
    • Escaping content (轉譯內容)
      • <s:escapeBody htmlEscape="true">
        <h1>Hello</h1>
        </s:escapeBody>
        
      • 以上結果為&lt;h1&gt;Hello&lt;/h1&gt;
      • 也support javascript轉譯,但不能設定為variable 
      • <s:escapeBody javaScriptEscape="true">
        <h1>Hello</h1>
        </s:escapeBody>

沒有留言:

張貼留言