AI 摘要

文章系统梳理单例模式原理、实现与线程安全方案,对比懒汉、饿汉及静态内部类写法,并指出 Spring MVC Controller 默认单例需关注并发安全。随后完整演示 SSM 整合流程:导入依赖、配置 MyBatis、Spring、Spring MVC、web.xml,搭建 DAO、Service、Controller 三层,实现查询用户表并渲染 JSP,验证框架贯通。

单例模式

单例模式概述

单例(Singleton)模式的关键是在系统运行期间,某个类有且只有一个实例,单例模式可以对资源进行重复利用,节约重复创建和销毁的成本,从而降低服务器压力,提高程序效率。

单例模式在应用程序的日志服务、配置文件读取、数据库连接池设计、网站计数器等应用程序中广泛使用。

静态代码块

静态代码块本身不是单例模式的一种实现方式,但它可以在单例模式的实现过程中发挥作用,辅助完成单例模式的相关功能。

静态代码块是用 static 关键字修饰的一段代码块,它在类加载时执行且仅执行一次,常用于初始化类的静态成员变量,或执行一些在类初始化阶段就需要完成的一次性操作,如加载配置文件、建立数据库连接等。


示例:静态代码块

BaseDao 类(cn.duozai.BaseDao):

public class BaseDao {

    private static final Logger logger = Logger.getLogger(BaseDao.class);

    /**
     * 在静态代码块中读取数据库的配置文件
     * 类加载的时候只会执行一次
     */
    static {
        logger.debug("BaseDao => 读取数据库配置文件");
    }

    /**
     * 模拟数据库查询
     *
     * @return void
     */
    public void executeQuery() {
        logger.debug("BaseDao => 查询数据");
    }

}

测试类(test.StaticTest):

public class StaticTest {

    /**
     * BaseDao测试
     *
     * @return void
     */
    @Test
    public void test() {
        // 第一次调用BaseDao查询数据
        BaseDao baseDao1 = new BaseDao();
        baseDao1.executeQuery();

        // 第二次调用BaseDao查询数据
        BaseDao baseDao2 = new BaseDao();
        baseDao2.executeQuery();
    }

}

示例效果:


单例模式实现

单例模式的核心目标是确保一个类在整个应用程序中只有一个实例存在,并提供一个全局访问点来获取这个唯一的实例。

实现单例模式,需要满足:

  • 一个类只有一个实例:不允许外界随意实例化该类,即需要将构造函数私有化。
  • 自行创建实例对象:外界不能随意实例化该类,即需要在该类内部实例化一个实例,且实例化的实例是静态的、私有的。
  • 对外提供实例对象:对外提供实例对象的方法应该是公有的、静态的方法,该方法创建或获取本类中的静态私有对象并返回。

示例:实现单例模式

Singleton 类(cn.duozai.singleton):

/**
 * 确保该类在整个应用程序中只有一个实例存在
 * 并提供一个全局访问点来获取这个唯一的实例
 */
public class Singleton {

    /**
     * 自行创建实例对象
     * 外界不能随意实例化该类,需要在该类内部实例化一个实例
     * 实例化的实例是静态的、私有的
     */
    private static Singleton singleton;

    /**
     * 一个类只有一个实例
     * 不允许外界随意实例化该类,将构造函数私有化
     *
     * @return null
     */
    private Singleton() {

    }

    /**
     * 对外提供实例对象
     * 对外提供实例对象的方法应该是公有的、静态的方法
     * 该方法创建或获取本类中的静态私有对象并返回
     *
     * @return Singleton实例
     */
    public static Singleton getInstance() {
        if (singleton == null) {
            // 创建Singleton实例
            singleton = new Singleton();
        }

        // 返回Singleton实例
        return singleton;
    }

}

测试类(test.SingletonTest):

public class SingletonTest {

    /**
     * 单例模式测试
     *
     * @return void
     */
    @Test
    public void test() {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();

        // Singleton实例全局唯一
        System.out.println(singleton1);
        System.out.println(singleton2);
        System.out.println(singleton1 == singleton2);
    }

}

示例效果:


单例懒汉模式

懒汉模式即懒加载,即在类加载时不创建其实例,而是被调用的时候再创建实例。

