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 裡

2017年11月28日 星期二

Shell script 快速入門

第一行是 #!/bin/bash/
bin/bash 是大部人用的,也是預設的 shell,可看 /etc/passwd 的最後一個欄位


※參數

假設檔名叫 xxx.sh,下 xxx.sh aa bb
$#:表示有幾個參數,此例為 2
$0:xxx.sh,也就是檔名
$1:aa
$2:bb
一直到$9,10以上要用 {} 包起來,如 ${12}
$*:表示所有參數,整個參數是一個整體,如"$1 $2 $3",參數和參數之間用空隔隔開
$@:也表示所有參數,但每個參數是分開的,如 "$1" "2" "$3",參數和參數之間用空隔隔開
以上兩個需視情況用「"」包起來使用
$$:processID,ps -aux 可查看
$?:讀取上一行指令有沒有錯誤,沒錯回傳0,否則回傳錯誤代碼,最多255,如果超過255,那就是除 256 的餘數
man bash ,搜尋  ^exit status

※test.sh
#!/bin/bash
echo $#
echo $@
echo $0
echo $1
echo $$
echo $?
#echo xxx "aaa" "bbb"

xxx(){
    echo $1 $2
}
xxx "aaa" "bbb"

※./test.sh a b cccc de 結果為
4
a b cccc de
test.sh
a
547
0
aaa bbb

※./和sh可執行shell,但./需要w權限


※變數

