顯示具有 Spring 3.x MVC 標籤的文章。 顯示所有文章
顯示具有 Spring 3.x MVC 標籤的文章。 顯示所有文章

2018年1月6日 星期六

Spring、Spring MVC 設定檔整合 (Spring3.x MVC 十)

雖然可以只用 spring MVC 的設定檔就可以了,但要整合也是 OK 的,但要注意整合的問題

※整合時會出現注入兩次

spring 可以有很多設定檔,可用 web.xml,或者在設定檔使用 import 標籤將其他的設定檔抓進來,但好幾個 spring 設定檔都掃瞄一樣的包,就算設了很多次,不管使用 web.xml 或 import 標籤,都只會注入一次,以上的情形,spring MVC也是一樣

但這兩個設定檔是分開的,兩個都掃瞄一樣的包,會出現兩次


※java bean

package controller;
    
@Controller
@RequestMapping("/ooo/xxx/")
public class XxxAction {
    public XxxAction() {
        System.out.println("XxxAction");
    }
}




package service;
    
@Service
public class XxxServiceImpl {
    public XxxServiceImpl(){
        System.out.println("XxxServiceImpl");
    }
}




※設定檔

※spring 設定檔
<context:component-scan base-package="controller, service" />



※spring MVC 設定檔
<import resource="init.TestDispatcherServlet-servlet2.xml" />
<context:component-scan base-package="controller, service" />




※web.xml

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
    
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
    
<servlet>
    <servlet-name>testServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/init.TestDispatcherServlet-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
    
<servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>*.mvc</url-pattern>
</servlet-mapping>

※啟動伺服器後,會看到 console 會印出兩次 java bean 建構子的內容


※解決方式

可以使用掃瞄不同包來解決,如 spring 掃瞄 controller、spring MVC 掃瞄 service
也可以使用如下的方式:

<context:component-scan base-package="controller, service" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />
    <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" />
</context:component-scan>




<context:component-scan base-package="controller, service" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>

※use-default-filters 預設是 true,表示可掃瞄 @Controller、@Service…等的包,將它變成 false 就沒辦法掃了,然後子標籤在和它說可以掃瞄什麼類型的注解

※如果使用 annotation,可以用 @ComponentScan(useDefaultFilters = false, includeFilters = { @Filter(type = FilterType.ANNOTATION, value = App.class) })
.FilterType.ASSIGNABLE_TYPE 將指定的 class 加入 bean id
.FilterType.CUSTOM 寫個類別繼承 TypeFilter 後,即可使用




※spring 抓 spring MVC 注入的變數

spring 就像個全域變數, spring MVC 像個區域變數
區域變數知道有全域變數,但全域變數不知有區域變數
所以 spring MVC 可以抓 spring 的注入屬性;相反則不能

以上個例子來說,假設 controller 是 spring 設定檔設定的;而 service 是 spring MVC 設定的
那 service 使用 @Autowire 抓 XxxAction,伺服器啟動成功;
controller 使用 @Autowire 抓 XxxServiceImpl,伺服器啟動失敗

2018年1月2日 星期二

攔截器、例外處理 (Spring3.x MVC 九)

※攔截器

※HandlerInterceptor

public class HelloInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("A1");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("A2");
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("A3");
    }
}

※寫四支一模一樣的,分別是 ABCD 的 1~3

※preHandle 執行在目標方法之前
postHandle 執行在目標方法之後、view 之前
afterCompletion view 之後

※preHandle 回傳 false,那之後的都不會執行


※spring 設定


mapping 是映射什麼路徑
exclude-mapping 是排除路徑

<mvc:interceptors>
    <bean class="init.HelloInterceptor" />
    
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <bean class="init.HelloInterceptor2" />
    </mvc:interceptor>
    
    <mvc:interceptor>
        <mvc:mapping path="/ooo/xxx/a/b/*.mvc" />
        <bean class="init.HelloInterceptor3" />
    </mvc:interceptor>
    
    <mvc:interceptor>
        <mvc:mapping path="/**" />
        <mvc:exclude-mapping path="/ooo/xxx/a/*/*.mvc" />
        <bean class="init.HelloInterceptor4" />
    </mvc:interceptor>
</mvc:interceptors>
    
<mvc:view-controller path="ooo/xxx/b/a/c.mvc" view-name="jump1" />
<mvc:view-controller path="ooo/xxx/a/b/c1.mvc" view-name="jump1" />
<mvc:view-controller path="ooo/xxx/a/b/c2.mvc" view-name="jump1" />

※可以不加 <mvc:annotation-driven />

※HelloInterceptor、HelloInterceptor2 可以說是一樣的

※注意 exclude-mapping 最上面一定要有 mapping,否則會報錯,也就是先指定一個大範圍,然後排除,排除可以寫好幾個


※測試

<a href="ooo/xxx/b/a/c.mvc">c</a>
<a href="ooo/xxx/a/b/c1.mvc">c1</a>
<a href="ooo/xxx/a/b/c2.mvc">c2</a>

※第一個執行 ABD,其他兩個都是 ABC


※執行順序

preHandle:寫在設定檔的前面最先執行,全部的 preHandle執行完才執行 postHandle
postHandle:寫在設定檔的前面最後執行,全部的 postHandle 執行完才執行 afterCompletion
afterCompletion :寫在設定檔的前面最後執行

preHandle 是正序;postHandle、afterCompletion 執行順序是倒序



※例外處理

使用 @ExceptionHandler

※controller

@RequestMapping("exception")
public String testException(Integer i) {
    System.out.println(2/i);
    return "hello";
}
    
@ExceptionHandler(ArithmeticException.class)
public String exceptionMethod() {
    System.out.println("我錯了");
    return "errPage";
}

※回傳的字串是配合 spring 設定檔的 InternalResourceViewResolver

※測試

<a href="ooo/xxx/exception.mvc?i=0">exception</a>

※網址是不變的



※不能使用 Map

@ExceptionHandler(ArithmeticException.class)
public ModelAndView exceptionMethod(Exception ex /*, Model model*/) {
    System.out.println("我錯了" + ex);
    ModelAndView mav = new ModelAndView("errPage");
    mav.addObject("excep", ex);
    return mav;
}

※當然包括子類 Model,只是放在參數裡,都還沒有 put,就 500了,想塞值到前端,要用 ModelAndView


※多個 @ExceptionHandler

@ExceptionHandler(ArithmeticException.class)
public String exceptionMethod(Exception ex) {
    System.out.println("A");
    return "errPage";
}
    
@ExceptionHandler(Exception.class)
public String exceptionMethod2(Exception ex) {
    System.out.println("B");
    return "errPage";
}

※會以完全匹配為主,沒有完全匹配才會退而求其次


※全域的例外處理 @ControllerAdvice

@ControllerAdvice
public class GlobalException {
    @ExceptionHandler(ArithmeticException.class)
    public String exceptionMethod(Exception ex) {
        System.out.println("C");
        return "errPage";
    }
    
