版权提醒
本系列文章为原创内容。转载请注明文章来源。

前言

本文为搭建 SSSP 框架极简书店教程系列的其中一篇。本文主要内容:配置 SpringDataJpa 相关部分并实现网站后台书籍管理功能。

至此,所有 SSSP(Spring+Struts2+SpringDataJpa)框架的整合,以及其与 JavaWeb 项目的配合等重要知识点已经全部讲完。如果需要其他部分的代码,请参考在 Github 中的项目源码。

配置 SpringDataJpa

建立数据库信息统一管理文件

在 resource 目录中建立 db.properties 文件。

db.datebase=h2
db.driverClassName=org.h2.Driver
db.userName=sa
db.password=123456
db.url=jdbc:h2:~/Bookstore

配置 Spring 配置文件

向 applicationContext.xml 中添加以下内容:

    <!-- 引入数据库配置文件 db.properties -->
    <context:property-placeholder location="classpath:db.properties"/>

    <!-- 数据库连接池连接设置 -->
    <bean id="dataSource" class="org.h2.jdbcx.JdbcConnectionPool" factory-method="create" destroy-method="dispose">
        <constructor-arg index="0" value="${db.url}"/>
        <constructor-arg index="1" value="${db.userName}"/>
        <constructor-arg index="2" value="${db.password}"/>
    </bean>

    <!-- hibernate 的 Jpa 实现 -->
    <bean id="hibernateJpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
    <jpa:repositories base-package="com.chiyu.dao"
                      transaction-manager-ref="transactionManager"
                      entity-manager-factory-ref="entityManagerFactory"/>

    <!-- 配置针对 JPA 的局部事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory"/>
    </bean>
    <tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
    <!-- SpringDataJpa设置 -->
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaDialect">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect"/>
        </property>
        <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter"/>
        <property name="packagesToScan" value="com.chiyu.entity"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
                <!-- 显示SQL -->
                <prop key="hibernate.show_sql">ture</prop>
                <!-- 格式化SQL -->
                <prop key="hibernate.format_sql">ture</prop>
                <!-- 生成SQL注释 -->
                <prop key="hibernate.use_sql_comments">ture</prop>
                <!-- 配置如何根据java模型生成数据库表结构 -->
                <prop key="hibernate.hbm2ddl.auto">validate</prop>
                <!-- 配置表名字段的习惯,采用下划线,例如My_Name -> MyName -->
                <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
            </props>
        </property>
    </bean>

完成 DAO 层

可以说,使用 SpringDataJpa 的优势在此处表现地淋漓尽致了。下面是 BookDao.java 的全部代码。

欸嘿,就是这么简单!因为具体操作 SpringDataJpa 已经帮我们完成了!

package com.chiyu.dao;

import com.chiyu.entity.BookEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface BookDao extends JpaRepository<BookEntity, String> {
}

完成 Service 层

首先写接口:BookService。

有没有奇怪为什么 insert 操作与 update 操作使用同样的方法?

package com.chiyu.service;

import com.chiyu.entity.BookEntity;

import java.util.List;

public interface BookService {
    //插入或更新一条图书信息
    BookEntity insertOrUpdateBook(BookEntity book);

    //删除一条图书信息
    void deleteBook(String id);

    //获取所有图书信息
    List<BookEntity> findAllBooks();

    //寻找指定书号的图书信息
    BookEntity findBookById(String id);
}

接下来我们在 service 包下建立 Impl 包,存放 service 接口的实现类。在 Impl 包中写一个类实现这个接口:BookServiceImpl 。

package com.chiyu.service.Impl;

import com.chiyu.dao.BookDao;
import com.chiyu.entity.BookEntity;
import com.chiyu.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class BookServiceImpl implements BookService {
    private BookDao bookDao;

    public BookDao getBookDao() {
        return bookDao;
    }

    //使用Spring Bean自动赋值
    @Autowired
    public void setBookDao(BookDao bookDao) {
        this.bookDao = bookDao;
    }

    @Override
    public BookEntity insertOrUpdateBook(BookEntity book) {
        return bookDao.save(book);
    }

    @Override
    public void deleteBook(String id) {
        bookDao.deleteById(id);
    }

    @Override
    public List<BookEntity> findAllBooks() {
        return bookDao.findAll();
    }

    @Override
    public BookEntity findBookById(String id) {
        return bookDao.findById(id).orElse(null);

    }
}

是不是很神奇?对数据库书籍表的增删改查工作,我们从未写过,但我们就是可以直接用!感谢 SpringDataJpa 。
同时,回答上面的问题,插入与更新使用同一个方法,是因为 SpringDataJpa 中,这两个操作都使用 save() 方法,区别两种操作的方法,是要保存的项的主键在数据库中是否已经存在。

