(SpringMVC+Spring+Mybatis+Bootstrap)保姆级教程:SSM框架整合—图书管理系统
前言
提示:
本篇文章主要是针对狂神视频中的SSM书籍管理项目进行介绍,小白博主所做的工作,是将自己在完成项目的过程中遇到的困难以及我觉得讲解视频中可能不方便理解的内容进行复述,同时结合已有的一些优秀博客进行进一步总结。希望可以帮助到这在学习SSM框架的其他小白们。
本文主要还是我的学习总结,因为网上的一些知识分布比较零散故作整理叙述。如果有不对的地方,还请帮忙指正。本文不产生任何收益,如有出现禁止转载、侵权的图片,亦或者任何不被允许转载的文字内容请联系我进行修改。
相关参考:
【狂神说Java】SpringMVC最新教程IDEA版通俗易懂:链接: link.
李奕锋:手把手教你整合最优雅SSM框架:SpringMVC + Spring + MyBatis 链接: link
狂神说SpringMVC05:整合SSM框架 链接: link.
网络笨猪:SSM三大框架的运行流程、原理、核心技术详解 链接: link.
青城山小和尚:Web服务器工作原理详解(基础篇) 链接: link.
柒晓白:SSM框架下web项目运行流程 链接: link.
进击的小赤佬:Maven静态资源过滤 链接: link.
Java-我的世界-begin:tomcat 工作原理 链接: link.
sp_wxf:mvc:annotation-driven和mvc:default-servlet-handler的作用 链接: link.
zcl1199:spring 和mybatis整合时 使用context:property-placeholder载不进属性 还报org.springframework.beans.factory.BeanCrea:链接: link.
黄玮鹏:Web.xml详解:链接: link
莫振杰 人民邮电出版社:从0到1:HTML+CSS+JavaScript 快速上手
Horizons-code
:org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned 链接: link.
文章目录
SSM框架介绍
SSM框架中各框架的作用
web访问流程
如下图所示,当用户输入url进行页面访问时,前端接受到用户的url请求,根据url将请求交给controller中相映射的方法进行处理,controller接受请求后,在方法体内调用service实现类中的相应业务,由业务方法进一步调用dao操作,最后由dao操作来执行sql语句对数据库进行访问。

为什么是自底向上?
笔者的理解是因为开发者在实际编码阶段,应该先从dao操作的实现开始做起,然后再向上逐步编写实现service,实现controller,以及前端页面。
各框架在项目中的作用
-
SpringMVC: 负责Controller控制层以及View视图层,用来处理用户请求,当用户在地址栏中输入http://*,springmvc将对url请求进行拦截并拆分成三部分,
例如:http://localhost:8080/springmvc/hello, 那么经过springmvc分析后,可以表示为,请求位于服务器localhost:8080上的springmvc站点的hello控制器。然后由springmvc调用控制器中hello映射的方法,进行相应处理并将结果返回到页面呈现给用户。 -
Spring: spring负责Service业务层,Spring中的IOC容器可以装载bean,我们把创建对象和维护对象间关系的权力交给spring容器。对于添加了bean标签的类,当我们加载spring框架时,spring会自动创建一个bean对象,并存入内存。
此外在Spring的xml配置文件中定义此接口的实现类,就可在其他模块中调用此接口来进行数据业务的处理,而不用关心接口的具体实现类是哪个类,这里往往用到的就是反射机制,DAO层的jdbc.properties数据源配置,以及有关数据库连接的参数都在Spring的配置文件中进行配置。 -
Mybatis:Mybatis完成了对JDBC的封装,它使得数据库底层操作透明化,通过mybatis-config.xml来获取sqlSessionFactory对象申请一个sqlSession实例,sqlSession完全包含了面向数据库执行SQL命令所需的所有方法,我们用sqlSession来获取dao操作对应的实现,再执行sql命令。
这样的好处在于可以自由控制sql,同时可以使用xml方式来组织管理sql语句。
项目结构图
项目整体结构如下所示:



项目环境搭建
创建数据库
打开Navicat,创建数据库连接,有的话直接双击等待连接亮起即可。编写SQL代码如下。注意不同红框内的内容应该分开执行。
1.先新建ssmbuild数据库;
2.转入新建好的ssmbuild数据库;
3.创建books表单,并设置表单属性并设置bookID为主键
4.利用insert 语句往数据库中添加初始数据

新建Maven项目并添加web支持
创建好数据库之后,打开我们的IDEA创建一个Maven项目。添加web依赖,完成后观察是否出现了web目录。


导入pom相关依赖
往pom.xml中导入需要的jar包。同时刷新maven等待jar包加载完成。
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<!--Junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!--数据库驱动-->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!--Servlet - JSP -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--Mybatis-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<!--Spring-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>
刷新后观察依赖是否成功导入。