懒汉模式是非线程安全的,在并发环境下可能存在严重问题,假设线程 A 和线程 B 几乎同时进入 getInstance 方法,线程 A 和线程 B 都会同时创建实例对象,这就导致了创建出了多个实例对象,违背了单例模式 一个类只有一个实例 的设计初衷。

解决懒汉模式的线程安全问题,可以使用 synchronized 关键字将 getInstance 方法定义为同步方法。


示例:解决单例懒汉模式存在的问题

Singleton 类(cn.duozai.singleton):

public class Singleton {

    // ...

    /**
     * 对外提供实例对象
     * 对外提供实例对象的方法应该是公有的、静态的方法
     * 该方法创建或获取本类中的静态私有对象并返回
     * synchronized:同步方法,保证线程安全
     *
     * @return Singleton实例
     */
    public static synchronized Singleton getInstance() {
        // ...
    }

}

测试类(test.SingletonTest):

public class SingletonTest {

    /**
     * 单例模式测试
     *
     * @return void
     */
    @Test
    public void test() {
        // 多线程创建Singleton实例
        for (int i = 0; i < 10; i++) {
            new Thread(
                    () -> {
                        // 获取Singleton实例
                        Singleton singleton = Singleton.getInstance();
                        // 多线程的情况下没有处理线程安全问题,有可能会得到多个Singleton实例
                        System.out.println(singleton);
                    }
            ).start();
        }
    }

}

示例效果:


通过为 getInstance 方法添加同步关键字 synchronized 的方式可以有效避免线程安全问题,但是这种处理方式效率不高。

单例饿汉模式

单例模式下解决线程安全问题,可以使用饿汉模式。

饿汉模式即在类加载的时候就完成了实例的创建工作,不存在线程安全问题。


示例:单例饿汉模式

Singleton 类(cn.duozai.singleton):

public class Singleton {

    /**
     * 自行创建实例对象
     * 外界不能随意实例化该类,需要在该类内部实例化一个实例
     * 实例化的实例是静态的、私有的
     */
    private static Singleton singleton;

    /**
     * 静态代码块初始化实例对象
     * 在类加载的时候初始化Singleton实例
     */
    static {
        singleton = new Singleton();
    }

    /**
     * 一个类只有一个实例
     * 不允许外界随意实例化该类,将构造函数私有化
     *
     * @return null
     */
    private Singleton() {

    }

    /**
     * 对外提供实例对象
     * 对外提供实例对象的方法应该是公有的、静态的方法
     * 该方法创建或获取本类中的静态私有对象并返回
     *
     * @return Singleton实例
     */
    public static Singleton getInstance() {
        // 直接返回Singleton实例
        return singleton;
    }

}

单例懒饿汉对比

单例模式的两种实现方式:

  • 懒汉模式:在类加载时暂不创建实例,在调用 getInstance 方法时再创建实例,因此类的加载速度快,运行时获取实例对象的速度相对较慢,具有延迟加载的特性,但是存在线程安全问题。
  • 饿汉模式:在类加载时就创建了实例,因此类加载速度慢,运行时获取实例对象的速度快。

单例静态内部类

单例模式可以结合静态内部类,在实现饿汉模式解决线程安全问题的同时,又能具备懒汉模式的延迟加载的特性,既保证了线程安全,又避免了性能影响。

静态内部类是在首次使用该静态内部类的时候才会被加载,其遵循 Java 类加载机制中懒加载的特性。


示例:单例静态内部类

Singleton 类(cn.duozai.singleton):

public class Singleton {

    /**
     * 自行创建实例对象
     * 外界不能随意实例化该类,需要在该类内部实例化一个实例
     * 实例化的实例是静态的、私有的
     * 类初始化的时候不创建Singleton实例
     */
    private static Singleton singleton;

    /**
     * 一个类只有一个实例
     * 不允许外界随意实例化该类,将构造函数私有化
     *
     * @return null
     */
    private Singleton() {

    }

    /**
     * 静态内部类
     * 在首次使用该静态内部类的时候才会被加载
     */
    public static class SingletonHelper {
        // 定义为静态常量
        private static final Singleton INSTANCE = new Singleton();
    }