a=apple(=前後不可有空格,字串有空格時,用「'」或「"」包起來)
echo $a 印出a變數
export aaa 將aaa變數提升為系統變數
source xxx 因為提升為系統變數後,要重登入才有效,懶得重登入就可用這個指令
unset xxx 刪除xxx變數

「`」鍵盤左上角下來一格的按鍵,可用這個包指令,然後串接字符串
如:echo xxx `date` ooo


四則運算(+-*/%)和test

$a+$b 用 $(( 和 )) 包起來,然後裡面的$可有可無-> $((a+b))
無法用小數點

${#a}:長度
${a:2}:最前面扣2長
${a:2:4}:最前面扣2長後,取4長
${a#a*c}:從前面開始刪除abc,刪一次(勉強)
${a##a*c}:從前面開始刪除abc,(貪婪)
${a%a*c}:從後面開始刪除abc,刪一次(勉強)
${a%%a*c}:從後面開始刪除abc,(貪婪)


test

-f -d -r -w -x
test -f $f && echo "is a file" || echo "isn't a file"
&& || 為三元運算子


[]

1.前後必需空格
2.變數要用「"」包起來
[ "$a" == '5' ] && echo "yes" || "no"
== != -eq -ne -gt -ge -lt -le


※條件

if

read xxx

if [ "$xxx" == '1' ]; then
elif []; then
else
fi

/proc/cpuinfo
/proc/meminfo

-a 且
-o 或

if [ "$xxx" == 'a' -o "$xxx" == 'd' -o "$xxx" == 'xx' ]


case

read n
case "$n" in
'a') echo 'aaa';;
'b') echo 'bbb';;
  *) echo '***';;
esac


※陣列

xxx=("aaa" "bbb" "ccc")
echo length:${#xxx[@]}
for((i=0; i<${#xxx[@]}; i++)); do
      echo ${xxx[i]}
done

※迴圈

while

while 條件; do

done;

while [ "$i" -le 10 ]; do
  echo "$i"
  i=$((i+1))
done


until

#觀念和 while 相反,判斷式為 true 時停止
until 條件; do

done;

until [ "$i" -gt 10 ]; do
  echo "$i"
  i=$((i+1))
done

for

for (()); do
  內容
done;

for ((i=0;i<10;i++)) do
  echo "$i"
done

for in

for x in aaa bbb ccc; do
  echo "$x"
done

2017年11月27日 星期一

linux 用戶指令快速入門

※一般命令

echo xxx && ooo
ls  -l    --all = -a    --human-readable = -h  --inode = -i,看多檔用空隔隔開
pwd
whoami
cd
tree
date '+%Y/%m/%d %H:%M:%S',%a %b %Z
cal
mkdir
rmdir:刪除空目錄
cp --recursive = -r   --interactive = -i:windows為 xcopy /E /H 
mv --interactive = -i:windows為 move
rm --recursive = -r   --interactive = -i:
      windows 分兩步,為 [del|erase] /S /Q bbb && [rmdir|rd] /S /Q bbb 
touch xxx.txt,windows 為 type nul > xxx.txt
man: see also -> man [number] command
cat --number = -n
tac
tty
history    !!:上一個命令     !c:最接近c開頭命令    !號碼
file 查看是什麼檔案
more
less

grep,過濾結果,windows 有類似的,如 netstat -ano |find ":80"
-A -B -C 還蠻好用的
cat xxx.txt |grep abc -A 5:表示找xxx.txt裡有abc這行和之後的5行,找到多個會用「--」隔開
-B:之後
-C:前後,可以不打-C,直接打數字即可

|
head -行數,預設 10 行
tail -行數 --follow = -f:f 不能和 -行數一起使用,功能是畫面停住,等待新的訊息進來,然後顯示,Ctrl + c 可離開
ln


※vim

visual(視覺的) improved(改進過的),因為原本是 vi,改進過後是 vim
man vi 可查參數,再按 h 可查文檔

命令模式

剪下:[number]D
複製:[number]yy,yank 為使勁拉的意思
貼上:[number]p
刪除:[number]dd
上一次修改:u
下一次修改:Ctrl + r
上一頁:Ctrl + b (before)
下一頁:Ctrl + f (after)
到某一行:ngg 或打「:」加行數

切換成編輯模式:a、i、o、A、I、O
切換成搜尋取代模式::、/、?


編輯模式

回命令模式:Esc


搜尋取代模式

:

數字:跳到某一行
set nu:顯示行號,如果想一進去就有行號,可用臨時的 vi -c "set nu" 檔名,或用永遠的方法,在 ~裡,創建 .vimrc 或 .virc 檔案,將 set nu 打進去存檔,這樣以後開所有檔都有行號了,是針對當前使用者的設定,vi 開頭的用 vi; vim 開頭的 用 vim,當然也可以兩個檔案都打
set nonu
set ignorecase:預設搜尋、取代時,是有分大小寫的,可打上這個命令來忽略大小寫
set noignorecase
set nohl:no high light,搜尋完後,下次進去,預設會高亮,可用這個指令
w
q
q!

取代,如「1,$s/xxx/ooo/gci」,第一行到最後一行,將 xxx 取代成 ooo
    「.,$s/xxx/ooo/gci」:目前行到最後一行
    「%s/xxx/ooo/gci」:第一行到最後一行,和 1,$s 一樣
g 為全部取代,沒加只取代一個
c 為檢查,會有 y/n/a/q/l/^E/^Y 的提示,yn就不說了
    a 表示目前到之後都取代
    q 表示不取代,並且直接離開
    l 表示取代目前這一個,並且直接離開
    ^E 表示跳上一頁
    ^Y 表示跳下一頁
    但 ^E 和 ^Y 我不知道要怎麼按
i 為不區分大小寫,也可用 上面的 set ignorecase

:2,10y:複製 2~10 行
:2,10d:剪下 2~10 行
:2,10y co 15:複製 2~10 行並貼到 15 行
:2,10y m 15:剪下 2~10 行並貼到 15 行

/

尋找,如「/xxx」,搜尋 xxx,然後使用 n|N可往下往上搜尋
搜尋完整單字,如 do、doc、dock,使用 /do 時,三個都會找到
可用 /\<do\>,使用「<」「>」包起來,但要用跳脫字元「\」就可以找完整的單字
又如果下/\<[dD]o\>,那會找 do 和 Do

?

同 / 的尋找,但一開始是下往上尋找,一樣用n|N找


回命令模式:Esc

多檔編輯

vim -o,水平,使用 Ctrl + w 後,按左或右切換
vim -O,垂直,使用 Ctrl + w 後,按上或下切換
vim -p 檔一 檔二,命令模式使用 :tabn 或 :tabp 切換
vim 檔一 檔二,命令模式使用「:sp 檔名」,然後使用 Ctrl + w ,上或下切換


※alias

alias vim='vim --cmd "set nu"',但登出後就沒了
將上一行打在 ~/.bashrc 裡,然後使用 source ~/.bashrc 或者下次登入就會生效了
unalias 為相反的命令

/etc/profile 全域環境變數     ~/.bash_profile 使用者環境變數
/etc/bashrc 全域 bash shell    ~/.bashrc 使用者 bash shell


※標準輸出入、標準錯誤輸出

< 檔案
<< 文字結束
>、1>
>>、1>>
2>、>&
2>>、>>&
&>

wc < /etc/passwd 行、單字、容量
wc < test1 >> test2


※搜尋

which:查尋環境變數的可執行程序,結果只有路徑
whereis:自訂的資料庫,結果有可執行命令 設定檔 手冊
locate:自訂的資料庫(var/lib/mlocate),比 whereis 更詳細,模糊查尋
find:最後的絕招, find /home -name xxx
find 後面接要搜尋的路徑,不打就是當前的路徑
-atime
-mtime 3 前三天的那一天,檔案內容有被修改過的
  +3 3天之外
  -3 3天之內
-ctime

-user 帳號名
-group 用戶組
-nouser 不屬於任何用戶
-nogroup 不屬於任何用戶組

-name 檔名或目錄名,可用通配符,但要用「'」包起來,如 '*bru*'
-iname 檔名或目錄名(不分大小寫)

-type f,查找檔案,l、d為連結和目錄
-size +100k:大於等於100K的文件
-perm +7000:找特殊權限
-exec 找到後執行命令,一直找到「;」之前
{}:會被替換當前的檔名
\:每個系統的「;」有可能有不同的意義,所以加上這個跳脫字元
例:
find -name 'test[34]' -exec sed -i 's/aaa/zzz/g' {} \;
sed 指令說明
find 指令說明

搜檔案內容
find /etc -name "*.text" -exec grep -H "xxx" {} \; 在 /etc 下搜尋 .text 結尾的檔案,內容含有 xxx的,--with-filename = -H 為列出路徑


※ls -l  的訊息

drwx-w--wx 2 bruce root 4096

d、-、l 目錄 檔案 連結

rw-r-x-wx
三個一組,分別表示權限,擁有者 群組 其他

目錄
r:查看目錄內容
w:增刪改
x:進入目錄

檔案
r:查看檔案內容
w:增刪改
x:執行

2 為檔案數量,目錄至少有.和..,不包括子目錄;檔案一定是 1
擁有者是誰
群組名
檔案大小(Byte)
Mtime
檔名

除了 root 外,擁有者、群組、其他人只能有一個角色
例如有一個使用者,是 A 檔的擁有者也是群組,此時若擁有者的權限不足,並不會抓群組的權限

※mtime、ctime、atime 區別

mtime:檔案內容最後被修改的時間,ls -l
ctime:檔案的內容、屬性最後被修改的時間,如修改權限、修改擁有者、改檔名,ls -cl
atime:最後被讀取的時間,vi 開啟就會讀到了,但 echo 不會讀到,ls -ul,但在安裝作業系統時可以選擇不要更新 atime,可以增進效能


※su、su -、sudo

su:切換成 root,shell 不切換
su -:切換成 root,shell 也切換
以上兩個後面如果接使用者,就會切換到那個使用者,輸入的密碼為切換帳號的密碼

sudo:臨時切換成 root,必需先設定 /etc/sudoers,可用 vi /etc/sudoers 或者 visudo 編輯,
輸入的密碼為自己的密碼

帳號     來源主機名稱=(可切換的身份)     能下什麼指令(絕對路徑)
root ALL=(ALL) NOPASSWD: ALL
root 這個帳號從任何主機連進來都可用現在的設定,可切換任何身份執行任何命令
其中(ALL) 和 NOPASSWD: 可以不寫
(ALL) 表示切換哪個帳號的身份,不寫就是 root,ALL 表示任何帳號
NOPASSWD: 注意最後有個冒號,不寫預設是要打密碼的,打登入時的密碼,不是切換誰的密碼

xxx 192.168.0.1=(ooo) /aaa/bbb,/aaa/ccc
xxx 這個帳號從 192.168.0.1 這台主機連進來可用現在的設定,可切換成 ooo 
這個帳號,執行 /aaa/bbb 和 /aaa/ccc 這兩支程式

%zzz ALL=(ALL) ALL
加上「%」就變成群組了
zzz這個群組從任何主機連進來都可用現在的設定,可切換任何身份執行任何命令

sudo 指令說明



※背起來

rsync -avzhpr

a --archive
v --verbose
z --compress
h --human-readable
p --perms:preserve permissions
r --recursive


tar -zxvf -cvf

c --create
z --gunzip
x --extract
v --verbose
f --file

.tar.gz -> zxvf
.tar -> xvf
cvf -> 打包

rpm -ivh -Uvh -qa -e

i --info
h --hash
U --upgrade
q --query
a --all
e:erase,御載

rpm -Uvh
rpm -qa |grep httpd

rpm 必須自己解決依賴關係


yum [-y] [install | remove | update | search | list | list installed]

search:列出伺服器上可貨下載的
list:列出伺服器上可供下載和已安裝的
list installed:列出已安裝的
update:更新某個套件,如果後面不加套件,會整個系統更新

yum 底層還是 rpm,但會幫我們解決依賴關係



編譯

下載源碼後,如 nginx,然後 wget xxx 並解壓後進目錄
./configure --prefix=/xxx/ooo
make && make install
以上三步搞定,但有時候第一步有依賴問題會報錯,根據報錯的訊息下載,然後再重做一次,只要有錯就得重做
※「./」開頭是一定要加的,通常環境變數不會有設定你下載的路徑,在 linux 裡,只要是執行檔就是綠色的,執行時環境變數沒有時,就要加「./」,表示當前層
但有六個目錄例外:
bin
usr/bin
usr/local/bin

sbin
usr/sbin
usr/local/sbin

有錯時,如沒有 pcre,那就在下載時,後面加上 -devel,是 develop 的簡寫,通常開發會比較多東西
yum -y install pcre-devel

搞定後,去 --prefix 的目錄查看



2017年11月18日 星期六

設定 java 版本 (Maven 6)

官網文件

※方法一

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>

※為什麼有 compilerVersion?可以到這篇看看


※方法二

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

※我試了官方的不行,還得把 version 刪除才可以


※方法三

<profiles>
    <profile>
        <id>javaVersion</id>
        <properties>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
        </properties>
        <!-- <activation> -->
        <!--     <activeByDefault>true</activeByDefault> -->
        <!-- </activation> -->
    </profile>
</profiles>
<activeProfiles>
    <activeProfile>javaVersion</activeProfile>
</activeProfiles>

※前面兩個方法都是在 pom.xml 設定,最好是在父 pom 設定,其他子 pom 就不用設了

※這個方法是在 settings.xml 設定,是全域的

※profile 寫好後並沒有生效,用 activeProfiles 標籤和註解的 activation 標籤都可以

2017年11月13日 星期一

私服 (Maven 5)

私人伺服器,簡稱私服



左邊為電腦,右邊為 maven central,中間為私服,每次都要連到 maven central 太慢了,所以才會架設中間的私服,讓每一台電腦用,如果私服沒有,那私服會去下載

如果只有一台電腦,那就沒差了,但可以練習用

此篇用的私服是 Sonatype Nexus,下載 OSS 版的來練習,這裡用的是 nexus-3.2.1-01-win64
下載解壓後會有兩個資料夾,將 nexus-3.2.1-01 資料夾裡的 bin 加到環境變數,然後執行 nexus /run,啟動有點久,出現了「Started Sonatype Nexus OSS 3.2.1-01」不要關掉,然後到 localhost:8081,預設帳密是 admin/admin123,可看官網文件的 Part1
換 port 可以到 nexus-3.2.1-01\etc\nexus-default.properties 改

type 有三種
group:可以將以下兩個類型,一個類型包括很多網址,可以合併成一個
hosted:本地倉庫
proxy:代理倉庫(maven-central)


1的圖示要登入才有,左圖的 Security \ Users 預設只有 admin 和 anonymous,可以先將 anonymous 關閉,也可以在這新增使用者、修改密碼…等操作


第 2 版可以全拉,但第 3 版不知道為什麼不行,只好拉二個了

maven-central 裡有個 Rebuild index 按鈕,因為一開始 本地倉庫可能以經有 jar 了,但現在才架好 nexus,那 nexus 肯定沒有這些 jar,所以可以按這個鈕,同步一下

打開 pom 檔,增加如下的設定



<repositories>
    <repository>
        <id>nexus2</id>
        <url>http://localhost:8081/repository/maven-public/</url>
        <releases>
            <enabled>false</enabled>
        </releases>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    
    <repository>
        <id>central</id>
        <url>http://localhost:8081/repository/maven-central/</url>
    </repository>
</repositories>

※將網址貼到 url 裡。 尤於 url 只能寫一個,所以我寫了兩個 repository 了
如果是第 2 版,因為可以全拉到 group,所以可以只寫一個

※release、snapshots 標籤下的 enabled 標籤表示是否可以下載

※此時隨便找一個目前還沒有的jar,然後下 mvn install,在 console 就能看到從 nexus 下載了

※但如果沒有設定這個東西的人,就不會去 私服下載,所以最好把這個設定放在 settings.xml裡,所以 pom 檔可以刪了,改放在 settings.xml,然後將 settings.xml 複製給要用的人,但要注意設定完不會生效,必需要配置 activeProfile,如下:

<profiles>
    <profile>
        <id>nexus</id>
        <repositories>
            <repository>
                <id>nexus2</id>
                <url>http://localhost:8081/repository/maven-public/</url>
                <releases>
                    <enabled>false</enabled>
                </releases>
                <snapshots>
                    <enabled>false</enabled>
                </snapshots>
            </repository>
    
            <repository>
                <id>central</id>
                <url>http://localhost:8081/repository/maven-central/</url>
            </repository>
        </repositories>
    </profile>
</profiles>
    
<activeProfiles>
    <activeProfile>nexus</activeProfile>
</activeProfiles>


※上面提供的 nexus 連結的 Part2 就幫我們設定好了,只要我們把 copy 的網址貼進去就行
裡面分成三個大 element
profiles:將 pom 檔裡的設定改成放在這,但不會生效
activeProfiles:必需配置這裡的設定才會將 profiles 對應的 id 生效
mirrors:如果不配置,私服關閉了,還是可以去 maven central 下載,可在 settings.xml 加設定,如下:
<mirrors>
    <mirror>
        <id>nexus</id>
        <mirrorOf>*</mirrorOf>
        <url>http://localhost:8081/repository/maven-central/</url>
    </mirror>
</mirrors>

※預設其中有一個 id 為 central 的,在 下載好的maven\lib\maven-model-builder-3.5.2.jar 的 org\apache\maven\model\pom-4.0.0.xml ,他預設 snapshot 的 enable 是 false,可在這裡覆寫

※如果要將自己寫的檔案發佈到私服還得在父 pom 檔裡設定  distributionManagement,這樣子之後就可以用 mvn deploy 發佈到私服,但是會出現「Error code 401, Unauthorized」,如下:

<distributionManagement>
    <repository>
        <id>dmRelease</id>
        <url>http://localhost:8081/repository/maven-releases/</url>
    </repository>
    
    <snapshotRepository>
        <id>dmSnapshot</id>
        <url>http://localhost:8081/repository/maven-snapshots/</url>
    </snapshotRepository>
</distributionManagement>



因為沒有帳號密碼,所以還得在 settings.xml 設定 servers element,裡面的 id要對要到 distributionManagement 裡的 repository 的 id,這要才能發佈到私服
<servers>
    <server>
        <id>dmRelease</id>
        <username>admin</username>
        <password>admin123</password>
    </server>
    
    <server>
        <id>dmSnapshot</id>
        <username>admin</username>
        <password>admin123</password>
    </server>
</servers>

※可以來這個路徑確認一下有沒有 deploy 成功,看你是releases 還 snapshots

※也可以用 Search >> Maven 搜尋

2017年11月4日 星期六

排序:冒泡、選擇、插入


※冒泡排序


元素會像泡泡一樣,往左或往右移動,取決於使用升序或降序,做法是判斷相鄰的元素

綠排為 index,藍排為值
以升序為例,先判斷 index 0和1,然後 index 1和2,一直到最後
每判斷一次,如果左值 > 右值就交換
內迴圈跑完就會產生最大的數在後面,所以內迴圈跑完就會少一次外迴圈



int count = 0;
int[] intArray = new int[] { 5, 4, 3, 2, 1 };
int len = intArray.length - 1;
    
for (int i = 0; i < len; i++) {
    boolean finish = true;
    for (int j = 0; j < len - i; j++) {
        count++;
        if (intArray[j] > intArray[j + 1]) {
            int temp = intArray[j];
            intArray[j] = intArray[j + 1];
            intArray[j + 1] = temp;
            finish = false;
        }
    }
    if (finish) break;
} System.out.println("迴圈跑幾次=" + count); System.out.println(Arrays.toString(intArray));

※外迴圈只有控制內迴圈跑完一次減少一次

※如果要排序的值有一些已經排好順序了,那 finish 變數就會看出效果


※選擇排序

選擇、插入 都是將資料分成已排序和未排序兩個部分
從未排序的元素選擇一個最大或最小值放入排序的元素裡,和插入排序相反

以最上面的圖為例
以升序為例,以 index 0為基底,判斷 index 1、index2…到最後
每判斷一次,如果左值 > 右值就交換
內迴圈跑完就會產生最小的數在前面,所以內迴圈跑完就會少一次外迴圈


int count = 0;
int[] intArray = new int[] { 5, 4, 3, 2, 1 };
int len = intArray.length - 1;
    
for (int i = 0; i < len; i++) {
    boolean finish = true;
    for (int j = 0; j < len - i; j++) {
        count++;
        if (intArray[i] > intArray[j + 1 + i]) {
            int temp = intArray[i];
            intArray[i] = intArray[j + 1 + i];
            intArray[j + 1 + i] = temp;
            finish = false;
        }
    }
    if (finish) break;
} System.out.println("迴圈跑幾次=" + count); System.out.println(Arrays.toString(intArray));

※和冒泡排序只差在 if 而已

※外迴圈是抓基底的來判斷


※插入排序

從未排序的元素第一個插入到排序的元素裡,和選擇排序相反

以最上面的圖為例
以升序為例:
第一次判斷 index 0和1
第二次判斷 index 1和2,再判斷 index 0和1
第三次判斷 index 2和3,再判斷 index 1和2,再判斷 index 0和1
依此類推...
每判斷一次,如果左值 > 右值就交換



int count = 0;
int[] intArray = new int[] { 3, 9, 4, 6, 1, 7, 2, 5, 8 };
    
for (int i = 0; i < intArray.length - 1; i++) {
    for (int j = i; j >= 0; j--) {
        boolean finish = true;
        count++;
        if (intArray[j] > intArray[j + 1]) {             int temp = intArray[j];             intArray[j] = intArray[j + 1];             intArray[j + 1] = temp;             finish = false;         }
        if (finish) break;
    }
}
System.out.println("迴圈跑幾次=" + count);
System.out.println(Arrays.toString(intArray));

※外迴圈 i 值給內迴圈 j

2017年10月31日 星期二

volatile 和 原子性

※volatile


尤於 CPU 的速度比主存還快,所以每個 Thread 都會有一個專屬的 cache,將主存的資料從主存拷過去

volatile 能保證 1.內存可見性和 2.排序性
.volatile 只能宣告在全域變數
.內存可見性:volatile 有揮發性的意思,意思就是用完就丟,在 java 裡的意思是值有改變就去主存抓
以上面的圖來說,要是值改變了,就相當於沒有 CPU cache 了,所以會到主存去抓 (保下面的賣票例子不可用 volatile,因為沒有原子性)

.排序性:CPU 會將我們寫的程式碼重排,如寫在第一行,但不一定就是先執行,不過他保證結果是一樣的,但可惜只是單線程一樣,多線程有可能會不一樣,而加上這個關鍵字可以禁止 CPU 重排

.只能解決一寫多讀的情形

.++i 是原子性;i++ 不是原子性,可用 AtomicXXX 或 LongAdder 產生原子性,高併發時用 LongAdder 較快,否則 AtomicXXX 較快



public class App {
    private volatile boolean flag = false;
    
    public void xxx() {
        new Thread(() -> {
            try {
                Thread.sleep(200);
                flag = true;
                System.out.println("oooooooooooooo");
            } catch (InterruptedException e) {
            }
        }).start();
    
        new Thread(() -> {
            while (true) {
                if (flag) {
                    System.out.println("xxxxxxxxxxxxxx");
                    break;
                }
            }
        }).start();
    }
    
    public static void main(String... ss) {
        new App.xxx();
    }
}


※此例如果不加 volatile 有可能兩個 CPU 同時抓到,所以迴圈裡的 flag 永遠都是 false了

※此例也可用 synchronized 或 Lock,但效能比較差

※不代表 volatile 可取代 synchronized
volatile 並沒有互斥性,也不能保證原子性
互斥性:像 synchronized 和 Lock 就有,一次只能一個進入,但指的是相同的物件
例:HttpSession 可以鎖定成功;但 HttpServletRequest 就不可能鎖定成功


※賣票(必需要有互斥性)

public class AppTest {
    volatile AtomicInteger ticket = new AtomicInteger(10);
    // Integer ticket = 10;
    
    public static void main(String... s) {
        new AppTest().xxx();
    }
    
    public void xxx() {
        Runnable run = () -> {
            for (;;) {
                // if (ticket > 0) {
                System.out.println("票" + ticket.get());
                if (ticket.get() > 0) {
                    try {
                        TimeUnit.SECONDS.sleep(1);
                        // System.out.println(--ticket);
                        System.out.println(ticket.decrementAndGet());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        };
    
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }
}


※還沒加原子性:雖然在減 1 時,其他線程有看見,但有可能還沒寫,其他線程就讀到舊的,因為 volatile 沒有原子性

※加原子性的 AtomicInteger 後,使用兩個線程看起來好像可以,實際上還是有問題,因為此例是偶數票,每次少兩張,最後是 0,但用更多的線程去跑或改成單數票又不行了

.因為雖然加了原子性,但在改之前,其他線程還是讀的到 (資料庫的不可重覆讀)
如第 1 次,四個線程都讀到 10,然後都減 1,但有原子性,所以最後是 6
而第 2 次,四個線程都讀到  6,然後都減 1,還是有原子性,所以最後是 2
而第 3 次,四個線程都讀到  2,然後都減 1,還是有原子性,所以最後是 -2
也就是說一次只能有一個線程讀取,所以必需要有互斥性

如果沒有 if,只讓它一直加或一直減,最終會是正確的



※原子性

表示不能切割,如後置遞增/減
例:i++ 在底層運作是
int temp = i;
i = i+1;
return temp;
這三步是不能切割的,但 volatile 無法保證這一點



public class MyThread implements Runnable {
    private int increment;
    // private AtomicInteger increment = new AtomicInteger(0);
    
    @Override
    public void run() {
        try {
            Thread.sleep(200);
            System.out.println(increment++);
            // System.out.println(increment.getAndIncrement());
        } catch (InterruptedException e) {}
    }
    
    public static void main(String... ss) {
        MyThread my = new MyThread();
        for (int i = 0; i < 10; i++) {
            new Thread(my).start();
        }
    }
}

※increment 有可能會有同時進入 CPU 的情形,使用 volatile 也沒有用

※java 1.5 新增了 java.util.concurrent.atomic 套件,專門做原子的操作

※incrementAndGet方法就是前置遞增了,而遞減單字是 decrement

※原子性採用 CAS (compare and swap) 算法,屬於樂觀鎖

※CAS 有三個值v和a都是主存抓來的,b是替換值,只有 v 和 a 的值相等才會將 b 的值取代 v 的值(CPU2 的圖錯了,v=1, a=0 才對)

※以上圖為例,CPU1 從主存抓到 i 的值是 0,此時 CPU2也進來抓到0
這時又換 CPU1,比較過後是一樣的,所以更新為 1,i 更新成功為1
這時又換 CPU2,因為 i 是 volatile,所以 v=1, a=0, 什麼也不做
※如果是 CAS 自旋,那 CPU2 還會繼續,這是 v 和 a 都是 1,就看 b 是什麼就可以取代

※判斷和塞新值要看成一個做法,也就是說不會在判斷時,另一個 Thread 進來的情況

※此例當然也是可以用 synchronized,但 synchronized 效能比較差

※還可看高手寫的文章

2017年10月30日 星期一

git 常用命令和圖

本地 git 圖



※此篇都是以 windows 版本 1.9.5 操作的

官網連結
working directory = working tree(工作區):上圖的中間那兩個,切換分支會被 git 刪除或增加
staging area:staged = cached = index(暫存區),上圖的最右邊
.git 目錄(本地庫):commit 之後,就會到這個階段,裡面有所有歷史的 commit

※git config

git config 設定的鍵值對,有以下三種範圍
--global :當前使用者:%userprofile%\.gitconfig
--local :.git 下(預設)\config
--system :git 安裝目錄下,預設在 %userprofile%\AppData\Local\Programs\Git\mingw64\etc\gitconfig (所有登入的使用者)
.優先級:local > global > system
有 local 就抓 local,沒有就去 global 找,再沒有就去 system,都沒有就出現警告或錯誤


官方說需設定 user.name 和 user.email,commit 時會用到,如果不加不能 commit (會提示)
如 git config user.name xxx
--list = -l 查看所有變數,可再下上面三條其中之一的範圍命令
--unset 和 --unset-all 刪除一個鍵和多個鍵,可再下上面三條其中之一的範圍命令
別名
git config alias.st status 只要打上 alias.xx 內鍵命令,就能使用別名代替,以後只要用 st 就能代替 status 了


※基本命令

git status --short = -s:查看狀態
git commit --message = -m;--all = -a :提交,一起使用時,a 一定要在 m 前面,但如果此檔沒有 add 過是不行的
git rm --cached :從 unmodified 移到 untracked
git log --oneline --graph:查看提交記錄並顯示圖
git reflog:查看所有歷史記錄
git show:後面接 id 號,可看 commit 了什麼,id 號要用 log 或 reflog 查


※.gitignore

只會對 untracked 有效
在 Windows 新增檔案 .gitignore 會失敗,記得在最後加個「.」即可,副檔名記得要打開
內容如下範例:
.gitignore
*.log
!zz.log

這表示忽略本檔案和 log 結尾的檔案,但 zz.log 還是不忽略,注意第二行和第三行不能調換順序

預設是所有目錄、子目錄都會進行這樣的事,但如果只要根目錄下就好了,就要下絕對路徑
/.gitignore
/*.log
/!zz.log

最前面加個「/」即可

※如果已經 commit

git update-index --assume-unchanged fileName

如果又想反悔
git update-index --no-assume-unchanged fileName

查看所有已經 update-index 的檔案:git ls-files -v |grep '^h',windows 用 git ls-files -v |findstr /b h
或 git ls-files -v |findstr "^h"

注意:
1.雖然加了update-index --assume-unchanged,但有其他人更新了這個檔案,git pull 仍然會更新,只是方便 git add . 而已

2.ignore 必須在 untracked 使用才有效果

※將檔案加入上一個 commit,不會多一個 id,但會換掉id

git commit --amend --no-edit fileName


※暫存

git stash list:列出所有的暫存
git stash:暫存,但必需未 commit 的才可暫存,可多筆暫存,最新的是stash@{0},如果又stash,那最新的還是stash@{0},之前的會自動往下stash@{1},依此類推
git stash show:如果不加index,是 show 出最新的一筆
git stash apply:如果不加index,回復最新的一筆
git stash drop:如果不加index,刪除最新的一筆
git stash pop:apply + drop


※分支相關

git branch branchName (不會切換到新分支,切換用 git checkout)
git checkout -b branchName (會切換到新分支)
git branch --delete = -d 刪除本地分支
git branch --all = -a 看本地及遠端分支,不加只能看本地

※注意!如果已有 modified 、staged 的檔案
1.在已有分支的情形,無法切換
2.在沒有分支的情形,可以切過去,也能切回 master,
但其中一個分支對 modified 、staged 的檔案 commit 後,
另一個分支的 modified 、staged 的檔案內容,會自動退回上一次 commit 的內容,
從此以後,分支才沒有相關

※分支名預設是 master,是 git init 時預設的,但後期的 git 版本預設改成 main 了,
如果想改可用 git branch -m 或 -M,大 M 表示目標存存也覆蓋

.刪除遠端分支

git push --delete = -d origin 分支名稱
※git ls-remote 可以看到最新的所有遠端分支
git branch -r 可以看到從遠端複製下來的副本
但有一件很奇怪的事會使其他分支 pull 有問題
如果在本地創分支並提交到遠端,最後刪除了這個分支,然後要創造同名的分支,那其他分支 pull 時會強制更新,不會有問題
但有一種是斜線區隔的,如 bruce/aaa,bruce/bbb,這樣會產生資料夾 bruce,然後底下有兩個分支 aaa 和 bbb
假設我創分支 bruce 到遠端,最後刪除了,然後新增分支 bruce/aaa,自己的分支不會有問題,其他分支會無法 pull,錯誤圖如下:

這是 git branch -r 的問題
創建 bruce 分支的那個人,刪除了這個分支,會更新 git branch -r 的內容,但其他分支不知道,
然後創建 bruce/aaa 到遠端,git ls-remote 可以看到
其他分支 pull 時,認為 bruce 還在,但又有 bruce/aaa,bruce 變成資料夾了,就會報這個錯,
解決方法如上圖的紅框,下 git remote prune origin 即可,git branch -r 就會和遠端同步了


※合併

merge:

要合併到哪個分支,就要先切換 (checkout) 到那個分支
假設 master 分支有 a b 兩個檔案;而 develop 有 a c 兩個檔案
.如果切換到 develop 分支進行 merge,那 develop  分支會有 a b c 三個檔案;而 master 還是只有 a b 兩個檔案
.又如果是切換到 master 分支進行 merge,那 master 分支會有 a b c 三個檔案;而 develop 還是只有 a c 兩個檔案

例:git fetch 後 git merge origin master
.ff 和 --no-ff 的差別
git merge --ff 表示 fast forward,這是預設的,只會有分支開始到合併的所有提交記錄
git merge --no-ff --message = -m
--no-ff 除了有分支開始到合併的所有提交記錄,還會多一條 merge 的記錄(有衝突還會更多)
不加 --message,會跳出 vi 的畫面,這時打訊息內容也是一樣,同 commit 沒 -m 的道理
此時分支的畫面會有合併的圖,可用 git log --graph 查看


rebase:

rebase 後,log --graph 可以看到所有的分支都只剩一條了(但分支還在),而且有很多的 commit 都沒有了,速度也慢,唯一的優點就是只剩一條,看起來比較乾淨
如果想回復,還是可以用 reflogId 來回復

假設原本的圖是綠框(從左到右)

可以看出 rebase 變成一條了


※多功能的 checkout

git checkout 是將 HEAD 移動到指定的地方,所以可以(HEAD把他想成目前分支,.git資料夾裡有HEAD檔案,是純文字,可切換分支看裡面的變化)
1.切分支、創建分支
2.回退、退到之前的 tag
※分支名稱如果和檔案名稱一樣,就不能回退了,會以切分支為主

由於 checkout 有兩個功能,在後面的版本又多了兩個指令將這兩個功能分開,但 checkout 仍然可以用,多了 switch (切分支)和 restore(回退,由修改改成未修改)



※diff、difftool

比對兩個檔案,diff 是上下比對; difftool 會分成兩個視窗,左右比對

※未 commit 回退

checkout 從 modified -> unmodified
reset --mixed 從 staged -> modified,還會提示說 --mixed 已經 deprecated 了,可以不加
reset 的 soft 和 hard 會報錯


※已 commit 後回退

checkout:

git checkout 回退成功後,git log 、reflog 不會有變化

reset:

小心使用
git reset --hard 後面有5種
1. HEAD (回退最新的)
2. HEAD^^ (回退上二層,一個^表示一層)
3. HEAD~50 (回退50層,因為不想打很多的^)
4. gitlogId
5. HEAD@{number} (git reflog 看到的)
P.S. ^和~只能後退,不能前進

※reset 有三個容易搞混的參數 --hard --soft --mixed,預設是 --mixed
假設做了以下步驟
新增空檔案提交,提交訊息是 first
增加此檔內容 aaa,然後提交,訊息是 second
增加此檔內容 bbb,然後提交,訊息是 third
增加此檔內容 ccc,然後提交,訊息是 fourth
最後此檔的內容是 aaabbbccc

使用 git reset --soft HEAD^^
回退的指令是 commit 
檔案內容一樣,狀態是 stage(綠色),所以 diff --cached 有東西
log 沒有fourth
※不管回退多少內容都不會變,但已經 add,還沒 commit,所以是綠色的 

使用 git reset --mixed HEAD^^
回退的指令是 commit 和 add
檔案內容一樣,狀態是 modified(紅色),所以 diff 有東西
log 沒有fourth
※不管回退多少內容都不會變,但都還沒 add,所以是紅色的

使用 git reset --hard HEAD^^
回退的指令是 commit 和 add,然後再將檔案內容回復
檔案內容只有aaabbb,狀態是 unmodified,diff 和 diff --cached 都沒有 log 沒有fourth
※最好理解就這一個,回到哪時就是哪時

※回到伺服器上最新的版本:
git fetch origin master && git reset --hard origin master,不管有沒有 commit 都可以,如果確定是最新版的可以不下 git fetch


--merge
在 commit 之後編輯但還沒 add,此時 pull 下來,這時你覺得不滿意,想回到上一層,但用 --hard 會清空工作區(圖中間的二個),此時可以用 git reset --merge,會回退 pull 之前,且不會管尚未 add 的東西,但 add 會回退


revert:

從遠端 pull 完後,做了 reset 操作,但遠端不知道,此時 push 會有問題,所以 reset 要小心操作,但以這個需求,應該用 revert,reset 是回退到某一個 commit;而 revert 是增加一個 commit,所以 push 不會有問題,但不加參數會跳出討厭的 commit message edit
可下 git revert --no-edit xxx


checkout、restore、reset、revert區別

checkout:git log不變(modified->unmodified 紅色變無色)
restore --staged:git log不變(add->modified 綠色變紅色)
reset:git log 往後
revert:git log 往前



cherry-pick

檢櫻桃,意思就是將很多分支的其中某幾個 commit 合併到目前分支
假設有兩個分支 master 和 dev
dev 新增 ooo.txt,最後三個 commit 內容如下:
左內容右 hash
ccc                 e258
bbb                ffa7
aaa                 8frt

假設 master 只要 ooo.txt 內容為 bbb 和之前的內容,就可以切換到 master 後,下如下的指令:
git cherry-pick ffa7
如果要合併多個 commit,用空格隔開
這樣,master 分支裡就有 ooo.txt,內容是 bbb 和 aaa
有可能有衝突,衝突解決方式和之前一樣


※log

git log
--oneline 只顯示一行,只有 id 和提交訊息
--graph 最左邊有 git 的圖形
--author= 針對作者
fileName 針對某個檔案


※遠端

 .git 資料夾裡有個 config 檔,裡面是純文字檔
git remote add remoteName http~~
一做完這件事,config檔、git config、 git rermote --verbose = -v 都看得到
主要是因為網址太長了不容易記憶,所以取個別名,這樣以後就可以用這個別名上傳了,當然不設定也是可以的,只是每次都要複製網址 (除非你記的起來),預設叫 origin,是在 clone 時加的,不管遠端叫什麼名字,本機都是 origin,可以用 clone 的 --origin 來取遠端名稱
如果第一次 push,可以不叫 origin,但別人 clone 時,如沒有特別給 --origin 參數,會變成 origin

git remote set-url remoteName http~~ 可以修改別名
git remote --verbose 會看到 push 和 fetch,預設是一樣的,可以修改 push,用 --push 即可

git push 會提示要設定 push.default 的 key,value可以是 matching 或 simple
他的解釋是設定 matching 後,push 會上傳所有分支;simple 只會上傳目前的分支,但我試的時候,都只有上傳目前分支
git push --set-upstream = -u
git push --u remoteName branchName (每新開分支的第一次都要用 -u,之後只要 git push,branchName 可以多個,用空格隔開)
-u 後,config 檔會出現[branch "branchName"],表示已經有用過了,已後只要 git push 即可
但要注意 config 檔是「branchName」,所以每個分支第一次都要 -u
這個 -u 也會影響到 fetch
git pull = git fetch + git merge origin/branchName(用 git branch -a 查看)
git pull --rebase = git fetch + git rebase origin/branchName
git pull 只會更新自己分支的部分,其他分支只會知道有更新,但不會更新檔案內容
可以切換到其他分支下 git pull,檔案內容才會更新,但也可以在不切分支的情形使用
git pull origin 分支名 也可以

git push origin master:xxx 將本地的 master 分支推送到遠端的 xxx 分支,且遠端名叫 origin,但要注意遠端分支
1.不能有 xxx 的分支名
2.和本地名稱一樣可以

如果本地分支和遠端分支名稱一樣,可省略成 git push origin master

※clone

git clone下來後,只會有 master 分支,可以下 git ls-remote 分支名
產生遠端分支到本地分支:直接 checkout
假設遠端是 remotes/origin/dev,那只要 git checkout dev,這樣就可以了

※如果 clone 時,有加 --depth 1,這時會出 「error: pathspec 'xxx' did not match any file(s) known to git」的錯
此時只要先創建分支並切過去,然後 git pull 遠端名 分支名即可


※工具

可以使用 gitk 看,有畫面


※使用ssh

clone 時有 https 和 ssh 可選,但 ssh 需要設定才可以 clone,否則會出現 Please make sure you have the correct access rights and the repository exists.

首先要知道 Windows 放 ssh key 的目錄在 %userprofile%\.ssh; linux 在~/.ssh
使用 bash 有 ssh-keygen 這個命令可用
1.ssh-keygen -t rsa 會在目錄出現 id_rsa、id_rsa.pub 這兩個檔案
2.將 副檔名是 pub 的內容全部複製到 github 放 Key 的地方,如下圖:


Title 可不打,儲存後打 github 的密碼即可,此時已可使用 git clone ssh 地址,有提示要打yes,不能直接案 enter,用完會在邊錄多一個 known_hosts 檔案


※還可使用 ssh 測試,ssh -T git@github.com
這時會出現
Warning: Permanently added the RSA host key for IP address 'IP 地址' to the list of known hosts.
Hi bruce12452002! You've successfully authenticated, but GitHub does not provide shell access.
-T 表示 Disable pseudo-terminal allocation,禁止假的終端機分配
當使用 ssh 或 telnet 登錄時,系統給我們的終端就是假的終端機,禁止的意思是只能取得 shell 而已,很多環境變數都沒有,但測試已足夠


※如果重新使用 ssh-keygen 會針測到已有 key,會問你要不要覆蓋,如果打 y,會覆蓋
所以覆蓋或刪除檔案都必需重新做一次,否則一樣會出現權限不足的錯誤,如果別台電腦也想使用 ssh,將這兩個檔案複製到目錄即可

※如果密碼使用 SSH

ssh-keygen 有設定密碼,那每次 pull 或 push 都要打密碼
可以使用 ssh-agent 和 ssh-add 將密碼儲起來,這樣就不用打密碼了
1.eval `ssh-agent` 會顯示 Agent pid xxx
2.ssh-add 會提示要打密碼

※如果密碼使用 https

Windows 安裝完 git 是 credential.helper=manager 有 manager、cache、store 可選
可用 git config --list 查看
manager 會將密碼存在 windows 控制台的 credential manager,裡面有 Windows Credentials
   有安裝 git GUI 就會有 manager,意思是給 GUI 管理
cache 會將密碼儲 15 分鐘,可用 --timeout=300 改時間,單位為秒
store 會用明文的方式儲存,預設存在 ~/.git-credentials,可用 --file <path> 修改


※hook

.git 目錄裡有個 hooks 子目錄,裡面已經寫好了一些腳本,只要把「.sample」拿掉即可使用
pre 開頭表示什麼之前會執行;post 開頭表示什麼之後會執行

假設有一個特定的檔案,commit 前要顯示提示

※pre-commit 例一
#!/bin/bash
    
files=$(git diff --name-only --cached HEAD)
    
#使 read 可以使用
exec < /dev/tty
    
for fileName in ${files}; do
    if [ "$fileName" == 'test.txt' ]; then
        read -p "包含 test.txt, 真的要上傳嗎?()y|n)" u
        echo $u
        if [ "$u" != "y" ]; then
            echo "push fail"
            exit 1;
        fi
    fi
done
    
#回傳0表示繼續執行,非0結束
exit 0;

※但寫完這支,git 無法 push,所以先將 pre-commit 複製到 .git 的同一層,然後寫一支批次檔,此範例是微軟的
copy pre-commit .git\hooks\
del pre-commit
mklink /H pre-commit .git\hooks\pre-commit

複製完再刪除,才可以硬連結


※pre-commit 例二
#!/bin/bash
    
files=$(git diff --name-only --cached HEAD)
username=$(git config user.name)
path=src/main/resources/
account=(user1 user2 user3)
    
for fileName in ${files}; do
    if [ "$fileName" == "$path \\ba.java" -o "$fileName" == "$path \\bb.java" -o "$fileName" == 'c.java' ]; then
        for un in $account; do
            if [ "$un" == "$username" ]; then
                echo "你沒有權限修改" $fileName
                exit 1;
            fi
        done
    fi
done
    
#回傳0表示繼續執行,非0結束
exit 0;

※例一的例子,如果使用小烏龜,不用命令的方式,不支援鍵盤打字

※此範例是針對帳號,不能修改特定的檔案

※copy.cmd

move pre-commit .git\hooks\
del copy.cmd

※將 pre-commit 複製到 hooks 後,刪除自己

※還可以看我覺得寫的很好的 教學


※小技巧

※merge 部分檔案

merge 時,會將所有改變合併,如果只想 merge 某些檔案,可以如下使用

git checkout 分支名 檔名 檔名 檔名…
假設有個分支叫 dev,只有 a.txt 和 b.txt 想 merge 到 master 分支,操作如下:
git checkout master
git checkout dev a.txt b.txt
注意:沒有什麼衝不衝突,會直接將 master 的檔案覆蓋


※部分提交

如果已經都 add 了,那下 commit 會將已經 add 都提交
下 rm -r --cached xxx 可以將不想要 add 的,變成非 add 狀態,如果是檔案,可以不加 -r


※windows 針對資料夾底下的git專案pull

@echo off
setlocal
    echo 今仔日是 %date%
        
    FOR /D %%d IN (*) DO (
        cd %%d &&  echo ===============目前目錄是 %%d===============
    
        if exist == ".git" (
            git pull
        ) else (
            echo 根本沒git嘛!想唬哢我!
        )
        echo. && echo. && cd ..
    )
endlocal
pause

※副檔名為 cmd 或 bat 即可


※git add commit 原理


windows 每幾秒執行檔案查詢的批次程式碼:
@echo off
:begin
TREE /F
TIMEOUT /T 5
cls
goto begin
pause
存成 bat 檔並放在 .git 裡即可
 
linux 本來就有指令,所以不需寫程式
watch -n 1 -d find .
每一秒將當前目錄顯示出來



git add 後
會增加 index 檔案
還有在 objects 增加 hash 前 2 個數字的資料夾,裡頭有前 2 個 hash 之後檔案名稱

git cat-file -t 檔案的 hash,可看類型,blob
git cat-file -p 檔案的 hash,可看內容
git cat-file -s 檔案的 hash,可看檔案大小

git 只存檔案內容,檔案名稱不存的,如果檔案內容一樣,git 不會有變化

hash 使用的是 sha1,它是 160 長的
使用「類型 內容長度\0內容」來算出 hash 值的,內容長度(Byte)包括換行,可用 ls -lh 查看
如 「blob 15\0hello world」

git ls-file 可查看暫存區有什麼檔案
git ls-file -s 可將每個檔案的權限、hash、檔名列出來
原本的檔案修改完再 add,會產生新的 hash,舊的也還在,但新的 hash 會用在新的檔案
---

git commit 之後
會有兩個檔案,注意 commit 的訊息有 hash
git cat-file -t hash,可發現類型為 commit
再用 -p 看到檔案內容又有個 hash
然後再用 -t 看新的 hash 為 tree 型態
-p 內容為 commit 的檔案名稱和 hash

同時,refs\heads 也有一個叫此次提交的分支檔案,如 master,內容是 commit 的 hash
HEAD 這個檔案都是指向目前的分支 refs\heads\master,一切分支就會改變,
但也有不正常情況不會指向目前分支,如 checkout 到之前的 hash,此時狀態是 detached