Maven资源过滤设置
标准Maven项目都会有一个resources目录用来存放所有的资源配置文件,但我们有时候也需要将一些配置文件放在src目录下,此时如果没有进行静态资源导出,就会导致maven编译时,放在java目录下的.xml静态文件在target中找不到。
(这里之所以还写上src/main/resources只是保险起见,也可以不写,既然追求刺激那就贯彻到底喽 )
<!-- 静态资源导出-->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>`在这里插入代码片`
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
建立基本结构和配置框架
依照项目结构在java目录和resources下创建以下几个包和xml文件
- com.kouti.pojo 存放javaBean
- com.kouti.dao 存放dao类,dao类对数据库进行操作 DAO(Data Access Object) 数据访问对象
- com.kouti.service 存放service接口和实现类,service通过组合调用dao操作来实现业务
- com.kouti.controller 存放controller类,实现不同url请求的页面跳转
- mybatis-config.xml 进行mybatis配置
- applicationContext.xml Spring配置整合文件
- com.kouti.filter 可选项
Mybatis层
编写数据库配置文件 database.properties
在resoures下创建我们的database.properties文件,设置我们的驱动器,这里注意因为我们加载数据库连接依赖mysql-connector是5.x版本,所以用com.mysql.jdbc.Driver就好,如果是6.x版本(使用哪个版本取决于你的MySQL版本是什么版本),使用com.mysql.cj.jdbc.Driver
账户名密码使用自己的账号密码。
url中,突出注意端口号使用3306,不是3366 ,此处使用3366是因为博主的MySQL端口是3366,正常默认端口是3306,如果不能使用注意考虑时区问题,在后面添加&serverTimezone=Asia/Shanghai

配置Maven项目中的DataBase
选中MySQL进行配置

输入账户名和密码,点击左下Test Connection测试是否可以连接成功.

此处可能提示时区错误,设置时区为Asia/Shanghai即可.
连接成功后进入Schemas选中我们新建好的数据库ssmbuild,点击Apply再点击OK即可
此时Maven连接数据库成功.可以双击books查看表单存储元组内容.