    /**
     * 对外提供实例对象
     * 对外提供实例对象的方法应该是公有的、静态的方法
     * 该方法创建或获取本类中的静态私有对象并返回
     *
     * @return Singleton实例
     */
    public static Singleton getInstance() {
        // 调用静态内部类,获取Singleton实例
        return SingletonHelper.INSTANCE;
    }

}

Spring MVC 中的单例

Spring MVC 中定义的 Controller 实际上也是 Spring Bean,默认情况下 Spring Bean 的作用域是单例模式。

Spring MVC 将 Controller 设计为单例模式,这样的设计就不需要每次访问都创建控制器实例,速度和性能可以更加优秀。

在单例模式的 Controller 中声明成员变量,需要注意线程安全问题,单例模式的 Controller 中的成员变量是公共的,会被所有的请求共享。

SSM 框架整合

SSM 概述

SSM 一般是指将 Spring、SpringMVC、MyBatis 这三个主流的 Java 开发框架整合在一起,用于构建高效、灵活且易于维护的企业级 Java Web 应用程序。

SSM 整合中各个框架的角色:

  • Spring:在整个应用中负责对象的创建、配置以及管理它们的生命周期等工作,为其他框架的集成提供基础支撑。
  • Spring MVC:主要处理用户与服务器之间的交互,接收来自客户端浏览器的 HTTP 请求,根据配置的映射规则找到对应的 Controller 层方法进行处理,然后返回响应结果给客户端。
  • MyBatis:负责与数据库进行交互,在应用程序中执行对数据库的各种操作。

SSM 整合的优势:

  • 提升开发效率:整合后可利用各框架成熟功能,如 Spring 依赖注入、SpringMVC 便捷请求处理、MyBatis 高效数据库访问,避免重复造轮子,开发者能专注业务逻辑,缩短开发周期。
  • 便于维护与扩展:基于清晰的分层架构,各层职责明确,修改代码时定位清晰,影响范围小,且方便替换组件或添加新功能,可灵活应对技术升级与业务变化需求。
  • 优化团队协作:不同开发人员可依据专长分别负责 SpringMVC、Spring、MyBatis 相关部分,并行开展工作,降低学习成本,提高协作效率。
  • 增强系统性能:各框架自带的性能优化机制能协同发挥作用,保障系统高效运行,提升整体执行效率与数据准确性。

SSM 整合

SSM 的整合步骤:

  • 导入 JAR 依赖包:Spring 依赖、MyBatis 依赖、Spring 与 MyBatis 整合依赖、Spring MVC 依赖、JSP 依赖。
  • 编写 MyBatis 核心配置文件:配置运行时行为(日志实现、自动映射级别等)、配置 SQL 映射文件。
  • 编写 Spring 核心配置文件:配置数据源、配置 SqlSessionFactoryBean 工厂、配置 MapperScannerConfigurer 注入映射器、配置事务管理器、开启 Ioc、AOP、事务注解。
  • 编写 Spring MVC 核心配置文件:开启 MVC 注解、配置视图解析器。
  • 编写 Web 应用配置文件:配置 Spring MVC 前端控制器、配置 Spring 监听器。
  • 编写三层架构:DAO、Service、Controller。

示例:SSM 整合

SQL 脚本:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_address
-- ----------------------------
DROP TABLE IF EXISTS `t_address`;
CREATE TABLE `t_address` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `contact` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人姓名',
  `addressDesc` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '收货地址明细',
  `postCode` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮编',
  `tel` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人电话',
  `userId` bigint DEFAULT NULL COMMENT '用户id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='地址表';

-- ----------------------------
-- Records of t_address
-- ----------------------------
BEGIN;
INSERT INTO `t_address` (`id`, `contact`, `addressDesc`, `postCode`, `tel`, `userId`) VALUES (1, '张三', '福建省泉州市安溪县宝龙城市广场', '362400', '13275000001', 1);
INSERT INTO `t_address` (`id`, `contact`, `addressDesc`, `postCode`, `tel`, `userId`) VALUES (2, '张三', '福建省厦门市集美区软件园3期', '361000', '13275000002', 1);
COMMIT;

