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月15日 星期五

Windows 批次檔-迴圈、if

※迴圈

help for 看文檔
在批次檔裡變數用兩個 %;cmd只要一個 % 即可
變數名有區分大小寫,其他沒有
如果批次檔沒有寫好,會閃一下就結束了


※檔案(不加參數)

@echo off
::列出當前路徑的所有檔案名稱,只有檔名
::for %%i in (*) do echo %%i
for %%i in (*) do (
    echo %%i
)
    
echo.
echo.
    
::列出指定路徑下的所有副檔名為txt的檔案名稱,路徑+檔名
for %%j in ("C:\Program Files\Java\jdk1.8.0_121\*.txt") do echo %%j
pause

※如有多個指令要操作,可像此例用「( )」包起來

※do 後面不能換行,至少要有「(」


※目錄(/d):directory

@echo off
::列出當前路徑的所有目錄名稱,只有目錄名稱
for /d %%i in (*) do echo %%i
    
echo.
echo.
    
::列出指定路徑的所有目錄名稱為j開頭的,路徑+目錄名
for /d %%j in ("C:\Program Files\Java\jdk1.8.0_121\j*") do echo %%j
pause




※遞迴(/r):recursive

@echo off
::列出當前路徑的所有檔案名稱,路徑+檔名
for /r %%i in (*) do echo %%i
    
echo.
echo.
    
::列出當前路徑的所有目錄名稱,路徑+目錄名且以\.結尾
for /r %%j in (.) do echo %%j
    
echo.
echo.
    
::列出指定路徑下的所有副檔名為txt的檔案名稱,路徑+檔名
for /r "C:\Program Files\Java\jdk1.8.0_121" %%k in (*.txt) do echo %%k
pause

※檔案和目錄都只有操作一層而已,但用遞迴可以從最外層到最裡層拆開


※列出(/L):list

@echo off
::列出-5到100,每次增加5的數字
for /L %%i in (-5,5,100) do echo %%i
    
echo.
echo.
    
::新增xxx1~xxx3的目錄
for /L %%j in (1,1,3) do mkdir xxx%%j && echo 新建xxx%%j目錄成功
pause

※第一和第三個參數是開始和結束,中間是增加或減少,顯示出來的是數字

※L 最好用大寫,避免混淆



※檔案內容(/f)

一行一行的抓取檔案內容,要注意 delims 的預設是空格、換行
( )裡有三種 1.檔名  2.命令(用`包起來)  3.字串(用"包起來)

※test.txt

aaa,
bbb,
ccc,
ddd,
eee,
fff,
ggg,
hhh,
iii,
jjj,



※test2.txt

kkk,
lll,
mmm,
nnn,
ooo




::基本款,多檔用空格隔開
for /f %%i in (test.txt test2.txt) do echo %%i
    
::直接寫字串,但我不知道怎麼寫換行
for /f %%i in ("abcde") do echo %%i
    
::如果檔名有空格,必須用"包起來,但會被誤認為字串,所以前面加上 usebackq 即可
for /f usebackq %%i in (test.txt "test 2.txt") do echo %%i
    
::使用命令時也一樣,一定會有空格,所以也要加上 usebackq
for /f usebackq %%i in (`type test.txt`) do echo %%i
    
::換行改成用,,這樣就不是預設的空格、換行了
for /f "delims=," %%i in (test.txt test2.txt) do echo %%i
    
::每個檔案忽略前三行
for /f "skip=3" %%i in (test.txt test2.txt) do echo %%i
    
::可以混合使用
for /f "delims=,skip=3" %%i in (test.txt test2.txt) do echo %%i
    
::忽略 f 開頭
for /f "eol=f" %%i in (test.txt test2.txt) do echo %%i
    
::特殊語法
for /f %%i in (test.txt test2.txt) do echo %%~di%%i

※特殊語法可參考文檔



※tokens

行中的內容

※test3.txt

a b c d e f g h i j
k l m n o p q r s t


::取2到5個(注意預設是以空格換行區分第幾個),2到5有4個變數,直接用即可,不用宣告
for /f "tokens=2-5" %%i in (test3.txt) do echo %%i %%j %%k %%l
    
::取第2個之後,%%j表示第3個到最後
for /f "tokens=2*" %%i in (test3.txt) do echo %%i %%j
    
::取2和4,然後7到9,所以有5個變數
for /f "tokens=2,4,7-9" %%i in (test3.txt) do echo %%i %%j %%k %%l %%m

※變數宣告為%%i,注意後面是依序的,如%%i、%%j、%%k…
又如果是宣告為%%a,那後面自然就是%%b、%%c…

※變數不一定要拿來用,也不一定要先印 i 才能印 j,隨自己需求



※if


@echo off
setlocal
    
set a=apple
set b=banana
    
if /i "%a%" == "APPLE" (
    echo is apple
) else (
    echo is not apple
)
    
if not "%b%" == "banana" (
    echo is not banana
) else (
    echo is other
)
    
endlocal
pause

※/i 是忽略大小寫,注意繁中版的文檔寫錯了;not 可判斷相反的情形

※/i 和 not 都不是必要的

※判斷除了上面 2 種以外,還有 6 種
equ:equals,等於,等同此例的 ==
neq:not equals,不等於,等同此例的 not ==
lss:less,小於
leq:less equals,小於等於
gtr:greater,大於
geq:greater equals,大於等於


※exist

@echo off
setlocal
    
if exist "x.txt" (
    echo o
) else (
    echo x
)
    
endlocal
pause

※判斷本路徑下有沒有 x.txt 這支檔案或目錄


※迴圈裡判斷檔案

@echo off
setlocal
    
for %%i in (*) do (
    ::if exist "fileName" (
    if "%%i" == "fileName" (
        echo o
    ) else (
        echo x
    )
)
    
endlocal
pause

※如果在迴圈裡判斷,不要用 exist,它只要路徑下有,所有迴圈都會認為是有,除非剛好此路徑下只有一支檔案


※迴圈裡判斷目錄

@echo off
setlocal
    
for /d %%i in (*) do (
    ::if exist "folderName" (
    if "%%i" == "folderName" (
        echo o
    ) else (
        echo x
    )
)
    
endlocal
pause

※同樣的,也不要在裡面判斷目錄,它只要路徑下有這個目錄,所有迴圈都會認為是有,除非剛好此路徑下只有一個目錄


※判斷變數

@echo off
setlocal
    
set a=aaa
if defined a (
    echo have a
)
    
endlocal
pause

※使用 defined 判斷有沒有變數

2017年12月14日 星期四

Windows 批次檔-一般命令

可用 「help 命令名稱」看教學


※echo

echo:顯示目前是關閉或開啟
echo xxx:印出xxx
echo.:印出空行,文檔沒有
echo off:關閉左邊的 "C:\USERS\USER>"
echo on:與 echo off 相反
@echo off:和 echo off 一樣,但連本身這一行都不顯示,執行批次才看得出來





第一行可看出有沒有@的差別


※rem

註解,網路上有人也可用「::」,文檔並沒有說明,但我試的結果是可以的
注意,要寫在最前面才行


※pause

螢幕暫停,讓使用者按任意鍵繼續


※call

呼叫另外一支批次檔


※set

針對變數增刪改查,但批次檔一關閉就沒了
增:set a=apple,設定 a 是 apple
刪:set a=,設定為空就是刪除
改:set a=apples,key一樣,value不同就會修改了
查:set,1.什麼都不用加就能查看全部的變數  2.set a 為查看 a 開頭的變數

set a=apple
如果要用 echo 顯示,就要用%將變數包起來,注意「=」的前後不可空

使用 echo 時,內鍵命令也可用 %包起來,如 date、cd、windir、systemroot…等


※setlocal、endlocal


這兩個命令包起來的變數是區域變數

※setVar.cmd

@echo off
echo -----setVar.cmd start-----
set b=banana
    
setlocal
set c=cat
echo %b%,%c%
endlocal
    
echo %b%,%c%
echo -----setVar.cmd end-----
pause



※setVar2.cmd

@echo off
echo -----setVar2.cmd start-----
echo %b%,%c%
call setVar.cmd
echo %b%,%c%
echo -----setVar2.cmd end-----
pause


倒數第二行的 setVar2.cmd end 可看出 banana 是抓的到的,但區域變數 cat 抓不到


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 裡