SpringDataJpa 的使用并不需要写 SQL 语句,而是依靠方法名,例如: findBookById(String id) ,如果把 find 改为 select ,是不是看到了 SQL 的影子?如果需要进行其他对数据库的操作,需要使用对应的方法名。方法名命名规则可以参考SpringDataJpa 官方文档

.orElse(null) 方法并不是 SpringDataJpa 中定义的,因为在执行数据库中查找操作时,可能找不到匹配的项,所以 SpringDataJpa 中查找操作返回的数据类型为 java.util.Optional ,即可以为空的类型。使用 .orElse(null) 方法,可以让 Optional 类型直接返回原始数据类型,如果为空,则返回 null 。

完成 Action 层

建立 后台书籍管理 Action :BookManager 。

@Autowired 使用 Spring Bean 为此 set 方法对应的变量赋值。
其他变量的 get 和 set 方法用于实现 Struts 的 Action 与 jsp 页面的数据传递。
InputStream 用于完成 AJAX 操作后向请求方返回数据。

package com.chiyu.action;

import com.chiyu.entity.BookEntity;
import com.chiyu.service.BookService;
import com.opensymphony.xwork2.ActionSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;

@Controller
public class BookManager extends ActionSupport {
    //用于获取从页面返回的书籍信息。
    private BookEntity book;
    private List<BookEntity> bookList;
    //用于对书籍进行各种操作的服务层,由Spring Bean赋值
    private BookService bookService;
    //用于使用AJAX技术执行书籍添加删除更新时向网页传递信息
    private InputStream inputStream;

    public BookEntity getBook() {
        return book;
    }

    public void setBook(BookEntity book) {
        this.book = book;
    }

    public List<BookEntity> getBookList() {
        return bookList;
    }

    public void setBookList(List<BookEntity> bookList) {
        this.bookList = bookList;
    }

    public BookService getBookService() {
        return bookService;
    }

    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

    public InputStream getInputStream() {
        return inputStream;
    }

    public void setInputStream(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    //访问书籍管理页面,并返回所有书籍信息。
    @Override
    public String execute() throws Exception {
        //返回所有书籍信息
        bookList = bookService.findAllBooks();
        return SUCCESS;
    }

    //添加或更新图书方法,使用AJAX
    public String addOrUpdateBook(){
        bookService.insertOrUpdateBook(book);
        String result = "操作成功";
        inputStream=new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8));
        return "addOrUpdateBookResult";
    }

    //删除图书方法,使用AJAX
    public String deleteBook(){
        bookService.deleteBook(book.getBid());
        String result = "删除成功";
        inputStream=new ByteArrayInputStream(result.getBytes(StandardCharsets.UTF_8));
        return "deleteBookResult";
    }
}

配置 Struts 配置文件

在 background.xml 文件中添加以下内容:

	<action name="bookManager" class="bookManager">
            <result name="success">/admin/admin_book.jsp</result>
            <result name="addOrUpdateBookResult" type="stream">
                <!-- 设置返回给浏览器的数据类型 -->
                <param name="contentType">text/html</param>
                <!--指定获取InputStream的方法,getInputStream(),约定:去掉get,后面部分使用camel写法 -->
                <param name="inputName">inputStream</param>
            </result>
            <result name="deleteBookResult" type="stream">
                <!-- 设置返回给浏览器的数据类型 -->
                <param name="contentType">text/html</param>
                <!--指定获取InputStream的方法,getInputStream(),约定:去掉get,后面部分使用camel写法 -->
                <param name="inputName">inputStream</param>
            </result>
        </action>

下面两个 result 用于指定向 AJAX 请求发起方返回的数据类型和数据名。

完成书籍管理页面

admin_book.jsp 全部代码如下:

<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!doctype html>
<html lang="zh-cmn-Hans">
<head>
    <title>后台-书籍管理</title>
    <!-- 引入CSS样式和JS脚本文件等-->
    <%@include file="common/admin_headContext.jsp"%>