    @ExceptionHandler(Exception.class)
    public String exceptionMethod2(Exception ex) {
        System.out.println("D");
        return "errPage";
    }
}

※區域例外處理全部都沒有 @ExceptionHandler 才會執行全域的  @ControllerAdvice

※全域必需設定
1. <context:component-scan base-package="controller" />
    屬性 use-default-filters 不能是 false,預設是 true
2. <mvc:annotation-driven />
否則有寫也不會執行



※預設的例外處理

看原碼 DefaultHandlerExceptionResolver.doResolveException,Eclipse 可使用 Ctrl + Shift + T
以 3.2.13 版為例,會發現有 13 個預設的例外處理
NoSuchRequestHandlingMethodException
HttpRequestMethodNotSupportedException
HttpMediaTypeNotSupportedException
HttpMediaTypeNotAcceptableException
MissingServletRequestParameterException
ServletRequestBindingException
ConversionNotSupportedException
TypeMismatchException
HttpMessageNotReadableException
HttpMessageNotWritableException
MethodArgumentNotValidException
MissingServletRequestPartException
BindException

※上圖為預設的,下圖為修改過的,想修改就必需如下設定


※自訂例外

@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "我錯了")
public class DefinedSelfException extends RuntimeException {}

※HttpStatus 有很多,隨便選即可

※controller

// @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "我錯了2")
@RequestMapping("definedSelfExp")
public String definedSelfExp(Integer i) {
    if (i == 0) throw new DefinedSelfException();
    
    System.out.println(2 / i);
    return "hello";
}

※@ResponseStatus 若寫在方法上,i == 0 時不變;但若不是 0,不會跳頁,會產生例外,但寫在這裡的中文會亂碼,寫在另外一支 class 不會,這個我沒解

※測試

<a href="ooo/xxx/definedSelfExp.mvc?i=0">definedSelfExp</a>





※SimpleMappingExceptionResolver


※spring 設定檔

<bean id="simpleMappingExceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>    
            <prop key="java.lang.ArithmeticException">arithmeticPage</prop>
            <prop key="java.lang.ArrayIndexOutOfBoundsException">arrayIndexPage</prop>
        </props>
    </property>
    <property name="exceptionAttribute" value="xxx" />
</bean>
    
<mvc:default-servlet-handler />
    
<mvc:annotation-driven />

※prop 包起來的字串是頁面名稱,配合 InternalResourceViewResolver

※exceptionAttribute 如果不寫,預設是 exception,可以在 JSP 用 EL


※controller

@RequestMapping("simpleExp")
// @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR, reason = "我錯了2")
public String simpleExp(Integer i) {
    // if (i == 0) throw new DefinedSelfException();
    System.out.println(2 / i);
    return "hello";
}

※方法上的 @ResponseStatus 是有用的


※測試

<a href="ooo/xxx/simpleExp.mvc?i=0">simpleExp</a>



2017年12月31日 星期日

上傳、國際化 (Spring3.x MVC 八)

※上傳


官網說要有 commons-fileupload

※maven、gradle

compile group: 'commons-fileupload', name: 'commons-fileupload', version: '1.3.3'
-------------------------------
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>




※spring 設定檔

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" p:defaultEncoding="UTF-8" p:maxUploadSize="10240" />

※不需要 <mvc:annotation-driven />

※id 如果不是這個,就會出「Expected MultipartHttpServletRequest: is a MultipartResolver configured?」的錯,所以複製官網的就好



※controller

