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>

參考資料