</head>
<body>
<!-- 引入顶部导航条-->
<%@include file="common/admin_common_top.jsp"%>
<div class="container-fluid">
    <div class="row">
        <!-- 引入左侧导航栏-->
        <%@include file="common/admin_common_left.jsp"%>
        <!-- 页面主体 -->
        <main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">

            <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
                <h1 class="h2">书籍总览</h1>
                <div class="btn-toolbar mb-2 mb-md-0">
                    <div class="mr-2">
                        <button type="button" class="btn btn-sm btn-outline-secondary" id="addBookButton">新增书籍</button>
                    </div>
                </div>
            </div>

            <table class="table table-striped">
                <thead>
                <tr>
                    <th scope="col">书号</th>
                    <th scope="col">书名</th>
                    <th scope="col">单价</th>
                    <th scope="col">作者</th>
                    <th scope="col">出版社</th>
                    <th scope="col">操作</th>
                </tr>
                </thead>
                <tbody id="bookList">
                    <s:iterator value="bookList">
                        <tr>
                            <th scope="row"><s:property value="bid"/></th>
                            <td><s:property value="bname"/></td>
                            <td><s:property value="price"/></td>
                            <td><s:property value="author"/></td>
                            <td><s:property value="press"/></td>
                            <td>
                                <div class="btn-group mr-2">
                                    <button type="button" class="btn btn-sm btn-outline-secondary" onclick="updateBookFunc(this)">修改</button>
                                    <button type="button" class="btn btn-sm btn-outline-secondary" onclick="deleteBookFunc(this)">删除</button>
                                </div>
                            </td>
                        </tr>
                    </s:iterator>
                </tbody>
            </table>

        </main>
    </div>
</div>

<script type="text/javascript">
    <!-- 设置左侧导航栏为正确选中项-->
    $(document).ready(function(){
        $("#book").addClass("active");
    });

    <!--对应添加按钮-->
    $("#addBookButton").click(function() {
        $("#bookList").append("<tr id='newLine'><th scope=\"row\"><input id='input_bid' type=\"text\" name=\"book.bid\" placeholder=\"书号\" required autofocus></th> <td><input id='input_bname' type=\"text\" name=\"book.bname\" placeholder=\"书名\" required></td> <td><input id='input_price' type=\"text\" name=\"book.price\" placeholder=\"单价\" required></td> <td><input id='input_author' type=\"text\" name=\"book.author\" placeholder=\"作者\" required></td> <td><input id='input_press' type=\"text\" name=\"book.press\" placeholder=\"出版社\" required></td> <td> <button onclick='submitBookFunc(this)' type=\"button\" class=\"btn btn-sm btn-outline-secondary deleteBookButton\">提交</button> </td></tr>");
        //只有在一条新记录提交完成后,才能创建下一条新的记录。
        $(this).attr("disabled",true);
    });

    <!--对应添加提交按钮-->
    function submitBookFunc(btn){
        //防止多次提交
        $(btn).attr("disabled",true);
        let bid = $("#input_bid").val();
        let bname = $("#input_bname").val();
        let price = $("#input_price").val();
        let author = $("#input_author").val();
        let press = $("#input_press").val();
        $.ajax({
            url:"bookManager!addOrUpdateBook.action",
            type:"post",
            data:{"book.bid":bid,"book.bname":bname,"book.price":price,"book.author":author,"book.press":press},
            dataType:"text",
            error:function (){
                alert("操作失败!");
            },
            success:function (data){
                alert(data);
                if (data === "操作成功"){
                    //只有在一条新记录提交完成后,才能创建下一条新的记录。
                    $("#addBookButton").attr("disabled",false);
                    $("#newLine").remove();
                    $("#bookList").append("<tr><th scope='row'>" + bid+ "</th> <td>" + bname + "</td> <td>" + price + "</td> <td>" + author + "</td> <td>" + press + "</td> <td> <div class='btn-group mr-2'> <button type='button' class='btn btn-sm btn-outline-secondary' onclick='updateBookFunc(this)'>修改</button><button type='button' class='btn btn-sm btn-outline-secondary' onclick='deleteBookFunc(this)'>删除</button></div></td></tr>'");
                }
            }
        })
    }

    <!--对应修改按钮-->
    function updateBookFunc(btn){
        let item = $(btn).parents("div").parent("td").siblings("th");
        //防止多次提交
        $(btn).attr("disabled",true);
        let bid = item.text();
        let bname = item.next().text();
        let price = item.next().next().text();
        let author = item.next().next().next().text();
        let press = item.next().next().next().next().text();
        item.parents("tr").remove();
        $("#bookList").append("<tr id='newLine'><th scope=\"row\"><input id='input_bid' type=\"text\" name=\"book.bid\" value=" + bid + " required disabled></th> <td><input id='input_bname' type=\"text\" name=\"book.bname\" value=" + bname + " required></td> <td><input id='input_price' type=\"text\" name=\"book.price\" value=" + price + " required></td> <td><input id='input_author' type=\"text\" name=\"book.author\" value=" + author + " required></td> <td><input id='input_press' type=\"text\" name=\"book.press\" value=" + press + " required></td> <td> <button onclick='submitBookFunc(this)' type=\"button\" class=\"btn btn-sm btn-outline-secondary deleteBookButton\">提交</button> </td></tr>");
    }

    <!--对应删除按钮-->
    function deleteBookFunc(btn){
        if(confirm('确定要删除该行信息?')){
            let item = $(btn).parents("div").parent("td").siblings("th");
            //防止多次提交
            $(btn).attr("disabled",true);
            let bid = item.text();
            $.ajax({
                url:"bookManager!deleteBook.action",
                type:"post",
                data:{"book.bid":bid},
                dataType:"text",
                error:function (){
                    alert("删除失败!");
                },
                success:function (data){
                    alert(data);
                    if (data === "删除成功"){
                        $(btn).parents("tr").remove();
                    }else {
                        $(btn).attr("disabled",false);
                    }
                }
            })
        }
    }