@RequestMapping(value = "test_upload", method=RequestMethod.POST)
public String upload(@RequestParam("file") MultipartFile file) {
    System.out.println("類型=" + file.getContentType());
    System.out.println("參數類型=" + file.getName());
    System.out.println("名稱=" + file.getOriginalFilename());
    System.out.println("大小=" + file.getSize());
    try {
        System.out.println("大小=" + file.getBytes().length);
        System.out.println("InputStream=" + file.getInputStream());
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "hello";
}

※不寫 @RequestParam,就會出「Could not instantiate bean class [org.springframework.web.multipart.MultipartFile]: Specified class is an interface」的錯


※測試

<form action="ooo/xxx/test_upload.mvc" enctype="multipart/form-data" method="post">
    <input type="file" name="file" /> 
    <input type="reset" value="重置">
    <input type="submit" value="提交">
</form>

※如果使用 GET ,就會出「The current request is not a multipart request」的錯



※國際化

※自動轉換

瀏覽器是什麼語言會自動轉換

※properties

------------------ i18n.properties ------------------
hello=default hello
mvc.lang=default value
    
    
------------------ i18n_en_US.properties ------------------
hello=hello
mvc.lang=english {0}{1}
    
    
------------------ i18n_zh_CN.properties ------------------
hello=\u4F60\u597D
mvc.lang=\u7B80\u4E2D {0}{1}
    
    
------------------ i18n_zh_TW.properties ------------------
hello=\u4F60\u597D
mvc.lang=\u7E41\u4E2D {0}{1}

※hello=你好,mvc.lang=繁中、简中


※spring 設定檔

<context:component-scan base-package="controller" />
    
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
</bean>
    
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
    p:basename="i18n" p:defaultEncoding="UTF-8" />
    
<mvc:annotation-driven />
    
<mvc:view-controller path="ooo/xxx/next1.mvc" view-name="jump1" />

※連 id 都不能改



※頁面

------------------ index.jsp ------------------
<%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    
<fmt:bundle basename="i18n">    
    <fmt:message key="hello" />
</fmt:bundle>
    
<fmt:message key="hello" />
    
<a href="ooo/xxx/next1.mvc">next1</a>
    
    
------------------ jump1.jsp ------------------
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    
<h1>jump1</h1>
<fmt:message key="mvc.lang">
    <c:choose>
        <c:when test="${not empty zero && not empty one}">
            <fmt:param value="${zero}" />
            <fmt:param value="${one}" />
        </c:when>
        <c:otherwise>
            <fmt:param value="" />
            <fmt:param value="" />
        </c:otherwise>
    </c:choose>
</fmt:message>
<a href="next2.mvc">next2</a>
    
    
------------------ jump2.jsp ------------------
<h1>jump2</h1>
${msg}<br />
<fmt:message key="mvc.lang"></fmt:message>

※fmt:bundle basename 是用 jstl 的功能

※注意 index 是第一頁,這時 spring 還沒有連到國際化的檔案,所以只使用第 9 行,就只會出現「???hello???」,按 next1 會有 request,所以再來就不用「fmt:bundle basename」了

※jump1 看起來看多,其實只是要把「{0}{1}」變不見而已

※注意 jump1 要跳到 jump2 的超連結,不用再加 ooo/xxx


※controller

package controller;
    
import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
    
@Controller
@RequestMapping("/ooo/xxx/")
public class XxxAction {
    @Autowired
    private ResourceBundleMessageSource messageSource;
    
    @RequestMapping("next2")
    public ModelAndView next2(Locale locale) {
        String zero = null, one = null, suc = null;
        if (Locale.TAIWAN.equals(locale)) {
            zero = "對";
            one = "嗎?";
            suc = "成功";
        } else if (Locale.PRC.equals(locale)) {
            zero = "对";
            one = "吗?";
            suc = "成功";
        } else if (Locale.US.equals(locale)) {
            zero = "all";
            one = " right?";
            suc = "success";
        }
    
        System.out.println(messageSource.getMessage("mvc.lang", new Object[] { zero, one }, locale));
    
        ModelAndView mav = new ModelAndView();
        mav.setViewName("jump2");
        mav.addObject("msg", suc);
        mav.addObject("zero", zero);
        mav.addObject("one", one);
        return mav;
    }
}

※注意替換 {0} 這種東西時,只有在 java 有用,顯示還是會將「{0}{1}」顯示出來,並不會在頁面替換,所以才有 zero、one 這兩個變數讓前端顯示



※手動轉換

頁面有超連結,想換什麼語言就換什麼語言,有三種,參考官網,這裡只寫 SessionLocaleResolver

※頁面

<a href="ooo/xxx/btnChangeLang.mvc?locale=en_US">english</a>
<a href="ooo/xxx/btnChangeLang.mvc?locale=zh_CN">简体中文</a>
<a href="ooo/xxx/btnChangeLang.mvc?locale=zh_TW">繁體中文</a>

※jump1 和 jump2 的頁面和自動轉換一樣


※controller

@RequestMapping("btnChangeLang")
public String btnChangeLang(Locale locale) {
    System.out.println(messageSource.getMessage("mvc.lang", null, locale));
    return "jump1";
}

※jump1 有個超連結,會到 next2 方法,方法和自動轉換一樣


※spring 設定檔

<context:component-scan base-package="controller" />
    
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
</bean>
    
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />
    
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
p:basename="i18n" p:defaultEncoding="UTF-8" />
    
<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" />
</mvc:interceptors>
    
<mvc:default-servlet-handler />
    
<mvc:annotation-driven />

※id localeResolver、messageSource 不能改

※和自動轉換比,增加了 SessionLocaleResolver 和攔截器 LocaleChangeInterceptor,最後刪除了 mvc:view-controller

2017年12月29日 星期五

JSON (Spring3.x MVC 七)

※Maven、Gradle

def jacksonVer = '2.9.3'
    
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "${jacksonVer}"
compile group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: "${jacksonVer}"
// compile group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: "${jacksonVer}"
-------------------------------------------------------
<properties>
    <jacksonVer>2.9.3</jacksonVer>
</properties>
    
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${jacksonVer}</version>
</dependency>
    
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>${jacksonVer}</version>
</dependency>
    
<!-- <dependency> -->
<!-- <groupId>com.fasterxml.jackson.core</groupId> -->
<!-- <artifactId>jackson-core</artifactId> -->
<!-- <version>${jacksonVer}</version> -->
<!-- </dependency> -->

※使用 @ResponseBody、@RequestBody 註解,不用下載這些 jar 包,還是 import 的到,但是只要其中一包沒下到,執行時都會出 406

※下載 jackson-databind 就會將 3 包都一起下載了,但 jackson-annotations 居然只有 2.9.0,所以我才將 jackson-annotations 加進去



※spring 設定檔

<mvc:default-servlet-handler />
<mvc:annotation-driven />

※一定要加 annotation-driven,不然執行時也會出 406

※default-servlet-handler 視情況加,第四篇有說明



※@ResponseBody

1.將資料轉成 JSON 後,回傳給客戶端
2.只能寫在方法上


※java bean

public class Book {
    private Integer id;
    private String name;
    private Integer price;
    private Date date;
    // setter/getter...
    
    public Book() {}
    
    public Book(Integer id, String name, Integer price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}

※注意建構子我沒加 date


※controller

@RequestMapping("resBody")
@ResponseBody
public List<Book> json() {
    List<Book> list = new ArrayList<>();
    list.add(new Book(1, "七龍珠", 50));
    list.add(new Book(2, "寶島少年", 80));
    list.add(new Book(3, "尼羅河的女兒", 70));
    return list;
}

※不加 @ResponseBody,呼叫時會出 404


※測試

$(_ => {
    $("#btn").click(_ => {
        $.ajax({ 
            url:"ooo/xxx/resBody.mvc",
            dataType:"json",
            contentType:"application/json",
            success:function(data){
                for(d of data){
                    console.info(d.id, d.name, d.price);
                    console.info("--------------------");
                }
            }
        });
    });
});
--------------------
<input id="btn" type="button" value="click me" />

※結果會發現不加的 date 也會出現,是null


※@RequestBody

1.前端傳集合過來,可使用這個標籤來接
2.使用時一定要用 POST,否則會出 400
3.只能寫在參數裡


※controller

@ResponseBody
@RequestMapping(value = "reqBody", method = RequestMethod.POST)
public List<Book> json(@RequestBody List<Book> listBook) {
    for (Book b : listBook) {
        System.out.println(b.getId());
        System.out.println(b.getName());
        System.out.println(b.getPrice());
        System.out.println("----------");
    }
    return listBook;
}

※此功能是前端傳集合過來,然後又傳回給客戶端

※@ResponseBody 一定要有,否則無法傳回,會 404



※測試

let array=[];  
array.push({"id":"1","name":"七龍珠", "price":"50"});  
array.push({"id":"2","name":"寶島少年", "price":"80"});         
array.push({"id":"3","name":"尼羅河的女兒", "price":"70"});
    
$("#btn2").click(_ => {
    $.ajax({ 
        url:"ooo/xxx/reqBody.mvc",
        type:"POST",
        dataType:"json",
        contentType:"application/json",
        data:JSON.stringify(array),
        success:function(data){
            for(d of data){
                console.info(d.id, d.name, d.price);
                console.info("--------------------");
            }
        }
    });
});
--------------------
<input id="btn2" type="button" value="按我" />





※HttpEntity

可以塞值到 HTTP Header 裡
不用寫 <mvc:annotation-driven />,也不用下載 jackson


@RequestMapping("resEntity")
public HttpEntity<String> resEntity(HttpServletRequest req) throws IOException {
    HttpHeaders head = new HttpHeaders();
    head.set("xxx", "ooo");
    
    // HttpEntity<byte[]> he = new HttpEntity<>(head);
    HttpEntity<String> he = new HttpEntity<>("bruce", head);
    return he;
}
----------------------------------------------
<a href="ooo/xxx/resEntity.mvc?">resEntity</a>

※注解那一行只有塞到 HTTP Header 裡,沒註解那一行還多一個顯示 bruce 在畫面上



※ResponseEntity

為 HttpEntity 的子類,多個狀態碼
可實現下載功能

@RequestMapping("resEntity")
public ResponseEntity<byte[]> resEntity(HttpServletRequest req) throws IOException {
    InputStream is = req.getServletContext().getResourceAsStream("/WEB-INF/xxx.txt");
    byte[] bArray = new byte[is.available()];
    is.read(bArray);
    
    HttpHeaders head = new HttpHeaders();
    head.add("Content-Disposition", "attachment;filename=downloadName.txt");
    
    return new ResponseEntity<byte[]>(bArray, head, HttpStatus.OK);
}
----------------------------------------------
<a href="ooo/xxx/resEntity.mvc?">resEntity</a>



2017年12月26日 星期二

資料挷定 (Spring3.x MVC 六)

※基本資料型別和 Wrapper、String,都能被 springmvc 轉換成功,但日期不行,會出 400且還沒有錯誤訊息

※@InitBinder

※java bean

public class Book {
    private Integer id;
    private String name;
    private Integer price;
    private Date date;
    
    // setter/getter...
}



※controller

@RequestMapping("book")
public String pojo(Book b) {
    System.out.println(b.getId());
    System.out.println(b.getName());
    System.out.println(b.getPrice());
    System.out.println(b.getDate());
    return "hello";
}
    
    
    
@InitBinder
public void binder(WebDataBinder bind) {
    bind.setDisallowedFields("id");
    
    DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
    df.setLenient(false);// 不給打數字
    bind.registerCustomEditor(Date.class, new CustomDateEditor(df, true)); // 第二個參數為可以空
}

※標 @InitBinder 的方法只能回傳 null

※第一個表示 id 設為空,所以前端塞值也沒用

※測試

<a href="ooo/xxx/book.mvc?id=99&name=book&price=500&date=2000-01-01">bind</a>





※@NumberFormat、@DateTimeFormat


@InitBinder 寫好後,每個使用上例的方式,都會去抓,例如我再寫一個 ComicBook,裡面一樣有日期欄位,且名稱也一樣,但可能一個只想要年月日,另一個是年月日時分秒,這時候可以使用這兩個 annotation,都是在 spring-context,jar 包裡的 org.springframework.format.annotation;



public class Book {
    private Integer id;
    private String name;
    
    @NumberFormat(style=Style.CURRENCY, pattern="#,###.#")
    private Integer price;
    
    @DateTimeFormat(iso=ISO.DATE, pattern="yyyy-MM-dd")
    private Date date;
    
    // setter/getter...
}

※@DateTimeFormat 裡的 iso 有預設值,這個 pattern 就是預設值,所以也可以不寫,其他預設值,點進去看有註解



※Hibernate 驗證

官網有說明,要使用必需添加 <mvc:annotation-driven/> ,然後配合 @Valid 使用,但其實還要有 jar 包,否則 import 不到,可看 Hibernate Validator 第六版的文件
hibernate-validator 6.0.7 依賴三個 jar 包,validation-api、jboss-logging、classmate

 ※validation-api 裡的 javax.validation.constraints 的 API,有 22 個,然後每個都有 List,所以總共是 44 個,但 List 我怎麼試都是 500,網上也有說可能是規格錯誤



※教學的部分,我發現 4.3 版翻譯的較完全

※以下是看文檔翻譯的,我沒有每個都試過

空:

@Null:必需是 null
@NotNull:不能是 null
@NotBlank:不能是 null 或全是空格,只支援 CharSequence
@NotEmpty:不能是 null 或空,支援 CharSequence Collection Map Array


布林:

@AssertTrue:必需是 true
@AssertFalse:必需是 false


數字:

.@Mix、@Max 支援 6 種格式,byte short int long BigInteger BigDecimal


@Min(number):最小數字是什麼,所以必需是數字,而且要 >= 設定的數字,@Null是有效的

@Max(number):最大數字是什麼,所以必需是數字,而且要 <= 設定的數字,@Null是有效的



.@DecimalMin、@DecimalMax、@Digits 支援 7 種格式,byte short int long BigInteger BigDecimal CharSequence,要注意,並沒有 float double


@DecimalMin(number):最小數字是什麼,所以必需是數字,而且要 >= 設定的數字,@Null是有效的

@DecimalMax(number):最大數字是什麼,所以必需是數字,而且要 <= 設定的數字,@Null是有效的


.範圍

@Digits:(integer, fraction):必需是數字,且介於指定的範圍,@Null是有效的

@Size(min, max):必需介於指定的範圍,支援 CharSequence Collection Map array



正負數:

有四種,支援 8 種格式,byte short int long float double BigInteger BigDecimal
@Positive:必需為正數且不是0,@Null是有效的
@PositiveOrZero:必需為正數,0有效,@Null是有效的
@Negative:必需為負數且不是0,@Null是有效的
@NegativeOrZero:必需為負數,0有效,@Null是有效的

日期:

有四種,支援 16 種 格式
java.util.Date
java.util.Calendar
java.time.Instant
java.time.LocalDate
java.time.LocalDateTime
java.time.LocalTime
java.time.MonthDay
java.time.OffsetDateTime
java.time.OffsetTime
java.time.Year
java.time.YearMonth
java.time.ZonedDateTime
java.time.chrono.HijrahDate
java.time.chrono.JapaneseDate
java.time.chrono.MinguoDate
java.time.chrono.ThaiBuddhistDate

@PastOrPresent:必需是過去日期(包括現在日期),@Null是有效的
@Past:必需是過去日期,@Null是有效的
@Future:必需是未來日期,@Null是有效的
@FutureOrPresent:必需是未來日期(包括現在日期),@Null是有效的


其他:

@Pattern(regexp):必需合乎正則,只支援 CharSequence,@Null是有效的
@Email(regexp):必需是 email,只支援 CharSequence


※每個 annotation 都一定會有的三個屬性,message、groups、payload
message 是驗證不過時要顯示在控制台的訊息
其他兩個我沒看懂,參考這裡


※範例

設定好<mvc:annotation-driven/>後,如下

※java bean

public class Book {
    @Max(value=5, message="數字太大")
    // @Max.List({@Max(5),@Max(15),@Max(10)})
    private Integer id;
    
    @NotNull(message="不能為空")
    private String name;
    
    private Integer price;
    
    private Date date;
    
    // setter/getter...
}

※List 沒有試成功過

※controller

@RequestMapping("book")
public String pojo(@Valid Book b, BindingResult br) {
    System.out.println(b.getId());
    System.out.println(b.getName());
    System.out.println(b.getPrice());
    System.out.println(b.getDate());
    
    if(br.getErrorCount() > 0) {
        /*
        for(ObjectError oe:br.getAllErrors()) {
            System.out.println(oe.getObjectName());
            System.out.println(oe.getDefaultMessage());
    
            System.out.println();
            System.out.println("Arguments");
            for(Object o:oe.getArguments()) {
                System.out.println(o);
            }
    
            System.out.println();
            System.out.println("Codes");
            for(String s:oe.getCodes()) {
                System.out.println(s);
            }
        }
        */
    
        System.out.println("FieldErrors");
        for(FieldError fe:br.getFieldErrors()) {
            System.out.println(fe.getDefaultMessage());
            return "error";
        }
    }
    
    return "hello";
}

※<a href="ooo/xxx/book.mvc?id=10&name=book&price=100&date=2000-01-01">bind</a>

※如果不寫  @Valid,那 java bean 有寫跟沒寫一樣

※使用 BindingResult 可以判斷有沒有錯,跳轉到其他頁面



※hibernate-validator 裡 org.hibernate.validator.constraints 的 API


也有很多,只列出幾個常用的,標為棄用,我是用第六版試的,有可能第五版是 ok 的

@Length(min, max):必需介於指定的範圍
@Range(min, max):必需介於指定的範圍,應用在數字和字串
@CodePointLength(min, max):字串的長度必需在指定的範圍
@URL:必需是網址
@NotEmpty:標為棄用
@NotBlank:標為棄用
@Email:標為棄用

2017年12月23日 星期六

轉換器 (Spring3.x MVC 五)

.指的是前端發出 request 到 controller 的轉換,spring 已經幫我們實作很多常用的,但如有特殊需求,就要自己寫了

官網連接 ,可以看出有三種方式

.目前只寫 converter,其他兩種有空再寫


※converter

※Java Bean

public class Book {
    private Integer id;
    private String name;
    private Integer price;
    // setter/getter...
    
    public Book() {}
    
    public Book(Integer id, String name, Integer price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}

※假設前端傳三個值為 id@name@price 這樣的格式,所以我在此類有三個屬性

※轉換類

@Component
public class TestConverter implements Converter<String, Book> {
    @Override
    public Book convert(String source) {
        String[] array = source.split("@");
        return new Book(Integer.valueOf(array[0]), array[1], Integer.valueOf(array[2]));
    }
}



※spring 設定檔
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean" p:converters-ref="testConverter" />
<mvc:annotation-driven conversion-service="conversionService" />

※conversion-service 不加會 500


※測試

@RequestMapping("book")
public String pojo(@RequestParam("xxx") Book b) {
    System.out.println(b.getId());
    System.out.println(b.getPrice());
    System.out.println(b.getName());
    return "hello";
}
------------------------------
<a href="ooo/xxx/book.mvc?xxx=123@toy@500">bookTransfer</a>

※如果不合乎自訂的轉換要求,會出現 400,描述為「The request sent by the client was syntactically incorrect.

※注意:如果前端傳 Book 的屬性,如 id、name、price 只是給 Book 塞值,並不會去轉換



※傳特殊字元

public void bruceTest() throws UnsupportedEncodingException {
    final String encode = "UTF-8";
    final String and = URLEncoder.encode("&", encode);
    System.out.println(and);
    System.out.println(URLDecoder.decode(and, encode));
}
------------------------------
const and = encodeURIComponent("&");
console.log(and);
console.log(decodeURIComponent(and));

※上面是 java,下面是javascript,使用此方式,可以知道網址列的特殊字元的編碼為何

※假設想傳 ooo/xxx/book.mvc?xxx=123&toy&500,但「&」是 get 請求的關鍵字,所以要用「%26」取代,變成 ooo/xxx/book.mvc?xxx=123%26toy%26500

2017年12月22日 星期五

自定義 View、加載靜態資源 (Spring3.x MVC 四)

※自定義 View


※DefinedSelfView.java

@Component
public class DefinedSelfView implements View{
    @Override
    public String getContentType() {
        return "text/html";
    }
    
    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        response.setContentType("text/html; charset=UTF-8");
        response.getWriter().println("耶!自定義的 View 耶");
    }
}

※繼承 View 後,實作兩個方法


※設定檔

<context:component-scan base-package="controller,view"></context:component-scan>
    
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
</bean>
    
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="50" />

※尤於有多個 ViewResolver,所以要加上 order,這個數字是較小的優先,InternalResourceViewResolver 的父類 UrlBasedViewResolver 已經有定義 order = Integer.MAX_VALUE;,所以基本上,隨便打個數字都比它優先


※測試

@Controller
@RequestMapping("/ooo/xxx/")
public class XxxAction {
    @RequestMapping("definedSelfView")
    public String getView() {
        return "definedSelfView"; // bean id
    }
}
------------------------------
<a href="ooo/xxx/definedSelfView.mvc">definedSelfView</a>





※加載靜態資源 

官網
在首頁加載 jquery,如果 web.xml 是全部給 spring 處理時,在伺服器起動時,會發現「No mapping found for HTTP request with URI [/SpringMVC/jquery-3.2.1.js] in DispatcherServlet with name 'testServlet'」,當然可用副檔名的方式來避免這個問題,但如果不想用這招,還可以用以下的方式

※web.xml

<servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

※全部的資源都給 spring 處理,不管是給container處理、還是給 spring 管理,都必需這麼做才能體現他的功能


※給 container 處理

※XML 設定

<mvc:default-servlet-handler default-servlet-name="default" />
<mvc:annotation-driven />

※使用 default-servlet-handler 可將靜態資源交給 container 處理,預設名稱是 default,如此例的 default-servlet-name 可不用寫,大部分的 container 都會取名為 default,如果不是就加這個屬性

※本來不加 default-servlet-handler 是可以處理 @RequestMapping 的,但加了 default-servlet-handler,annotation-driven 不加的話,那就無法處理  @RequestMapping,所以一加就兩個都要加才行,看官網


※annotation 設定

@Component
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

※如果預設名稱不是 default,可以寫在 enable() 裡面


※tomcat 裡的 web.xml

<servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
    <init-param>
        <param-name>debug</param-name>
        <param-value>0</param-value>
    </init-param>
    <init-param>
        <param-name>listings</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

※這只是從 tomcat 裡的 web.xml 貼出來參考,並不用改



※給 spring 管理


※紅框由上而下,分別是 靜態資源、spring設定檔、測試頁

※XML 設定

<mvc:resources mapping="/xxx/**" location="/WEB-INF/static/" />

※location 裡寫的是真實路徑,可用 classpath

※mapping 如有多個,可用逗號隔開

※這個意思表示 /WEB-INF/static 底下的目錄可用 xxx 取代


※測試

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    
<script src='<c:url value="/xxx/js/jquery-3.2.1.js" />'></script>
    
<!-- <script src="xxx/js/jquery-3.2.1.js"></script> -->
    
$( _ => {
    alert("xxx");
});

※註解那一行也行,就看你要不要用 JSTL 了,前面有「/」的差別


※annotation 設定

@Component
@EnableWebMvc
public class Xxx extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/xxx/**").addResourceLocations("/WEB-INF/static/");
    }
}

※addResourceLocations 是 ... 參數,所以可以寫多個

※和 container 的 annotation 設定很像,差在覆寫不同的方法

2017年12月4日 星期一

@SessionAttributes、@ModelAttribute (Spring3.x MVC 三)

※@SessionAttribute

@Controller
@RequestMapping("/ooo/xxx/")
@SessionAttributes(value = {"xxx", "ooo"}, types = Date.class)
public class XxxAction {
    @RequestMapping(value = "session/*.mvc")
    public String session(Model model) {
        model.addAttribute("xxx", 5);
        model.addAttribute("ooo", "o");
        model.addAttribute("aaa", new Date(100,1,1));
        model.addAttribute("bbb", new Date(101,12,12));
        return "hello";
    }
}

※value 是 key 的名稱,type 表示某個類型的也會放入 session

※只能寫在 type 上,是針對整個 controller

※參數裡,Model、Map 都可以,其他沒試過

※測試

---------- index.jsp ----------
<a href="ooo/xxx/session/attr.mvc">session</a>


---------- hello.jsp ----------
request xxx:${requestScope.xxx}<br />
request ooo:${requestScope.ooo}<br />
request aaa:${requestScope.aaa}<br />
request bbb:${requestScope.bbb}<br />
<br />
session xxx:${sessionScope.xxx}<br />
session ooo:${sessionScope.ooo}<br />
session aaa:${sessionScope.aaa}<br />
session bbb:${sessionScope.bbb}<br />




※常見錯誤

@SessionAttributes("book")
public class XxxAction {
    @RequestMapping(value = "session/*.mvc")
    public String session(Model model, Book book) {
        model.addAttribute("xxx", 5);
        // model.addAttribute("book", "b");
        return "hello";
    }
}

※不管有沒有註解那一行,都會出「org.springframework.web.HttpSessionRequiredException: Session attribute 'book' required - not found in session」的錯

※因為參數 Book,有一個隱藏的 @ModelAttribute("book"),只要將裡面的值和 @SessionAttributes 的屬性值不一樣即可

※另外一個解決辦法就是使用下面要介紹的 @ModelAttribute,Map 裡放 book 也可以解決



※@ModelAttribute

※這個 annotation 是針對所有的 controller,每次都會先調用,所以要考慮清楚再寫,調用完後,才執行前端的方法

※只有一個屬性 value,是 String,而不是 String[],所以是 1 對 1 的關係,可以在 controller 裡寫很多的 @ModelAttribute,都會在開始前被調用

※可以寫在方法上和參數裡
方法上的 value:沒寫為 void
參數裡的 value:沒寫為參數型態的駝峰命名

参數上:req 的參數依名稱注入到指定物件裡,而且還會將這個物件自動加入 Model
方法上:@RequestMapping 方法前執行,如有返回值,會自動將返回值加入到 Model


public class Book {
    private Integer id;
    private String name;
    private Integer price;
    
    // setter/getter...
    
    public Book() {}
    
    public Book(Integer id, String name, Integer price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }
}




※controller

@ModelAttribute
public void getBook(@RequestParam(value="id", required=true) Integer id, Integer price, Model model) {
    model.addAttribute("book", new Book(id, "bruce", 1111)); // 模擬 DB
}
    
@RequestMapping("book")
public String pojo(Book book) {
    System.out.println(book.getId());
    System.out.println(book.getPrice());
    System.out.println(book.getName());
    return "hello";
}

※注意 model 的 key 必需是類名的駝峰命名法,如 ComicBook,那就要取名為 comicBook

※如果想自己取名,如 model.addAttribute("uuu", ...); 那 pojo 的參數也要宣告 @ModelAttribute("uuu"),也就是說,pojo 不寫 @ModelAttribute("uuu"),預設是@ModelAttribute("參數名的駝峰命名"),兩個對應就可以,沒對應不會報錯,只是替換沒成功

※測試

---------- index.jsp ----------
<a href="ooo/xxx/book.mvc?id=99&price=77">modelAttr</a>


---------- hello.jsp ----------
${requestScope.book.id}<br />
${requestScope.book.price}<br />
${requestScope.book.name}<br />

※此例是模擬前端傳 id 到 DB 取出對應的值,但我只想改 price,name 並沒有給,所以是 null,但我不給的意思是想要和 DB 一樣,所以就寫了@ModelAttribute

DB 取出來的 price 是 1111,然後再到 pojo 方法,此時會將前端傳過來的值覆蓋 DB 的值


※@ModelAttribute 的屬性

只有一個屬性 value,它的值可以用在前端

@ModelAttribute("abc")
public Book getBook() {
    return new Book(88, "bruce", 100);
}
    
@RequestMapping("book")
public String pojo(Book book) {
    return "hello";
}

※必須要 return 才行

※測試

---------- index.jsp ----------
<a href="ooo/xxx/book.mvc">modelAttr</a>


---------- hello.jsp ----------
${abc.id}<br />
${abc.price}<br />
${abc.name}<br />

好文章



※原理

在 Map 的地方下斷點,debug 調試後,HandlerMethodInvoker.java 有二個較重要的方法如下:

一、invokeHandlerMethod

有兩個迴圈,這個方法主要在處理 annotation

第一個迴圈:
如果有 @SessionAttributes 的 value 和 types 就將它們放入 ExtendedModelMap 裡,所以至少要執行過一次,瀏覽器才有

第二個迴圈:
@ModelAttribute 可以寫在方法上和參數裡,這個方法裡都是方法上的,有兩個重要變數

⑴ attrName:@ModelAttribute 的 value 屬性,沒有為 void,最後成為 ExtendedModelMap 的 key

⑵ attrValue:回傳的內容,沒回傳為 null,最後成為 ExtendedModelMap 的 value

1.resolveHandlerArguments 取得的是前端傳過來的資料,但 @ModelAttribute 裡的參數要有,不然是空,看方法二
2.如果 attrName 在 ExtendedModelMap 找到就下一個迴圈了,不會再繼續執行 3 之後的程式碼
3.回 controller 執行此迴圈的 @ModelAttribute 方法得到 attrValue
4.如果 @ModelAttribute 沒寫 value,那值就是 void
5.如果 ExtendedModelMap 沒有 attrName,就塞入
迴圈結束

再呼叫一次第二個迴圈裡的 1 方法,但是是針對 @RequestMapping 的,肯定只有一個,所以寫在迴圈外,回傳的是前端覆蓋 @ModelAttribute 方法裡的值


二、resolveHandlerArguments

迴圈裡還有迴圈,這個方法主要在處理方法的參數的 annotation

外層迴圈:方法有幾個參數就跑幾次,整個方法除了 return 外,都是外層迴圈

內層迴圈:每一個參數有幾個 annotation 就跑幾次,只有以下 8 種 annotation 才會處理
1.RequestParam
2.RequestHeader
3.RequestBody
4.CookieValue
5.PathVariable
6.ModelAttribute
7.Value
8.Valid 開頭的,spring3 只有 Validated
內層迴圈結束

以下還是在外層迴圈內,內層迴圈外
1~6 的 annotation,每一個參數最多只能給一個,否則會報「Handler parameter annotations are exclusive choices - do not specify more than one such annotation on the same parameter: 」 + 方法名

如果參數沒有 1~6 的 annotation,還有三個判斷
第一個我沒看懂
第二個是判斷有 @Valid 的 value 屬性就將參數塞到回傳的變數 args 裡
如果不是1、2 就是這一個,又分成 6 個判斷
⑴ 參數型態是 Model 或 Map :是的話再判斷是不是 ExtendedModelMap
是就將 ExtendedModelMap 塞到回傳的變數 args 裡
不是就拋「Argument [參數型態] is of type Model or Map but is not assignable from the actual model. You may need to switch newer MVC infrastructure classes to use this argument.」
⑵是 SessionStatus 或其父類,將變數 sessionStatus 塞到 args 裡
⑶是 HttpEntity 或其父類,呼叫 resolveHttpEntityRequest 後回傳給 args
⑷是 Errors 或其父類,拋「Errors/BindingResult argument declared without preceding model attribute. Check your handler method signature!」
⑸基本類型和Wrapper、enum、CharSequence、Number、Date、URI、URL、Locale、Class,只要是其中一個,就將變數 paramName 置為空
⑹都不是變數attrName為空

最後的部分還有 6 個判斷,前 5 個都是針對 annotation 的 value 屬性,有就呼叫相對應的方法後,回傳到 args
第 6 個是 @ModelAttribute 沒寫在參數裡或者寫了 value 值才會進去,
主要就是取得 WebDataBinder 後,有個 getTarget 方法,塞到 args 裡

2016年1月30日 星期六

Java Bean、參數、返回值 (Spring3.x MVC 二)

※Java Bean


public class Book {
    private int id;
    private String name;
    private ComicBook comicBook;
    
    // setter/getter...
}
------------------------------
public class ComicBook {
    private String name;
    
    // setter/getter...
}




@RequestMapping("book")
public String pojo(Book book) {
    System.out.println(book.getId());
    System.out.println(book.getName());
    System.out.println(book.getComicBook().getName());
    return "hello";
}

※傳到 controller 後,只要名稱符合 java bean 裡面的名字,就會抓的到


<a href="ooo/xxx/book.mvc?id=99&name=漫畫&comicBook.name=七龍珠">pojo</a>





※接受的參數

官方文件有說明,有一些上一篇也說過了,

Servlet API 

有 9 個:
.ServletRequest
.ServletResponse
.HttpSession
.Principal
.Locale
.InputStream
.Reader
.OutputStream
.Writer

@RequestMapping("api")
public String api(
    ServletRequest req, 
    ServletResponse res, 
    HttpSession session, 
    Principal principal, 
    Locale locale,
    InputStream is, 
    // Reader reader, 
    // OutputStream out,
    Writer writer) {
    
    System.out.println(req);
    System.out.println(res);
    System.out.println(session);
    System.out.println(principal);
    System.out.println(locale);
    System.out.println(is);
    // System.out.println(reader);
    // System.out.println(out);
    System.out.println(writer);
    return null;
}

※注意 XXX has already been called for this response,IO 的 InputStream 和 Reader 只能選其中一個; OutputStream 和 Writer 也只能選其中一個

※在 controller 第一行下繼點後,下面第 2 個 Annotation

※第二個 for  迴圈裡 resolveHandlerArguments 會看到判斷 annotation 的程式碼
RequestParam
RequestHeader
RequestBody
CookieValue
PathVariable
ModelAttribute
Value
Valid 開頭的
所以在參數裡,可以用以上這幾個 annotation

再往下看有一個 if,裡面有個方法 resolveCommonArgument
進去後,再找到 resolveStandardArgument,按 Open Implementation 就能看到這 9 個判斷

※測試
<a href="ooo/xxx/api.mvc">servlet api</a>


※Model

@RequestMapping("model")
public String model(Model model) {
    System.out.println(model.getClass().getName());
    model.addAttribute("m", Stream.of("ddd", "eee", "fff").collect(Collectors.toList()));
    return "hello";
}



※測試
---------- index.jsp ----------
<a href="ooo/xxx/model.mvc">model</a>
    
    
---------- hello.jsp ----------
m:${requestScope.m}



※Map

@RequestMapping("map")
public String map(Map<String, Object> map) {
    map.put("ooo", Stream.of("aaa", "bbb", "ccc").collect(Collectors.toList()));
    return "hello";
}



※測試
---------- index.jsp ----------
<a href="ooo/xxx/map.mvc">map</a>
    
    
---------- hello.jsp ----------
ooo:${requestScope.ooo}

※ModelMap 當然也可以,因為它是 Map 的子類

※從ExtendedModelMap 分開來的



※接受的返回值


官方文件


※ModelAndView

@RequestMapping("modelView")
public ModelAndView mv() {
    ModelAndView mav = new ModelAndView();
    // mav.setViewName("/WEB-INF/hello.jsp");
    mav.setViewName("hello");
    mav.addObject("xxx", "yeah");
    return mav;
}

※setViewName 的內容和 上一篇說的 InternalResourceViewResolver (Spring 設定檔裡) 有關

※也可使用建構子 new ModelAndView("hello");

※測試
---------- index.jsp ----------
<a href="ooo/xxx/modelView.mvc">modelAndView</a>
    
    
---------- hello.jsp ----------
xxx:${requestScope.xxx}



2016年1月28日 星期四

HelloWorld (Spring3.x MVC 一)

此專案使用 xml 加 annotation 架設 SpringMVC
這次使用 gradle,創建好 gradle 後,預設不是 Web 的,使用以下方式變成 Web


※變 Web 專案

專案按右鍵 Properties


產生 Web.xml



增加 jar 檔

※build.gradle

apply plugin: 'java-library'
    
repositories {
    jcenter()
}
    
def springVer = '3.2.13.RELEASE'
    
dependencies {
    api 'org.apache.commons:commons-math3:3.6.1'
    implementation 'com.google.guava:guava:21.0'
    
    testImplementation 'junit:junit:4.12'
    compile 'org.springframework:spring-context:"${springVer}"'
    compile group: 'org.springframework', name: 'spring-webmvc', version: "${springVer}"
    // compile group: 'javax.servlet.jsp.jstl', name: 'jstl', version: '1.2'
    compile group: 'javax.servlet', name: 'jstl', version: '1.2'
}

※之後只要有改,就要在專案按右鍵做如下的設定,才會更新 build.gradle

※web.xml

<servlet>
    <servlet-name>testServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/testServlet-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
    
<servlet-mapping>
    <servlet-name>testServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

※根據官方文件的第二張圖 (Context hierarchy in Spring Web MVC) 下面,第一段有說明 [servlet-name]-servlet.xml in the WEB-INF,表示預設會在 WEB-INF 下找 [servlet-name]-servlet.xml,以此例來說就是 testServlet-servlet.xml,如果是叫這個名字且放在這個目錄下,那就可以不寫 init-param

※如果想用 annotation,本篇最下面有


※testServlet-servlet.xml

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




※XxxAction.java

@Controller
public class XxxAction {
    @RequestMapping("/ooo/xxx/*.mvc")
    public String hello() {
        System.out.println("hello mvc");
        return "/WEB-INF/hello.jsp";
    }
}

※只要是/ooo/xxx/開頭且是 mvc 結尾就會進來這個方法,進來後導向到另一支 jsp


※index.jsp

<a href="ooo/xxx/hello.mvc">Hello World</a>

※也可用 form,但網址是 GET,所以也可以用這個方式連後端


※@RequestMapping 

可寫在類和方法上,兩個都寫可以合併,如上例可改成下面二種方式

@Controller
@RequestMapping("/ooo")
public class XxxAction {
    @RequestMapping("/xxx/*.mvc")
    public String hello() {
        System.out.println("hello mvc");
        return "/WEB-INF/hello.jsp";
    }
}

@Controller
@RequestMapping("/ooo/xxx/")
public class XxxAction {
    @RequestMapping("*.mvc")
    public String hello() {
        System.out.println("hello mvc");
        return "/WEB-INF/hello.jsp";
    }
}


※處理 view

<bean id="jspViewResolver"
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <!-- <property name="viewClass" -->
    <!-- value="org.springframework.web.servlet.view.JstlView" /> -->
    <property name="prefix" value="/WEB-INF/" />
    <property name="suffix" value=".jsp" />
</bean>

※可在 testServlet-servlet.xml 增加這段,可參考官網

※JstlView 需要 JSTL 的 jar 包,但此例可以不加這個屬性,只要一加上,也不用寫上面那個 viewClass屬性,DispatcherServlet.resolveViewName,裡面的迴圈有個 View,會回傳 JstlView,原本是 InternalResourceView
因為 InternalResourceViewResolver 的預設建構子有寫


※上面的 gradle.build 有寫兩個,要小心註解那一個是有問題的,會出找不到 jstl 的 class

※設定好後,controller 只要回傳 hello 即可


※編譯及執行問題

編譯時找不到 Servlet,可在專案按右鍵 Build Path >> Configure Build Path >> Libraries 活頁標籤裡的 Add Library,然後如下操作



運行時出現找不到 DispatcherServlet 的 class,可如下操作

※tomcat 還要打包到指定的路徑上,這裡不選 ,jar 包就不會包到指定的地方,此時就找不到jar 檔而報錯,所以只要將 gradle 下載的 jar 選起來即可


※@RequestMapping

※value 支援三種通配符:?、*、**

※@RequestMapping(value = "*.mvc", method = RequestMethod.POST, params = { "xxx", "ooo=9" }, headers="Host=localhost:9080")

※params 表示一定要有屬性 xxx 和 ooo,且屬性 ooo一定要是 9 的,且 request header 的 Host 一定要是 localhost:9080 才可以請求,否則 404

※param 裡的都是字串,只能用「=」、「!=」,沒有正則表達式

※注意如果 params 過了,但 headers 沒通過,錯誤訊息還是顯示 params 沒通過

※瀏覽器可看到 header 的訊息,以 Chrome 為例



※@RequestParam、@RequestHeader、@CookieValue

@RequestMapping(value = "abc/*.mvc")
public String hello(
    @RequestParam(value = "id", defaultValue = "0") int i,
    @RequestParam(value = "name", required = false) String n,
    @RequestHeader(value = "Accept-Language") String alang, 
    @CookieValue("JSESSIONID") String cookie) {
    
    System.out.println(i + ":" + n + ":" + alang + ":" + cookie);
    return "hello";
}

※這三個 annotation 裡的屬性一樣,用法也差不多,也都只能寫在參數裡

※假設 hello(@RequestParam(value="id") int id),因為都叫 id,所以 annotation 可以不寫,主要是用在 defaultValue 和 required 這兩個屬性要設定時

※注意如果是基本型態,沒有 null,required 設定 false,還是會 400,可以改成 Wrapper 型態或者使用 defaultValue

※@RequestHeader 的 value 一樣是看上一張圖

※@CookieValue 的 value 看上一張圖的 Cookie

※測試
<a href="ooo/xxx/abc/bruce.mvc?id=99&name=bruce">param</a>




※@PathVariable

※只能寫在參數裡


@RequestMapping("abc/{name}")
public String pathVar(@PathVariable("name") String n) {
    System.out.println(n);
    return "hello";
}

※{name} 必需和 @PathVariable 裡的字對應



<a href="ooo/xxx/abc/bruce">param</a>
<a href="ooo/xxx/abc/bruce.mvc">param</a>

※如果 web.xml 設定附檔名對應,就寫第二種;如果全部都給 spring 處理,就寫第一種,抓到的都是 bruce



※其他方式

※mvc:view-controller

假設 controller 沒做什麼事,只是重導而已,可以用這個,就不用寫 controller 了
官網連結

※testServlet-servlet.xml

<mvc:view-controller path="ooo/xxx/view.mvc" view-name="hello" />
<mvc:annotation-driven />

※path 為請求的網址,和 @RequestMapping 不同,如果 web.xml 設定 *.mvc,在這裡一定要打才行,但 @RequestMapping 可打可不打

※view-name 會配合 InternalResourceViewResolver 設定的路徑

※annotation-driven 不寫時,ooo/xxx/view.mvc 這個請求沒有問題,但其他的,如上面有很多的其他請求會 404,加上這個才可以正常訪問

※如果這個設定和 controller 都寫了,會以 controller 為主


※重導

@RequestMapping("view")
public String takeView() {
    System.out.println("yeah");
    return "forward:/WEB-INF/hello.jsp";
    //return "redirect:/success.html";
}
------------------------------
<a href="ooo/xxx/view.mvc">view</a>

※使用重導的方式,InternalResourceViewResolver 是沒有作用的

.直接請求重導 forward
A想拿東西給B,一進門看到C,C會轉交給B,有沒有轉交成功,A都會知道
伺服器端很忙,因為何服器會去找,所以可以找到WEB-INF之下的資源,所以網址不變
使用 RequestDispatcher


.間接請求重導 redirect
A想拿東西給B,一進門看到C,C叫他自己去找B
用戶端很忙,因為是用戶端,所以找不到WEB-INF之下的資源,所以網址會變
使用 HttpServletRequest,很明顯是用戶端


※不寫 web.xml 


@WebServlet(
    urlPatterns = "*.mvc", 
    initParams = @WebInitParam(
        name = "contextConfigLocation", 
        value = "/WEB-INF/applicationContext.xml"
    )
)
public class TestDispatcherServlet extends DispatcherServlet {}

※隨便寫個類實作 DispatcherServlet,然後如上代碼,注意 @WebInitParam 寫在 @WebServlet 裡面才有用

※如果不寫  @WebInitParam,那預設的 spring 設定檔名是「套件名.類名-servlet.xml」,此例為 init.TestDispatcherServlet-servlet.xml

※又如果不想用 applicationContext,直接使用 annotation,可如下設定
@WebServlet(
    urlPatterns = "/", 
    initParams = {
        @WebInitParam(name = "contextConfigLocation", value = "controller.XxxAction"),
        @WebInitParam(name = "contextClass", value = "org.springframework.web.context.support.AnnotationConfigWebApplicationContext") })
public class TestDispatcherServlet extends DispatcherServlet {}