编写Mybatis的核心配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 开启log4j日志-->
<!-- </properties>-->
<!-- <settings>-->
<!-- <setting name="logImpl" value="LOG4J"/>-->
<!-- <setting name="cacheEnabled" value="true"/>-->
<!-- </settings>-->
<!-- 别名-->
<typeAliases>
<typeAlias type="com.kouti.pojo.Books" alias="Books"></typeAlias>
</typeAliases>
<!-- 映射-->
<mappers>
<mapper resource="com/kouti/dao/BookMapper.xml"/>
</mappers>
</configuration>
编写数据库对应实体类 Books.java
这里注意保持成员变量名称与数据库成员属性名称一致,避免很多不必要的麻烦.如果打算使用lombok,只需要在pom.xml中导入相关jar包,并在定义好的Books上添加@Data,@AllArgsConstructor,@NoArgsConstructor注解即可,此处@Component注解是为了让该类可以被spring扫描器扫描.
package com.kouti.pojo;
import org.springframework.stereotype.Component;
@Component
public class Books {
private int bookID;
private String bookName;
private int bookCounts;
private String detail;
public int getBookID() {
return bookID;
}
public void setBookID(int bookID) {
this.bookID = bookID;
}
public String getBookName() {
return bookName;
}
public void setBookName(String bookName) {
this.bookName = bookName;
}
public int getBookCounts() {
return bookCounts;
}
public void setBookCounts(int bookCounts) {
this.bookCounts = bookCounts;
}
public String getDetail() {
return detail;
}
public void setDetail(String detail) {
this.detail = detail;
}
public Books() {
}
public Books(int bookID, String bookName, int bookCounts, String detail) {
this.bookID = bookID;
this.bookName = bookName;
this.bookCounts = bookCounts;
this.detail = detail;
}
@Override
public String toString() {
return "Books{" +
"bookID=" + bookID +
", bookName='" + bookName + '\'' +
", bookCounts=" + bookCounts +
", detail='" + detail + '\'' +
'}';
}
}
编写Dao层的Mapper接口 BookMapper.java
定义几种dao操作,分别是增删改查. 添加@Param注解是为了保证Mapper映射器#{}引用时可以按名称找到对应参数,当方法只接受一个参数的情况下可以用,接受多个参数的情况下,建议一定用@Param
package com.kouti.dao;
import com.kouti.pojo.Books;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface BookMapper {
//增加一个Books
int addBook(Books book);
//根据id进行删除
int deleteBookById(@Param("bookID") int id);
//更新Book
int updateBook(Books books);
//根据id查询,返回了一个Book
Books queryBookById(@Param("bookID") int id);
//查询全部Book,返回list集合
List<Books> queryAllBook();
//按名称查询
Books queryBookByName(@Param("bookName") String bookName);
}
编写接口对应的Mapper的xml文件 BookMapper.xml
注意namespace以及sql语句不要编写出错
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kouti.dao.BookMapper">
<!-- 增加一个Book-->
<insert id="addBook" parameterType="Books">
insert into books(bookID,bookName,bookCounts,detail) values (#{bookID},#{bookName},#{bookCounts},#{detail})
</insert>
<!--根据id删除一个Book-->
<delete id="deleteBookById" parameterType="int">
delete from books where bookID=#{bookID}
</delete>
<!--更新Book-->
<update id="updateBook" parameterType="Books">
update books
set bookName=#{bookName},bookCounts=#{bookCounts},detail=#{detail}
where bookID=#{bookID}
</update>
<!--根据id查询,返回一个Book-->
<select id="queryBookById" resultType="Books">
select * from books where bookID=#{bookID}
</select>
<!--查询全部Book-->
<select id="queryAllBook" resultType="Books">
select * from books
</select>
<!-- 按名称查询-->
<select id="queryBookByName" parameterType="String" resultType="Books">
select * from ssmbuild.books where bookName=#{bookName}
</select>
</mapper>
编写Service层的接口
service层可以组合多个dao操作实现业务,本项目中每个业务只涉及单纯的增删改查,所以直接定义增删改查业务即可.
package com.kouti.service;
import com.kouti.pojo.Books;
import java.util.List;
//BookService 底下需要去实现,调用dao层
public interface BookService {
//增加一个Books
int addBook(Books book);
//根据id进行删除
int deleteBookById(int id);
//更新Book
int updateBook(Books books);
//根据id查询,返回了一个Book
Books queryBookById(int id);
//查询全部Book,返回list集合
List<Books> queryAllBook();
//按名称查询
Books queryBookByName(String bookName);
}
编写Service接口实现类 BookService.java BookServiceImpl.java
@AutoWired注解将根据属性类型进行自动装配,将自动导入BookMapper接口的实现类, BookService的抽象方法实现起来也非常简单.直接通过导入的bookMapper调用相应的dao操作方法返回操作结果即可.
package com.kouti.service;
import com.kouti.dao.BookMapper;
import com.kouti.pojo.Books;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class BookServiceImpl implements BookService{
//service调dao层
//调用dao层的操作,设置一个set接口,方便spring管理
@Autowired
private BookMapper bookMapper;
public void setBookMapper(BookMapper bookMapper) {
this.bookMapper = bookMapper;
}
@Override
public int addBook(Books book) {
return bookMapper.addBook(book);
}
@Override
public int deleteBookById(int id) {
return bookMapper.deleteBookById(id);
}
@Override
public int updateBook(Books books) {
return bookMapper.updateBook(books);
}
@Override
public Books queryBookById(int id) {
return bookMapper.queryBookById(id);
}
@Override
public List<Books> queryAllBook() {
return bookMapper.queryAllBook();
}
@Override
public Books queryBookByName(String bookName) {
return bookMapper.queryBookByName(bookName);
}
}
Spring层
编写Spring整合MyBatis的配置文件 spring-dao.xml
Spring-dao主要负责整合Mybatis配置文件,将原先Mybatis项目中我们中读取properties文件连接数据库等工作,交给spring进行处理.起到整合mybatis的作用
加载了properties文件后,配置c3p0线程池连接数据库.
通过加载mybatis-config.xml文件来配置sqlSessionFactory对象,有了sqlSessionFactory对象就可以申请sqlSession,由sqlSession去获取Mapper映射器,然后由映射器执行dao操作.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置整合mybatis -->
<!-- 1.关联数据库文件 -->
<!-- 通过spring来读配置文件,而不再通过mybatis-->
<context:property-placeholder location="classpath:database.properties"/>
<!-- 2.数据库连接池 -->
<!--数据库连接池
dbcp 半自动化操作 不能自动连接
c3p0 自动化操作(自动的加载配置文件 并且设置到对象里面)
-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${driver}"/>
<property name="jdbcUrl" value="${url}"/>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<!-- <property name="driverClass" value="com.mysql.jdbc.Driver"/>-->
<!-- <property name="jdbcUrl" value="jdbc:mysql://localhost:3366/ssmbuild?useSSL=true&useUnicode=true&characterEncoding=UTF-8"/>-->
<!-- <property name="user" value="root"/>-->
<!-- <property name="password" value="123"/>-->
<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false"/>
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000"/>
<!-- 当获取连接失败重试次数 -->
<property name="acquireRetryAttempts" value="2"/>
</bean>
<!-- 3.配置SqlSessionFactory对象 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>
<!-- 4.配置扫描Dao接口包,动态实现Dao接口注入到spring容器中 -->
<!--解释 :https://www.cnblogs.com/jpfss/p/7799806.html-->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 注入sqlSessionFactory -->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描Dao接口包 -->
<property name="basePackage" value="com.kouti.dao"/>
</bean>
</beans>
org.springframework.beans.factory.BeanCrea错误
错误原因:context:property-placeholder载不进属性 ,jdbc.properties中属性压根没有加载,导致把配置的变量名直接当成参数值,才会报错。
引用:
在spring里使用org.mybatis.spring.mapper.MapperScannerConfigurer 进行自动扫描的时候,设置了sqlSessionFactory 的话,可能会导致 PropertyPlaceholderConfigurer失效,也就是用${jdbc.username}这样之类的表达式,将无法获取到properties文件里的内容。 导致这一原因是因为,MapperScannerConigurer实际是在解析加载bean定义阶段的,这个时候要是设置sqlSessionFactory的话,会导致提前初始化一些类,这个时候,PropertyPlaceholderConfigurer还没来得及替换定义中的变量,导致把表达式当作字符串复制了。 但如果不设置sqlSessionFactory 属性的话,就必须要保证sessionFactory在spring中名称一定要是sqlSessionFactory ,否则就无法自动注入。又或者直接定义 MapperFactoryBean ,再或者放弃自动代理接口方式。(待修改)
编写Spring整合Service层的配置文件 spring-service.xml
扫描service包将相应的bean注入IOC容器,同时配置事务管理器,要使用Spring的事务,首先要先定义一个与数据源连接的DataSource,然后使用Spring的DataSourceTransactionManager,并传入指向DataSource的引用
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描service相关的bean -->
<context:component-scan base-package="com.kouti.service"/>
<!-- -->
<!-- 将所有业务类,注入到spring,通过配置,或注解实现-->
<bean id="BookServiceImpl" class="com.kouti.service.BookServiceImpl">
<property name="bookMapper" ref="bookMapper"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
SpringMVC层
编写web.xml
当启动一个WEB项目时,容器包括(JBoss、Tomcat等)首先会读取项目web.xml配置文件里的配置,当这一步骤没有出错并且完成之后,项目才能正常地被启动起来。
配置说明:
1.springmvc是通过dispatcherservlet来接受并拦截请求的,所以首先注册dispatcherservlet前置控制器作为springmvc的控制中心.
2.配置编码过滤器,编码方式设置为utf-8,扫描设定为/*,保证可以扫描到jsp文件.从而避免发生中文乱码的情况
3.设定服务器保存会话的时间,即session的有效期,这里设置为15min即可.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--DispatcherServlet-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<!--一定要注意:我们这里加载的是总的配置文件,之前被这里坑了!-->
<!-- 这样dispatcherservlet才能找到service-->
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--encodingFilter-->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--Session过期时间-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>
</web-app>
编写spring-mvc.xml
先说一下视图解析器,这个很好理解,其实就是就是给controller返回的string,比如hello,视图解析器收到之后会给hello加上配置好的前缀后缀,从而让dispatcherservlet中心控制器可以找到相应的jsp文件.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 配置SpringMVC -->
<!-- 1.开启SpringMVC注解驱动 -->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置-->
<mvc:default-servlet-handler/>
<!-- 3.配置jsp 显示ViewResolver视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/jsp/" />
<property name="suffix" value=".jsp" />
</bean>
<!-- 4.扫描web相关的bean -->
<context:component-scan base-package="com.kouti.controller" />
</beans>
请求如何被处理
在理解一下注解驱动和静态资源配置的作用之前
首先我们得知道一个请求是如何被处理的.
1.所有的请求一定会被Servlet处理.springmvc中的话.就是DispatcherServlet处理
2.一个请求最多只会被一个Servlet处理 :
至于由哪个Servvlet处理,由Servlet配置的url-pattern决定,选择匹配程度最高的.
3.能够处理请求的Servlet有哪些:
- 应用中web.xml中注册的Servlet;(DispatcherServlet)
- tomcat配置目录下web.xml中的两个Servlet:DefaultServlet、JspServlet
DispatcherServlet的url-pattern为什么是 / 而不是 /*:
因为配置/*,jsp也会被交给 DispatcherServlet处理.但这本是tomcat中JspServlet的工作,DispatcherServlet只是用来覆盖tomcat中的DefaultServlet,接管相应请求.
按照Servlet能够处理的请求类型,可以把springmvc的请求分类:JSP请求、@RequestMapping映射的请求、其他请求(html、js、png等静态请求);
- 如果是JSP请求,由于JspServlet的匹配程度最高,这种请求由tomcat处理;
- 如果是@RequestMapping映射的请求,由DispatcherServlet处理;
- 如果是其他请求,由DispatcherServlet处理,由于没有配置请求映射,所以无法处理(@RequestMapping对应的处理函数内部跳转到静态资源不算,这属于Servlet的内部逻辑)
mvc:annotation-driven和mvc:default-servlet-handler的作用
这刚才的说明基础上再理解一下注解驱动和静态资源配置的作用.
已知我们想让DispatcherServlet处理除*.jsp请求以外的所有请求.但因为我们的配置过程中是不会写@RequestMapping("/*.html")这种映射的,所以这会导致我们的静态资源无法请求
这意味着如果DispatcherServlet的url-pattern配置成"/",默认情况下springmvc是没有处理静态资源请求能力的
这时就需要用到mvc:default-servlet-handler/, 也就引出了mvc:default-servlet-handler/、<mvc:annotation-driven />两个配置.,让tomcat调用内置的DefaultServlet去进行处理.
Spring配置整合文件 application.xml
application.xml编写
关联spring-dao.xml,spring-mvc.xml,spring-service.xml三个xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="spring-dao.xml"/>
<import resource="spring-service.xml"/>
<import resource="spring-mvc.xml"/>
</beans>
关联文件 Spring Alication Context
将四个xml文件都添加进同一个Application Context中,然后查看是否关联成功.



测试数据库是否连接成功
进行测试,如果控制台成功输出结果则说明没有问题
package com.kouti.dao;
import com.kouti.pojo.Books;
import com.kouti.service.BookService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class BooksTest {
@Test
public void test(){
ApplicationContext context=new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookServiceImpl=(BookService) context.getBean("BookServiceImpl");
for(Books books:bookServiceImpl.queryAllBook()){
System.out.println(books);
}
}
}
配置Tomcat
回顾Tomcat组成部分
- Server
- Service:
Service是一个集合:由一个或者多个Connector以及一个Engine组成,负责处理所有Connector所获得的客户请求 - Connector:
一个Connector将在某个指定端口上侦听客户请求,并将获得的请求交给Engine来处理,从Engine处获得回应并返回客户
TOMCAT有两个典型的Connector,一个直接侦听来自browser的http请求,一个侦听来自其它WebServer的请求
Coyote Http/1.1 Connector 在端口8080处侦听来自客户browser的http请求
Coyote JK2 Connector 在端口8009处侦听来自其它WebServer(Apache)的servlet/jsp代理请求 - Engine:
Engine下可以配置多个虚拟主机Virtual Host,每个虚拟主机都有一个域名
当Engine获得一个请求时,它把该请求匹配到某个Host上,然后把该请求交给该Host来处理
Engine有一个默认虚拟主机,当请求无法匹配到任何一个Host上的时候,将交给该默认Host来处理 - Host:
代表一个Virtual Host,虚拟主机,每个虚拟主机和某个网络域名Domain Name相匹配
每个虚拟主机下都可以部署(deploy)一个或者多个Web App,每个Web App对应于一个Context,有一个Context path
当Host获得一个请求时,将把该请求匹配到某个Context上,然后把该请求交给该Context来处理
匹配的方法是“最长匹配”,所以一个path==""的Context将成为该Host的默认Context
所有无法和其它Context的路径名匹配的请求都将最终和该默认Context匹配 - Context:
一个Context对应于一个Web Application,一个Web Application由一个或者多个Servlet组成
Context在创建的时候将根据配置文件CATALINA_HOME/conf/web.xml和WEBAPP_HOME/WEB-INF/web.xml载入Servlet类
当Context获得请求时,将在自己的映射表(mapping table)中寻找相匹配的Servlet类
如果找到,则执行该类,获得请求的回应,并返回
Tomcat Server结构图:

Tomcat Servlet处理一个http请求的过程
假设来自客户的请求为:
http://localhost:8080/wsota/wsota_index.jsp
- 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector获得
- Connector把该请求交给它所在的Service的Engine来处理,并等待来自Engine的回应
- Engine获得请求localhost/wsota/wsota_index.jsp,匹配它所拥有的所有虚拟主机Host
- Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机)
- localhost Host获得请求/wsota/wsota_index.jsp,匹配它所拥有的所有Context
- Host匹配到路径为/wsota的Context(如果匹配不到就把该请求交给路径名为""的Context去处理)
- path="/wsota"的Context获得请求/wsota_index.jsp,在它的mapping table中寻找对应的servlet
- Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类
- 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法
10)Context把执行完了之后的HttpServletResponse对象返回给Host
11)Host把HttpServletResponse对象返回给Engine
12)Engine把HttpServletResponse对象返回给Connector
13)Connector把HttpServletResponse对象返回给客户browser
Tomcat配置
先查看是否打包成功
主要注意lib包导入,如果Artifacts中没有打好的war,点击上面的+号自行添加就好,如果不能导入,则查看Modules是否添加了web目录.

配置tomcat然后添加war包,Apply即可.这里博主为了方便。将Application context写成了" / ",这个随意。

Controller和视图层
①查询全部书籍:
编写首页 index.jsp
因为我们没有在web.xml中设置主页,所以这里我们的默认主页是index.jsp。我们直接进行主页。CSS
设置这边就不讲解了。a,h3都是CSS选择器,我们知道CSS通过选择器给相应的模块进行了装饰即可。
在html主体部分。我们写一个超链接标签,并设置点击跳转到http://localhost:8080/book/allBook即可,这样跳转之后,controller就会根据新的url请求找到相应的映射器进行处理。有了我们的index.jsp。我们就得把这个book/allBook的映射方法糊一下。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>首页</title>
<style type="text/css">
a{
text-decoration: none;
color:black;
font-size: 18px;
}
h3{
width:180px;
height: 38px;
margin: 100px auto;
text-align: center;
line-height: 38px;
background: deepskyblue;
border-radius: 4px;
}
</style>
</head>
<body>
<h3>
<a href="${pageContext.request.contextPath}/book/allBook"/>进入书籍页面</a>
</h3>
</body>
</html>
启动tomcat观察一下。

控制器编写 BookController.java
1.开头先写两个注解
@Controller:申明控制器,以便mvc可以扫描到。
@RequestMapping("/book"):写个总的映射路径,避免之后每个方法都加上/book
2.借助@Auotowired+@Qualifier来进行属性注入,这样我们就可以获得BookService的实现类对象。从而可以用我们的bookService调用各种业务方法。
3.已知我们要写的url请求时 /book/allBook, 因为开头写了映射/book 此时我们的方法上的@RequestMapping 只需要写/allbook即可,这样我们收到url请求http://localhost:8080/book/allBook,后就会进入到我们BookController中的list方法中,在list方法中我们主要做三件事:(这里注意要把Model交给我们的list方法,不然没办法添加属性值就没法在jsp页面中引用)
- 调用我们的bookService对象中的queryAllBook方法进行查询并将返回的Books对象列表存在list中
- 添加到我们model中,设置属性名为list
- 返回字符串allBook,因为我们之前在springmvc.xml中配置了视图解析器,dispatcherServlet会自动访问WEB-INF/jsp下的allBook.jsp,然后将访问的视图返回浏览器给用户看
package com.kouti.controller;
import com.kouti.pojo.Books;
import com.kouti.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.ArrayList;
import java.util.List;
@Controller
@RequestMapping("/book")
public class BookController {
//利用controller调用service层
//Auotowired+Qualifier 根据bean名称进行检索,避免了同一service下有多个实现类的情况
@Autowired
@Qualifier("bookServiceImpl")
private BookService bookService;
//查询全部书籍,并且返回到一个书籍的展示页面
@RequestMapping("/allBook")
public String list(Model model){
List<Books> list=bookService.queryAllBook();
model.addAttribute("list",list);
return "allBook";
}
}
书籍列表页面 allbook.jsp
这里我们直接在head中引入bootstrap的风格,装饰一下。不然总有一种我做出来的东西怎么会那么丑的心理落差感。ps:偷bootstrap满足自我虚荣心的后端小白(我?)是屑。
div标签是用来划分HTML结构的,用来划分一个区域。我们主要可以分为两个大板块:对应container中的两个最外层的div标签。索性我们先进入http://localhost:8080/book/allBook,点击f12打开我们的控制台来看一下构造(附在代码后)
(因为这个是最终的allbook.jsp,因为删除修改等操作按钮都会设置到书籍列表页面,所以显得有点长。也让人觉得有点突兀,我们可以跟着注解以及控制台模块对应位置来理解一下。)
我们先把书籍列表主页的所有功能按钮和输入框写齐,此时因为其他映射路径还没写好对应的controller方法,我们可以先令action=“”,href=""(为空)
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%--
Created by IntelliJ IDEA.
User: kouti
Date: 2021/5/29
Time: 7:42
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
<%-- BootStrap美化界面--%>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<%-- 清除浮动--%>
<%-- 表格的上半部分,用来放置我们的标题,新增书籍及显示显示所有书籍按钮,在右上角设置一个查询输入框--%>
<div class="row clearfix">
<%-- 设置我们表格的行列大小以及标题 --%>
<div class="col-md-12 column">
<div class="page-header">
<h1>
<small>书籍列表------显示所有书籍</small>
</h1>
</div>
</div>
<%-- 新增书籍按钮--%>
<div class="row">
<div class="col-md-4 column">
<%-- addbook--%>
<%-- 点击此链接跳转到*/book/toAddBook页面,控制权交给controller,返回addBook.jsp--%>
<a class="btn btn-primary" href="${pageContext.request.contextPath}/book/toAddBook">新增书籍</a>
<a class="btn btn-primary" href="${pageContext.request.contextPath}/book/allBook">显示全部书籍</a>
</div>
<div class="col-md-4-column"></div>
<div class="col-md-4-column">
<%-- 查询书籍按钮--%>
<form class="form-inline" action="${pageContext.request.contextPath}/book/queryBook" method="post" style="float: right">
<span style="color: red;font-weight: bold"></span>
<%-- 灰色提示语--%>
<input type="text" name="queryBookName" class="form-control" placeholder="请输入要查询的书籍名称">
<%-- 提交--%>
<input type="submit" value="查询" class="btn btn-primary">
</form>
</div>
</div>
</div>
<div class="row clearfix">
<div class="col-md-12 column">
<table class="table table-hover table-striped">
<thead>
<th>书籍编号</th>
<th>书籍名称</th>
<th>书籍数量</th>
<th>书籍详情</th>
<th>修改操作</th>
</thead>
<%-- 书籍是从数据库中查询出来的,要从list中遍历出来--%>
<tbody>
<%-- 使用$符号直接从model中取出我们存入的list--%>
<c:forEach var="book" items="${list}">
<tr>
<td>${book.bookID}</td>
<td>${book.bookName}</td>
<td>${book.bookCounts}</td>
<td>${book.detail}</td>
<%-- 提供修改删除操作--%>
<td>
<%-- 跳转的同时,添加bookID参数,同样在控制器的映射方法中接受int参数--%>
<span style="color: red;font-weight: bold">${error}</span>
<a href="${pageContext.request.contextPath}/book/toUpdate?id=${book.bookID}">修改</a>
|
<%-- 这里使用了Restful风格--%>
<a href="${pageContext.request.contextPath}/book/deleteBook/${book.bookID}">删除</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
<div>
</div>
</body>
</html>
一进来的见到两个div分别对应上下两个模块

再进行进一步展开观察这两个div内部的div分别又代表着哪块


②添加书籍:
控制器编写 BookController.java
编写添加书籍功能,要添加书籍我们分两步:
先看第一步,对应着第一个方法
1.当我们在书籍主页(/allBook)下点击新增书籍按钮时,我们需要让网页链接跳转到*/book/toAddBook页面,并控制权交给controller,返回addBook.jsp,
因此我们写一个“/toAddBook”映射对应的方法toAddPaper,让toAddPaper方法来返回addBook字符串,跳转到我们WEB-INF/jsp下的addBook.jsp

所以我们很容易就能写成第一个方法:
//跳转到增加书籍页面
@RequestMapping("/toAddBook")
public String toAddPaper(){
return "addBook";
}
2.在介绍第二个方法之前,我们先简单看一下我们的addBook.jsp做了哪些工作
添加书籍页面 addBook.jsp
先看一下,addBook.jsp整体显示的效果。首先起头的是我们的一个标题,之后是三个text类型的文本输入框,因为我们要添加数据就必须要有输入,这里不写id是因为id作为主键设定了自增,可以不用自己设定。我们写其他三个属性就可以。
此外我们还得考虑如何将文本框数据交给后端controller进行接收处理的问题。这里我们采用form标签,以post方式进行提交。这样我们就可以在后端调用这些数据。最后再在下方编写一个submit提交按钮即可。这里注意input中name需要与属性名保持一致,不然会找不到。

<%--
Created by IntelliJ IDEA.
User: kouti
Date: 2021/5/29
Time: 7:42
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>书籍展示</title>
<%-- BootStrap美化界面--%>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<div class="page-header">
<h1>
<small>书籍列表------显示所有书籍</small>
</h1>
</div>
</div>
<%-- 将form表单内容提交到*/addBook页面,设定提交方式为post--%>
<%-- 这里${pageContext.request.contextPath}=/,含义就是http://localhost:8080/--%>
<form action="${pageContext.request.contextPath}/book/addBook" method="post">
<div class="form-group">
<%-- for名称关联input中的id名称--%>
<label>书籍名称:</label>
<%-- 注意提交表单中的name一定要与books实体类中的属性一一对应,不然会报null--%>
<input type="text" name="bookName" class="form-control">
</div>
<div class="form-group">
<label>书籍数量:</label>
<input type="text" name="bookCounts" class="form-control">
</div>
<div class="form-group">
<label>书籍描述:</label>
<input type="text" name="detail" class="form-control">
</div>
<div class="form-group">
<input type="submit" class="form-control" value="添加">
</div>
</form>
</div>
</div>
</body>
</html>
3.这时候我们再回来补齐我们的第二个方法,接受前端传进来的属性值,我们先在控制台输出看一下,确保addBook方法成功接收到了我们的数据。然后我们只需要调用bookService业务实现类对象中的addBook业务即可,该业务会去调用我们的添加书籍的dao操作来完成数据库的数据添加。最后添加完我们需要跳转回主页。这里我们使用重定向返回book/allBook书籍列表主页即可。
//添加书籍请求
@RequestMapping("/addBook")
public String addBook(Books books){
// 后台打印出books方便观察books参数是否顺利导入
System.out.println("addBook:"+books);
// 利用service进行添加业务操作
bookService.addBook(books);
// 添加完之后要进行重定向,重定向到我们的@RequestMapping("/allBook")请求
return "redirect:/book/allBook";
// 也可以直接返回allBook
// return "allBook";
}
测试界面


③修改书籍:
控制器编写 BookController.java
修改书籍我们也分两步走,这里的思路和添加书籍一样。我们先从书籍列表主页出发点击修改按钮到我们的修改页面。
点击修改按钮后,allBook.jsp会通过
/toUpdate?id=${book.bookID}将参数附在url请求后,以便/toUpdate映射下我们的toUpdatePaper方法可以用int id接收到bookID,拿到bookID之后我们调用queryBookById来按Id查询我们的Books返回,同时输出到控制台上看一下是否查询成功。然后为了将查询结果命名为QueryBooks属性,添加到model中,以便updateBook.jsp可以调用这些属性值来进行显示。最后返回字符串,跳转到updateBook.jsp显示出我们的修改页面以及待修改数据现有的属性值。
//1.跳转到修改页面
@RequestMapping("/toUpdate")
public String toUpdatePaper(int id,Model model){
Books books=bookService.queryBookById(id);
System.out.println(books);
//定义QueryBooks属性,将查询到的books添加到model中以便jsp调用
model.addAttribute("QueryBooks",books);
return "updateBook";
}
添加书籍修改页面 updateBook.jsp
先观察一下我们的修改页面。和之前添加页面差不多。逻辑也是类似。因为方法一的缘故,我们通过model中添加好的属性直接提取出来显示在文本框中。然后我么需要将修改后的文本框数据进行提交。交由我们的/ updateBook映射路径对应的 updateBook方法进行处理。

<%--
Created by IntelliJ IDEA.
User: kouti
Date: 2021/5/29
Time: 7:42
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>修改书籍</title>
<%-- BootStrap美化界面--%>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row clearfix">
<div class="col-md-12 column">
<div class="page-header">
<h1>
<small>书籍列表------显示所有书籍</small>
</h1>
</div>
</div>
<%-- 将form表单内容提交到*/addBook页面,设定提交方式为post--%>
<%-- 这里${pageContext.request.contextPath}=/,含义就是http://localhost:8080/--%>
<form action="${pageContext.request.contextPath}/book/updateBook" method="post">
<div class="form-group">
<%-- for名称关联input中的id名称--%>
<label>书籍编号:</label>
<%-- 注意提交表单中的name一定要与books实体类中的属性一一对应,不然会报null--%>
<input type="text" name="bookID" class="form-control" value="${QueryBooks.bookID}" required>
</div>
<div class="form-group">
<%-- for名称关联input中的id名称--%>
<label>书籍名称:</label>
<%-- 注意提交表单中的name一定要与books实体类中的属性一一对应,不然会报null--%>
<input type="text" name="bookName" class="form-control" value="${QueryBooks.bookName}" required>
</div>
<div class="form-group">
<label>书籍数量:</label>
<%-- 拿取model中的QueryBook,修改属性--%>
<input type="text" name="bookCounts" class="form-control" value="${QueryBooks.bookCounts}" required>
</div>
<div class="form-group">
<label>书籍描述:</label>
<input type="text" name="detail" class="form-control" value="${QueryBooks.detail}" required>
</div>
<div class="form-group">
<input type="submit" class="form-control" value="修改">
</div>
</form>
</div>
</div>
</body>
</html>
逻辑和之前添加一样,都是接受前端页面提交的数据后进行数据库更新然后跳转回书籍列表主页面,大家可以自己理解一下。
//2.进行修改书籍操作
@RequestMapping("/updateBook")
public String updateBook(Books books){
//看一眼数据
System.out.println("updateBook:"+books);
bookService.updateBook(books);
//回到结果页面
return "redirect:/book/allBook";
}

④删除书籍:
删除只需要接受前端返回的id(删除时提交数据的方式时RestFul,注意格式书写问题,以及不要忘记写上@PathVariable注解)
控制器编写 BookController.java
//删除书籍
@RequestMapping("/deleteBook/{bookId}") //这里使用@PathVariable 为了演示一遍restful风格
public String deleteBook(@PathVariable("bookId") int id){
bookService.deleteBookById(id);
return "redirect:/book/allBook";
}
可能遇到的问题
ASM错误

查看编译方式是否统一



统一修改成8-lambdas即可
org.xml.sax.SAXParseException
1.报错
org.xml.sax.SAXParseException; lineNumber: 10; columnNumber: 63; SchemaLocation: schemaLocation 值 ‘http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop/spring-aop.xsd’ 必须具有偶数个 URI。
2.原因:
应该是命名空间有问题,可能是上下没有一一对应
3.解决方法:
将spring-mvc.xml开头声明换成下面这段
org.apache.ibatis.exceptions
1、报错
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne()
2、原因
在写 Mapper 接口的时候使用对象作为返回值,而在Mapper.xml 文件中的 SQL 语句查询出来的记录有很多条,所有使用对象没有办法进行接收。
3、解决办法
将 Mapper 接口的返回值改为 List<对象> 进行数据接收。
源码下载
链接:https://pan.baidu.com/s/1XjVIHjsrS6qt11YvHmo6Rw
提取码:zf1x
复制这段内容后打开百度网盘手机App,操作更方便哦
总结
博主只是出于总结自己学习知识的目的完成了这篇博客,文中除了自己的一些思考之外还有数初引用其他博主的观点。相应的原文出处博主已经在文章开头处列出方便大家进行进一步的学习,如有不可转载的文字内容,请联系博主进行删除。有错误的地方,希望能够帮助博主指正,共同学习进步,同时,文中的所有图片出自博主绘制,引用请注明链接。
SSM框架整合图书管理系统教程
本文是SSM框架整合图书管理系统的保姆级教程。介绍了SSM框架中各框架作用、web访问流程,详细阐述项目环境搭建,包括创建数据库、配置Maven项目等。还分别讲解Mybatis、Spring、SpringMVC层配置,以及控制器和视图层开发,最后提及可能遇到的问题及解决办法。
363

被折叠的 条评论
为什么被折叠?