</script>
</body>
</html>

在 JSP 中接收 struts 中的 List 类型数据并遍历

这部分是解释上面 jsp 文件的部分内容。

"bookList" 是 struts 中 List 变量的变量名,"bid"是 List 中存储的项的数据类型中的变量名。

<s:iterator value="bookList">
	<tr>
		<th scope="row"><s:property value="bid"/></th>
		<td><s:property value="bname"/></td>
		<td><s:property value="price"/></td>
		<td><s:property value="author"/></td>
		<td><s:property value="press"/></td>
		<td>
			<div class="btn-group mr-2">
				<button type="button" class="btn btn-sm btn-outline-secondary" onclick="updateBookFunc(this)">修改</button>
				<button type="button" class="btn btn-sm btn-outline-secondary" onclick="deleteBookFunc(this)">删除</button>
			</div>
		</td>
	</tr>
</s:iterator>

使用 AJAX 技术

这部分是解释上面 jsp 文件的部分内容。

function submitBookFunc(btn){
        //防止多次提交
        $(btn).attr("disabled",true);
        let bid = $("#input_bid").val();
        let bname = $("#input_bname").val();
        let price = $("#input_price").val();
        let author = $("#input_author").val();
        let press = $("#input_press").val();
        $.ajax({
            url:"bookManager!addOrUpdateBook.action",
            type:"post",
            data:{"book.bid":bid,"book.bname":bname,"book.price":price,"book.author":author,"book.press":press},
            dataType:"text",
            error:function (){
                alert("操作失败!");
            },
            success:function (data){
                alert(data);
                if (data === "操作成功"){
                    //只有在一条新记录提交完成后,才能创建下一条新的记录。
                    $("#addBookButton").attr("disabled",false);
                    $("#newLine").remove();
                    $("#bookList").append("<tr><th scope='row'>" + bid+ "</th> <td>" + bname + "</td> <td>" + price + "</td> <td>" + author + "</td> <td>" + press + "</td> <td> <div class='btn-group mr-2'> <button type='button' class='btn btn-sm btn-outline-secondary' onclick='updateBookFunc(this)'>修改</button><button type='button' class='btn btn-sm btn-outline-secondary' onclick='deleteBookFunc(this)'>删除</button></div></td></tr>'");
                }
            }
        })
    }

将 JSON 作为 Ajax 的返回值类型

这部分是解释上面 jsp 文件的部分内容。

见本博客文章如何在 Struts2 中将 JSON 作为 Ajax 的返回值类型

配置相关文件使管理员点击“书籍管理”后跳转到书籍管理

在 BackgroundDispatcher 类中添加以下方法:

    //到达后台书籍管理。
    public String toBookManage() {
        return "toBookManage";
    }

修改 background.xml

<package name="background" namespace="/admin" extends="struts-default" strict-method-invocation="false">
        <!--后台总调度-->
        <action name="index" class="backgroundDispatcher">
            <result name="input">/admin/admin_login.jsp</result>
            <result name="success">/admin/admin_welcome.jsp</result>
            <result name="login">/admin/admin_login.jsp</result>
            <result name="toBookManage" type="redirectAction">bookManager.action</result>
        </action>
        <!--管理员登录相关-->
        <action name="toLogin" class="adminLogin">
            <result name="success" type="redirectAction">index.action</result>
            <result name="input">/admin/admin_login.jsp</result>
        </action>
        <!--书籍管理-->
        <action name="bookManager" class="bookManager">
            <result name="success">/admin/admin_book.jsp</result>
            <result name="addOrUpdateBookResult" type="stream">
                <!-- 设置返回给浏览器的数据类型 -->
                <param name="contentType">text/html</param>
                <!--指定获取InputStream的方法,getInputStream(),约定:去掉get,后面部分使用camel写法 -->
                <param name="inputName">inputStream</param>
            </result>
            <result name="deleteBookResult" type="stream">
                <!-- 设置返回给浏览器的数据类型 -->
                <param name="contentType">text/html</param>
                <!--指定获取InputStream的方法,getInputStream(),约定:去掉get,后面部分使用camel写法 -->
                <param name="inputName">inputStream</param>
            </result>
        </action>
    </package>

运行项目

现在,在登录后台,点击“书籍管理”,你应该可以看到从数据库获取的书籍信息。

现在项目所需的主要知识以及介绍完毕,开始你的创作吧!