-- ----------------------------
-- Table structure for t_sys_role
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_role`;
CREATE TABLE `t_sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `code` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色编码',
  `roleName` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='角色表';

-- ----------------------------
-- Records of t_sys_role
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_role` (`id`, `code`, `roleName`) VALUES (1, 'SYS_ADMIN', '管理员');
INSERT INTO `t_sys_role` (`id`, `code`, `roleName`) VALUES (2, 'SYS_MANAGER', '店长');
INSERT INTO `t_sys_role` (`id`, `code`, `roleName`) VALUES (3, 'SYS_EMPLOYEE', '店员');
COMMIT;

-- ----------------------------
-- Table structure for t_sys_user
-- ----------------------------
DROP TABLE IF EXISTS `t_sys_user`;
CREATE TABLE `t_sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `account` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '账号',
  `realName` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '真实姓名',
  `password` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '密码',
  `sex` int DEFAULT NULL COMMENT '性别,1女/2男',
  `birthday` date DEFAULT NULL COMMENT '出生日期',
  `phone` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '手机号码',
  `roleId` bigint DEFAULT NULL COMMENT '用户角色id',
  `certFile` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '证件照片',
  `avatarFile` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '工作证照片',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';

-- ----------------------------
-- Records of t_sys_user
-- ----------------------------
BEGIN;
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (1, 'admin', 'admin', '0000000', 1, '1980-11-01', '13888888888', 1, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (2, 'limingxing', '李明星', '0000000', 2, '1983-12-10', '13188888888', 2, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (3, 'wanglulu', '王璐璐', '0000000', 2, '1984-06-05', '15766666666', 2, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (4, 'hauzhiqing', '华志强', '0000000', 1, '1983-06-15', '15866666666', 3, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (5, 'huangweihua', '黄卫华', '0000000', 2, '1982-12-31', '13800099999', 3, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (6, 'zhaogang', '赵刚', '0000000', 2, '1980-01-01', '15266655555', 2, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (7, 'liuyang', '刘阳', '0000000', 2, '1978-03-12', '17099999999', 2, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (8, 'lijiangtao', '李江涛', '0000000', 1, '1983-12-10', '13277777777', 3, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (9, 'liuzhong', '刘忠', '0000000', 2, '1981-11-04', '15388888888', 3, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (10, 'zhangfeng', '张峰', '0000000', 2, '1980-01-01', '19288899999', 3, NULL, NULL);
INSERT INTO `t_sys_user` (`id`, `account`, `realName`, `password`, `sex`, `birthday`, `phone`, `roleId`, `certFile`, `avatarFile`) VALUES (11, 'songke', '宋科', '0000000', 1, '1983-10-10', '18900000000', 2, NULL, NULL);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

添加 Web 框架支持、导入相关依赖 JAR 文件,并添加为库。

示例效果:

Log4J 配置文件(classpath:log4j.properties):

log4j.rootLogger=debug,CONSOLE

##################
# Console Appender
##################
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=debug
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern= - (%r ms) - %d{yyyy-M-d HH:mm:ss}%x[%5p](%F:%L) %m%n

MyBatis 核心配置文件(classpath: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>
    <!-- 配置MyBatis框架运行时行为 -->
    <settings>
        <!-- 配置日志实现 -->
        <setting name="logImpl" value="LOG4J"/>
        <!-- 配置自动映射级别 -->
        <setting name="autoMappingBehavior" value="FULL"/>
    </settings>

    <!-- 配置SQL映射文件 -->
    <mappers>
        <package name="cn.duozai.dao"/>
    </mappers>
</configuration>

数据库配置文件(classpath:database.properties):

# 数据库驱动
db_driver=com.mysql.cj.jdbc.Driver
# 数据库地址
db_url=jdbc:mysql://localhost:3306/cvs_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
# 数据库用户名
db_username=root
# 数据库密码
db_password=123456..
# 连接池最小连接数
dbcp_minIdle=45
# 连接池最大连接数
dbcp_maxActive=100
# 连接池最大空闲连接数
dbcp_maxIdle=50
# 连接池初始化连接数
dbcp_initialSize=5
# 连接池最大等待时间
dbcp_maxWait=100
# 连接池是否开启无用连接回收
dbcp_removeAbandoned=true
# 连接池无用连接回收时间
dbcp_removeAbandonedTimeout=180

Spring 核心配置文件(classpath:applicationContext.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"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx
       http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!-- 开启Spring IoC注解 -->
    <context:component-scan base-package="cn.duozai"/>
    <!-- 开启Spring AOP注解 -->
    <aop:aspectj-autoproxy/>

    <!-- 加载properties配置文件 -->
    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
        <property name="location" value="classpath:database.properties"/>
    </bean>

    <!-- 创建dbcp数据源 -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="${db_driver}"/>
        <property name="url" value="${db_url}"/>
        <property name="username" value="${db_username}"/>
        <property name="password" value="${db_password}"/>
        <property name="initialSize" value="${dbcp_initialSize}"/>
        <property name="maxActive" value="${dbcp_maxActive}"/>
        <property name="maxIdle" value="${dbcp_maxIdle}"/>
        <property name="minIdle" value="${dbcp_minIdle}"/>
        <property name="maxWait" value="${dbcp_maxWait}"/>
        <property name="removeAbandonedTimeout" value="${dbcp_removeAbandonedTimeout}"/>
        <property name="removeAbandoned" value="${dbcp_removeAbandoned}"/>
        <property name="testWhileIdle" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="validationQuery" value="select 1"/>
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <property name="numTestsPerEvictionRun" value="${dbcp_maxActive}"/>
    </bean>

    <!-- 创建SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
    </bean>

    <!-- 创建MapperScannerConfigurer扫描DAO层生成映射器 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="cn.duozai.dao"/>
    </bean>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 开启Spring声明式事务注解 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

Spring MVC 核心配置文件(classpath:springmvc-servlet.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"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 开启Spring MVC注解 -->
    <mvc:annotation-driven/>
    <!-- 开启Spring IoC注解 -->
    <context:component-scan base-package="cn.duozai"/>

    <!-- 配置视图解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

Web 应用配置文件(/web/WEB-INF/web.xml):

<?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>dispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- 配置Spring IoC容器监听器,Tomcat启动时初始化IoC容器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>

    <!-- 配置Spring MVC乱码过滤器 -->
    <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>
</web-app>

用户表数据访问接口(cn.duozai.dao.SysUserMapper):

public interface SysUserMapper {

    /**
     * 查询用户表记录数
     *
     * @return 用户表记录总数
     */
    int count();

}

用户表数据访问接口 SQL 映射文件(cn/duozai/dao/SysUserMapper.xml):

<?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="cn.duozai.dao.SysUserMapper">
    <select id="count" resultType="int">
        SELECT COUNT(1) FROM t_sys_user
    </select>
</mapper>

用户表业务逻辑接口(cn.duozai.dao.SysUserService):

public interface SysUserService {

    /**
     * 查询用户表记录数
     *
     * @return 用户表记录总数
     */
    int count();

}

用户表业务逻辑接口实现类(cn.duozai.dao.SysUserServiceImpl):

@Service    // 标记Service实现类为Bean对象,交给Spring管理
public class SysUserServiceImpl implements SysUserService {

    @Resource   // Service层调用Dao层,注入Dao层对象
    SysUserMapper sysUserMapper;

    /**
     * 查询用户表记录数
     *
     * @return 用户表记录总数
     */
    @Override
    public int count() {
        return sysUserMapper.count();
    }

}

首页控制器(cn.duozai.controller.IndexController):

@Controller // 标记控制器类为Bean对象,交给Spring管理
public class IndexController {

    @Resource   // 控制器调用Service层,注入Service层对象
    SysUserService sysUserService;

    /**
     * 处理/index请求
     *
     * @param model 模型对象
     * @return 视图页面文件名称
     */
    @GetMapping("/index")
    public String index(Model model) {
        // 调用Service层查询数据
        int count = sysUserService.count();
        // 将查询结果添加到模型对象中
        model.addAttribute("count", count);
        // 返回视图页面名称
        return "index";
    }

}

首页视图页面(/web/WEB-INF/jsp/index.jsp):

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>用户表记录数:${count}</h1>
</body>
</html>

示例效果: