由於web page是stateless,所以,很多的資料(如:登入、購物車)就必須靠session來儲存,早期的session是依賴在browser端的cookie,不過,cookie是不安全的做法,所以,現在的session都是依賴伺服器端來儲存。Spring Session提供更彈性化的session管理。Spring Session提供不同方式進行session管理,如:Redis, Pivotal GemFire, JDBC, Mongo, Hazelcast。以下的範例都以JDBC+MySQL為例,將session資料儲存在資料庫的好處是不會因為重啟web server就遺失所有的session資料。
pom.xml (因為使用jdbc,所以,也需要jdbc的相關packages)
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>application.properties (因為使用jdbc,所以,也需要jdbc的設定),其中spring.session.jdbc.initializer.enabled=true會幫我們在資料庫建立所需要的資料表及欄位,所以,在資料表及欄位建立後就不需要了。
spring.datasource.url=jdbc:mysql://localhost/practice?verifyServerCertificate=false&useSSL=false&requireSSL=falsespring.datasource.username=rootspring.datasource.password=1234spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.session.store-type=jdbcspring.session.jdbc.table-name=SPRING_SESSIONspring.session.jdbc.initializer.enabled=true如果不使用initializer,可以自行產生相關的資料表
CREATE TABLE `SPRING_SESSION` ( `SESSION_ID` char(36) NOT NULL DEFAULT '', `CREATION_TIME` bigint(20) NOT NULL, `LAST_ACCESS_TIME` bigint(20) NOT NULL, `MAX_INACTIVE_INTERVAL` int(11) NOT NULL, `PRINCIPAL_NAME` varchar(100) DEFAULT NULL, PRIMARY KEY (`SESSION_ID`), KEY `SPRING_SESSION_IX1` (`LAST_ACCESS_TIME`)) ENGINE=InnoDB還有
CREATE TABLE `SPRING_SESSION_ATTRIBUTES` ( `SESSION_ID` char(36) NOT NULL DEFAULT '', `ATTRIBUTE_NAME` varchar(200) NOT NULL DEFAULT '', `ATTRIBUTE_BYTES` blob, PRIMARY KEY (`SESSION_ID`,`ATTRIBUTE_NAME`), KEY `SPRING_SESSION_ATTRIBUTES_IX1` (`SESSION_ID`), CONSTRAINT `SPRING_SESSION_ATTRIBUTES_FK` FOREIGN KEY (`SESSION_ID`) REFERENCES `SPRING_SESSION` (`SESSION_ID`) ON DELETE CASCADE) ENGINE=InnoDB可以在Application裡加@EnableJdbcHttpSession,攔截原本的HttpSession,以Spring Session取代。
package com.example.demo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;@SpringBootApplication@EnableJdbcHttpSessionpublic class DemoSessionApplication { public static void main(String[] args) { SpringApplication.run(DemoSessionApplication.class, args); }}或者,產生一個HttpSessionConfig,攔截原本的HttpSession,以Spring Session取代。
package com.example.demo;import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;@EnableJdbcHttpSession public class HttpSessionConfig {}在controller (如:BookController)裡,可以直接利用HttpSession來存取Session (要import javax.servlet.http.HttpSession;)。
@RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView openFormLogin() { ModelAndView model = new ModelAndView("hi"); return model; } @RequestMapping(value = "/HelloWithId", method = RequestMethod.POST) public ModelAndView processForm(@ModelAttribute("id") String id, HttpSession session) { ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); session.setAttribute("loginId", id); return model; }在view (如:bookList.html)就可以利用${session.loginId}來取得資料。
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"/> <title>List ALl Books</title></head><body>Hello <span th:text="${session.loginId}">login id</span>!<form action="bookRetrieveByCategory" method ="post"> 類別:<select name = "bookCategory" th:field="${bookCategory.id}"> <option th:each="bookCategory : ${allBookCategories}" th:value="${bookCategory.id}" th:text="${bookCategory.name}">商業</option></select> <input type="submit" value="Submit"/> <a href="bookRetrieveAll"><input type="button" value="Show All Books"/></a></form> <a href="shoppingCartList"><input type="button" value="Show Shopping Cart"/></a><table> <tr> <th>編號</th> <th>類別</th> <th>名稱</th> <th>價格</th> <th></th> </tr> <tr th:each="book : ${allBooks} " th:object="${book}"> <td th:text="*{id}">1</td> <td th:text="*{bookCategory.name}">business</td> <td th:text="*{name}">Ben</td> <td th:text="*{price}">300</td> <td><a th:href="@{shoppingCartAdd(id=*{id})}">加入購物車</a></td> </tr></table></body></html>如果儲存在session裡的是個類別,我們就稱這個類別為session bean。在Spring 4.3之後,可以使用@SessionScope來設定這個類別是個session bean。如果我們採用JdbcHttpSession,那就要將這個類別設為Serializable,並且定義serialVersionUID。(詳參:https://www.mkyong.com/java-best-practices/understand-the-serialversionuid/)
package com.example.demo.entity;import java.io.Serializable;import java.util.ArrayList;import java.util.List;import org.springframework.stereotype.Component;import org.springframework.web.context.annotation.SessionScope;@SessionScope@Componentpublic class ShoppingCart implements Serializable{ /** * */ private static final long serialVersionUID = -5494311567944263493L; private List<Book> cart = new ArrayList<Book>(); public Iterable<Book> getCart(){ return cart; } public void add(Book book){ cart.add(book); } public void cleanup(){ cart = new ArrayList<Book>(); } }如果我們採用JdbcHttpSession,那就要將Book.java設為Serializable,並且定義serialVersionUID。
package com.example.demo.entity;import java.io.Serializable;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.JoinColumn;import javax.persistence.ManyToOne;@Entitypublic class Book implements Serializable{ /** * */ private static final long serialVersionUID = 3316076651716569539L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private int price; // JoinColumn refers to the column name in the table @ManyToOne @JoinColumn(name = "category") private BookCategory bookCategory; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public BookCategory getBookCategory() { return bookCategory; } public void setBookCategory(BookCategory bookCategory) { this.bookCategory = bookCategory; }}如果我們採用JdbcHttpSession,那就要將BookCategory.java設為Serializable,並且定義serialVersionUID。
package com.example.demo.entity;import java.io.Serializable;import java.util.List;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;import javax.persistence.OneToMany;import javax.persistence.Table;@Entity@Table(name = "book_category")public class BookCategory implements Serializable{ /** * */ private static final long serialVersionUID = -2957645392914180170L; @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; // mappedBy refers to the variable in Book @OneToMany(mappedBy = "bookCategory") private List<Book> books; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Iterable<Book> getBooks() { return books; }}在Controller裡(如:BookController),利用@Autowired就可以使用這個session bean了!
package com.example.demo.controller;import javax.servlet.http.HttpSession;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.ModelAttribute;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.servlet.ModelAndView;import com.example.demo.dao.BookCategoryDAO;import com.example.demo.dao.BookDAO;import com.example.demo.entity.Book;import com.example.demo.entity.BookCategory;import com.example.demo.entity.ShoppingCart;@Controllerpublic class BookController { @Autowired BookDAO dao; @Autowired BookCategoryDAO categoryDao; @Autowired ShoppingCart cart; @RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView openFormLogin() { ModelAndView model = new ModelAndView("hi"); return model; } @RequestMapping(value = "/HelloWithId", method = RequestMethod.POST) public ModelAndView processForm(@ModelAttribute("id") String id, HttpSession session) { ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); session.setAttribute("loginId", id); System.out.println("loginId:"+id); return model; } @RequestMapping(value = "/bookCreate", method = RequestMethod.GET) public ModelAndView openFormCreate() { ModelAndView model = new ModelAndView("bookCreate"); Iterable<BookCategory> categories = categoryDao.findAll(); model.addObject("allBookCategories", categories); return model; } @RequestMapping(value = "/bookCreate", method = RequestMethod.POST) public ModelAndView processFormCreate(@ModelAttribute Book book) { // ModelAndView model = new ModelAndView("bookDone"); ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); dao.save(book); model.addObject(book); return model; } @RequestMapping(value = { "/bookRetrieveAll"}, method = RequestMethod.GET) public ModelAndView retrieveBooks() { ModelAndView model = new ModelAndView("bookList"); Iterable<BookCategory> categories = categoryDao.findAll(); model.addObject("allBookCategories", categories); BookCategory category = categories.iterator().next();//get first category model.addObject("bookCategory", category); Iterable<Book> books = dao.findAll(); model.addObject("allBooks", books); return model; } @RequestMapping(value = { "/bookRetrieveByCategory" }, method = RequestMethod.POST) public ModelAndView retrieveBooksByCategory( @RequestParam(value = "id", required = false, defaultValue = "1") Long id) { ModelAndView model = new ModelAndView("bookList"); Iterable<BookCategory> categories = categoryDao.findAll(); model.addObject("allBookCategories", categories); BookCategory category = categoryDao.findOne(id); model.addObject("bookCategory", category); model.addObject("allBooks", category.getBooks()); return model; } @RequestMapping(value = "/bookUpdate", method = RequestMethod.GET) public ModelAndView openFormUpdate( @RequestParam(value = "id", required = false, defaultValue = "1") Long id) { ModelAndView model = new ModelAndView("bookUpdate"); Book book = dao.findOne(id); model.addObject(book); Iterable<BookCategory> categories = categoryDao.findAll(); model.addObject("allBookCategories", categories); return model; } @RequestMapping(value = "/bookUpdate", method = RequestMethod.POST) public ModelAndView processFormUpdate(@ModelAttribute Book book) { ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); dao.save(book); return model; } @RequestMapping(value = "/bookDelete", method = RequestMethod.GET) public ModelAndView deleteBook( @RequestParam(value = "id", required = false, defaultValue = "1") Long id) { ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); dao.delete(id); return model; } @RequestMapping(value = "/shoppingCartAdd", method = RequestMethod.GET) public ModelAndView addShoppingCart( @RequestParam(value = "id", required = false, defaultValue = "1") Long id) { ModelAndView model = new ModelAndView("redirect:/bookRetrieveAll"); Book book = dao.findOne(id); cart.add(book); return model; } @RequestMapping(value = "/shoppingCartList", method = RequestMethod.GET) public ModelAndView showShoppingCart() { ModelAndView model = new ModelAndView("shoppingCart"); return model; } @RequestMapping(value = "/cleanShoppingCart", method = RequestMethod.GET) public ModelAndView cleanShoppingCart() { ModelAndView model = new ModelAndView("shoppingCart"); cart.cleanup(); return model; } }bookList.html稍微修改一下,可以將所想購買的物品置入購物車(呼叫/shoppingCartAdd)。
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"/> <title>List ALl Books</title></head><body>Hello <span th:text="${session.loginId}">login id</span>!<form action="bookRetrieveByCategory" method ="post"> 類別:<select name = "bookCategory" th:field="${bookCategory.id}"> <option th:each="bookCategory : ${allBookCategories}" th:value="${bookCategory.id}" th:text="${bookCategory.name}">商業</option></select> <input type="submit" value="Submit"/> <a href="bookRetrieveAll"><input type="button" value="Show All Books"/></a></form> <a href="shoppingCartList"><input type="button" value="Show Shopping Cart"/></a><table> <tr> <th>編號</th> <th>類別</th> <th>名稱</th> <th>價格</th> <th></th> </tr> <tr th:each="book : ${allBooks} " th:object="${book}"> <td th:text="*{id}">1</td> <td th:text="*{bookCategory.name}">business</td> <td th:text="*{name}">Ben</td> <td th:text="*{price}">300</td> <td><a th:href="@{shoppingCartAdd(id=*{id})}">加入購物車</a></td> </tr></table></body></html>另外,新增一個shoppingCart.html稍微修改一下,可以看到session bean裡的內容,在Thymeleaf裡使用"@"來取得bean的內容(如:${@shoppingCart.cart})。
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"/> <title>Shopping Cart</title></head><body>Shopping Cart for <span th:text="${session.loginId}">login id</span><a href="bookRetrieveAll"><input type="button" value="Keep Shopping"/></a><a href="cleanShoppingCart"><input type="button" value="Clean up Shopping Cart"/></a><table> <tr> <th>編號</th> <th>類別</th> <th>名稱</th> <th>價格</th> <th></th> </tr> <tr th:each="book : ${@shoppingCart.cart} " th:object="${book}"> <td th:text="*{id}">1</td> <td th:text="*{bookCategory.name}">business</td> <td th:text="*{name}">Ben</td> <td th:text="*{price}">300</td> </tr> </table></body></html>