Spring Session
Spring Session
基本概念
由於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=false
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.session.store-type=jdbc
spring.session.jdbc.table-name=SPRING_SESSION
spring.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
@EnableJdbcHttpSession
public 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 Bean
如果儲存在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
@Component
public 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;
@Entity
public 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;
@Controller
public 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>
參考資料
- http://projects.spring.io/spring-session/
- http://docs.spring.io/spring-session/docs/1.3.0.RELEASE/reference/html5/guides/httpsession-jdbc-boot.html
- http://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
- http://javasampleapproach.com/spring-framework/spring-bean-scope-using-annotation-singleton-prototype-request-session-global-session-application
- http://javasampleapproach.com/spring-framework/spring-bean-scope-annotation-requestscope-sessionscope-applicationscope