SSM(Spring+SpringMvc+Mybatis)纯注解整合示例

前面已经发布了 Spring 系列、SpringMvc系列、Mybatis系列的博客,是时候将它们整合到一起,形成一个完整的可以在实际开发中使用的技术了。SSM 是一款非常优秀的整合开发框架,轻松解决了我们在实际开发过程中所遇到的各种问题,提高了开发效率,降低了开发成本。有关 SSM 框架的理论知识,这里就不详细介绍了,相信大家最关心的就是如何通过代码的方式进行搭建和实现,这个才是最重要的。

本篇博客通过一个非常简单的需求(用户必须登录后,才能查询员工信息),尽可能多的使用前面博客所发布的各种技术,来演示 SSM 的代码搭建和实现。如果相关的技术点看不懂的话,请回看我之前发布的博客。由于我个人非常喜欢纯注解的搭建方式,因此本篇博客的 Demo 采用纯注解方式进行搭建。当然在本篇博客的最后面,会提供 Demo 源代码的下载。


一、搭建工程

新建一个 maven 项目,导入相关 jar 包,我所导入的 jar 包都是最新的,内容如下:

有关具体的 jar 包地址,可以在 https://mvnrepository.com 上进行查询。

<dependencies>
    <!--
    导入 Spring 和 SpringMvc 的 jar 包
    导入 jackson 相关的 jar 包
    -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.18</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.18</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.1</version>
    </dependency>

    <!--
    导入操作数据库所使用相关的 jar 包
    导入查询数据分页助手的 jar 包
    -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.3.17</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.8</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.28</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.9</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.7</version>
    </dependency>
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.3.0</version>
    </dependency>

    <!--Apache 提供的实用的公共类工具 jar 包-->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.9</version>
    </dependency>

    <!--导入 servlet 相关的 jar 包-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>

    <!--操作 Redis 的相关 jar 包-->
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-redis</artifactId>
        <version>2.0.6.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>

    <!--
    日志相关 jar 包,主要是上面的 Redis 相关的 jar 包,在运行时需要日志的 jar 包。
    日志的 jar 包也可以不导入,只不过运行过程中控制台总是有红色提示,看着心烦。
    -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

搭建后的最终工程如下图所示,有关具体包下的内容,一目了然,就不详细介绍了:

image


二、SSM 注解配置细节

在 resources 目录中,只有一些 properties 配置文件,配置的是数据库连接字符串,redis连接字符串,以及日志相关。

jdbc.properties 配置的是 mysql 数据库连接相关的信息,其中使用了 druid 数据库连接池:

mysql.driver=com.mysql.cj.jdbc.Driver
mysql.url=jdbc:mysql://localhost:3306/testdb?useSSL=false
mysql.username=root
mysql.password=123456

# 初始化连接的数量
druid.initialSize=3
# 最大连接的数量
druid.maxActive=20
# 获取连接的最大等待时间(毫秒)
druid.maxWait=3000

redis.properties 配置的是连接 redis 连接相关的信息,其中也使用了 redis 的连接池:

redis.host=localhost
redis.port=6379

# 如果你的 redis 设置了密码的话,可以使用密码配置
# redis.password=123456

redis.maxActive=10
redis.maxIdle=5
redis.minIdle=1
redis.maxWait=3000

log4j.properties 配置的是日志记录相关的信息,本 demo 中主要是因为 RedisTemplate 需要使用到 log4j ,如果我们不导入有关 log4j 的 jar 包和提供 log4j 的配置文件的话,也不会影响 SSM 的运行,但是控制台上总是有红色的缺包提示,看着让人心里很不爽,所以还是导入了吧。

log4j.rootLogger=WARN, stdout

# 如果你既要控制台打印日志,也要文件记录日志的话,可以使用下面这行配置
# log4j.rootLogger=WARN, stdout, logfile

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n

SSM 框架中 Spring 是底层基础核心,用来整合 Mybatis 和 SpringMvc 以及其它相关技术。有关 Spring 整合 Mybatis 的技术,前面的博客已经详细介绍过了。另外本博客 Demo 还需要使用 Redis ,用来保存已经登录的用户名,使用的 RedisTemplate ,前面的博客也已经介绍过了。因此这里仅仅列出具体代码细节。

JdbcConfig 的内容如下:

package com.jobs.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

//加载 jdbc.properties 文件内容
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {

    //获取数据库连接字符串内容
    @Value("${mysql.driver}")
    private String driver;
    @Value("${mysql.url}")
    private String url;
    @Value("${mysql.username}")
    private String userName;
    @Value("${mysql.password}")
    private String password;

    //获取 druid 数据库连接池配置内容
    @Value("${druid.initialSize}")
    private Integer initialSize;
    @Value("${druid.maxActive}")
    private Integer maxActive;
    @Value("${druid.maxWait}")
    private Long maxWait;

    //这里采用 @Bean 注解,表明该方法返回连接数据库的数据源对象
    //由于我们只有这一个数据源,因此不需要使用 BeanId 进行标识
    @Bean
    public DataSource getDataSource() {

        //采用阿里巴巴的 druid 数据库连接池的数据源
        DruidDataSource ds = new DruidDataSource();

        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);

        ds.setInitialSize(initialSize);
        ds.setMaxActive(maxActive);
        ds.setMaxWait(maxWait);

        return ds;
    }

    //让 Spring 装载 jdbc 的事务管理器
    //注意:Spring 框架内,事务的 bean 的名称默认取 transactionManager
    //因为这里使用 getTransactionManager 作为获取 bean 的方法名,所以系统会自动取 get 后的内容作为 bean 名称
    //如果你取的名字不是 getTransactionManager 的话,那么就必须使用 @Bean("transactionManager") 注解
    @Bean
    public PlatformTransactionManager getTransactionManager(@Autowired DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}

MyBatisConfig 的具体内容:

package com.jobs.config;

import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.logging.stdout.StdOutImpl;
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;
import java.util.Properties;

public class MyBatisConfig {

    //这里的 Bean 由 Spring 根据类型自动调用,因此不需要指定 BeanId
    //使用 @Autowired 注解,Spring 自动根据类型将上面的 druid 的数据源赋值到这里
    @Bean
    public SqlSessionFactoryBean getSqlSessionFactoryBean(
            @Autowired DataSource dataSource,
            @Autowired PageInterceptor pageInterceptor){

        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        //这里配置,将 com.jobs.domain 下的所有 JavaBean 实体类的名字作为别名
        //这样 MyBatis 中可以直接使用类名,而不需要使用完全限定名
        ssfb.setTypeAliasesPackage("com.jobs.domain");
        ssfb.setDataSource(dataSource);

        //这里配置,让 MyBatis 在运行时,控制台打印 sql 语句,方便排查问题
        Configuration mybatisConfig = new Configuration();
        mybatisConfig.setLogImpl(StdOutImpl.class);
        ssfb.setConfiguration(mybatisConfig);

        //这里配置分页助手拦截器插件
        ssfb.setPlugins(pageInterceptor);

        return ssfb;
    }

    //配置 MyBatis 使用 com.jobs.dao 下所有的接口,生成访问数据库的代理类
    @Bean
    public MapperScannerConfigurer getMapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("com.jobs.dao");
        return msc;
    }

    //这里配置分页助手拦截器插件,详情请查看官网
    //分页助手的官网地址为:https://github.com/pagehelper/Mybatis-PageHelper
    @Bean
    public PageInterceptor getPageInterceptor(){

        PageInterceptor pi = new PageInterceptor();

        Properties properties = new Properties();
        //设置分页助手插件使用的是 mysql 数据库
        properties.setProperty("helperDialect","mysql");
        //reasonable 分页合理化参数,默认值为false。
        //当该参数设置为 true 时,
        //pageNum<=0 时会查询第一页,pageNum>总页数时,会查询最后一页。
        properties.setProperty("reasonable","true");

        pi.setProperties(properties);
        return pi;
    }
}

RedisConfig 的具体内容:

package com.jobs.config;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@PropertySource("classpath:redis.properties")
public class RedisConfig {

    @Value("${redis.host}")
    private String host;
    @Value("${redis.port}")
    private Integer port;

    //@Value("${redis.password}")
    //private String password;

    @Value("${redis.maxActive}")
    private Integer maxActive;
    @Value("${redis.minIdle}")
    private Integer minIdle;
    @Value("${redis.maxIdle}")
    private Integer maxIdle;
    @Value("${redis.maxWait}")
    private Integer maxWait;

    //获取RedisTemplate
    @Bean
    public RedisTemplate getRedisTemplate(
            @Autowired RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        //设置 Redis 生成的key的序列化器,这个很重要
        //RedisTemplate 默认使用 jdk 序列化器,会出现 Redis 的 key 保存成乱码的情况
        //一般情况下 Redis 的 key 都使用字符串,
        //为了保障在任何情况下使用正常,最好使用 StringRedisSerializer 对 key 进行序列化
        RedisSerializer stringSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        return redisTemplate;
    }

    //获取 Redis 连接工厂
    @Bean
    public RedisConnectionFactory getRedisConnectionFactory(
            @Autowired RedisStandaloneConfiguration redisStandaloneConfiguration,
            @Autowired GenericObjectPoolConfig genericObjectPoolConfig) {
        JedisClientConfiguration.JedisPoolingClientConfigurationBuilder builder
                = (JedisClientConfiguration.JedisPoolingClientConfigurationBuilder)
                JedisClientConfiguration.builder();
        builder.poolConfig(genericObjectPoolConfig);
        JedisConnectionFactory jedisConnectionFactory =
                new JedisConnectionFactory(redisStandaloneConfiguration, builder.build());
        return jedisConnectionFactory;
    }

    //获取 Spring 提供的 Redis 连接池信息
    @Bean
    public GenericObjectPoolConfig getGenericObjectPoolConfig() {
        GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxTotal(maxActive);
        genericObjectPoolConfig.setMinIdle(minIdle);
        genericObjectPoolConfig.setMaxIdle(maxIdle);
        genericObjectPoolConfig.setMaxWaitMillis(maxWait);
        return genericObjectPoolConfig;
    }

    //获取 Redis 配置对象
    @Bean
    public RedisStandaloneConfiguration getRedisStandaloneConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration =
                new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(host);
        redisStandaloneConfiguration.setPort(port);
        //redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
        return redisStandaloneConfiguration;
    }
}

最后 SpringConfig 对它们进行导入,就算是整合了,很简单吧。

需要注意的是:为了防止 Spring 和 SpringMvc 重复进行包扫描,因此我们使用 Spring 扫描除 @Controller 之外的所有注解,让 SpringMvc 仅仅扫描 @Controller 注解。SpringConfig 内容如下:

package com.jobs.config;

import org.springframework.context.annotation.*;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
//让 Spring 扫描除了 controller 之外的所有包
@ComponentScan(value = "com.jobs",
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION, classes = {Controller.class}))
//启用数据库事务
@EnableTransactionManagement
//导入其它配置文件
@Import({JdbcConfig.class, MyBatisConfig.class, RedisConfig.class})
public class SpringConfig {

}

SpringMvcConfig 内容如下,需要注解的是:我们需要将拦截器专门独立出一个方法,加上 @Bean 注解,让 Spring 容器装载它。这样才能保障在拦截器中使用 @Autowired 注解注入其它的 Bean 对象,如 Service 和 Dao 的 Bean 对象。

package com.jobs.config;

import com.jobs.interceptor.CheckLoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.*;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

@Configuration
//让 SpringMvc 仅仅扫描加载配置了 @Controller 注解的类
@ComponentScan("com.jobs.controller")
//启用 mvc 功能,配置了该注解之后,SpringMvc 拦截器放行相关资源的设置,才会生效
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {

    //配置 SpringMvc 连接器放行常用资源的格式(图片,js,css)
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    //配置响应数据格式所对应的数据处理转换器
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

        //如果响应的是 application/json ,则使用 jackson 转换器进行自动处理
        MappingJackson2HttpMessageConverter jsonConverter =
                        new MappingJackson2HttpMessageConverter();
        jsonConverter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> typelist1 = new ArrayList<>();
        typelist1.add(MediaType.APPLICATION_JSON);
        jsonConverter.setSupportedMediaTypes(typelist1);
        converters.add(jsonConverter);

        //如果响应的是 text/html 和 text/plain ,则使用字符串文本转换器自动处理
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> typelist2 = new ArrayList<>();
        typelist2.add(MediaType.TEXT_HTML);
        typelist2.add(MediaType.TEXT_PLAIN);
        stringConverter.setSupportedMediaTypes(typelist2);
        converters.add(stringConverter);
    }

    //添加 SpringMvc 启动后默认访问的首页
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login.html");
    }

    //这里需要注意,拦截器加载是在 Spring Context 创建之前完成的,
    //所以在拦截器中使用 @Autowired 注解注入相关的 bean ,将为 null
    //此时必须要创建拦截器的 bean ,让 spring 容器装载拦截器的 bean
    //这样才可以在拦截器中,使用 @Autowired 注解
    @Bean
    public CheckLoginInterceptor getCheckLoginInterceptor() {
        CheckLoginInterceptor interceptor = new CheckLoginInterceptor();
        return interceptor;
    }

    //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //添加拦截器(可以添加多个拦截器,拦截器的执行顺序,就是添加顺序)
        CheckLoginInterceptor interceptor = getCheckLoginInterceptor();
        //设置拦截器拦截的请求路径
        registry.addInterceptor(interceptor).addPathPatterns("/emp/**");
        //设置拦截器排除的拦截路径
        //registry.addInterceptor(interceptor).excludePathPatterns("/");

        /*
        设置拦截器的拦截路径,支持 * 和 ** 通配
        配置值 /**         表示拦截所有映射
        配置值 /*          表示拦截所有 / 开头的映射
        配置值 /test/*     表示拦截所有 /test/ 开头的映射
        配置值 /test/get*  表示拦截所有 /test/ 开头,且具体映射名称以 get 开头的映射
        配置值 /test/*job  表示拦截所有 /test/ 开头,且具体映射名称以 job 结尾的映射
        */
    }
}

最后在 ServletInitConfig 中,实现 Spring 和 SpringMvc 的整合,内容如下:

package com.jobs.config;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

import javax.servlet.*;

public class ServletInitConfig extends AbstractDispatcherServletInitializer {

    //这个是首先执行的,加载 Spring 配置类,创建 Spring 容器
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
        ctx.register(SpringConfig.class);
        return ctx;
    }

    //这个是在 Spring 容器创建好之后,加载 SpringMvc 配置类,创建 SpringMvc 容器
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext cwa = new AnnotationConfigWebApplicationContext();
        cwa.register(SpringMvcConfig.class);
        return cwa;
    }

    //注解配置 SpringMvc 的 DispatcherServlet 拦截地址,拦截所有请求
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    //添加过滤器
    @Override
    protected Filter[] getServletFilters() {
        //采用 utf-8 作为统一请求的编码
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");

        //该过滤器,能够让 web 页面通过 _method 参数将 Post 请求转换为 Put、Delete 等请求
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}

在 createRootApplicationContext 中创建 Spring 的容器,在 createServletApplicationContext 中创建 SpringMvc 的容器。Spring 容器是根容器,需要先创建,SpringMvc 是在 Spring 容器的基础上进行创建,是小容器。


三、数据访问层和业务层

这个就非常简单了,就是 Spring 和 Mybatis 管理的,其中数据库脚本如下:

CREATE DATABASE IF NOT EXISTS `testdb`;
USE `testdb`;

CREATE TABLE IF NOT EXISTS `employee` (
  `e_id` int(11) NOT NULL COMMENT '主键id',
  `e_name` varchar(50) NOT NULL DEFAULT '' COMMENT '姓名',
  `e_gender` tinyint(4) NOT NULL DEFAULT '0' COMMENT '性别',
  `e_money` int(11) NOT NULL DEFAULT '0' COMMENT '薪水',
  `e_birthday` date NOT NULL DEFAULT '0000-00-00' COMMENT '出生日期',
  PRIMARY KEY (`e_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `employee` (`e_id`, `e_name`, `e_gender`, `e_money`, `e_birthday`) VALUES
	(1, '任肥肥', 1, 2000, '1984-01-27'),(2, '候胖胖', 1, 2100, '1982-05-15'),
	(3, '任小肥', 0, 1800, '1996-03-08'),(4, '候中胖', 1, 2300, '1992-12-26'),
	(5, '李小吨', 0, 1900, '1996-01-08'),(6, '任少肥', 1, 2200, '1988-03-25'),
	(7, '李吨吨', 0, 2100, '1993-11-15'),(8, '候小胖', 1, 2500, '1983-10-10'),
	(9, '李少吨', 1, 1700, '1998-11-15'),(10, '任中肥', 0, 2400, '1981-12-12'),
	(11, '候大胖', 1, 2150, '1982-06-18'),(12, '李中吨', 0, 2310, '1991-01-12'),
	(13, '任大肥', 1, 2020, '1995-06-23'),(14, '李大吨', 0, 2150, '1982-06-18'),
	(15, '候微胖', 1, 1950, '1998-07-12'),(16, '任巨肥', 1, 2200, '1984-06-20'),
	(17, '任微肥', 0, 1850, '1994-03-21'),(18, '候巨胖', 1, 1900, '1995-06-11'),
	(19, '李微吨', 1, 1750, '1998-02-15'),(20, '候少胖', 0, 2050, '1982-07-16'),
	(21, '李巨吨', 1, 1800, '1986-08-23'),(22, '任超肥', 1, 1960, '1989-05-09'),
	(23, '李超吨', 0, 1995, '1999-09-19'),(24, '候超胖', 1, 2198, '1982-03-18'),
	(25, '任老肥', 1, 2056, '1983-10-21'),(26, '候老胖', 0, 2270, '1986-12-16'),
	(27, '李老吨', 1, 2300, '1983-12-23'),(28, '李中吨', 1, 2068, '1990-02-26');

数据访问层细节为:

package com.jobs.dao;

import com.jobs.dao.sql.EmployeeDaoSQL;
import com.jobs.domain.Employee;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.SelectProvider;

import java.util.List;

public interface EmployeeDao {

    //根据姓名和性别查询员工,按照id升序排列
    //在 select 方法上定义 employee_map
    //建立 Employee 实体类的属性与数据库表 employee 的字段对应关系
    @Results(id = "employee_map", value = {
            @Result(column = "e_id", property = "id"),
            @Result(column = "e_name", property = "name"),
            @Result(column = "e_gender", property = "gender"),
            @Result(column = "e_money", property = "money"),
            @Result(column = "e_birthday", property = "birthday")})
    @SelectProvider(type = EmployeeDaoSQL.class, method = "getEmployeeListSQL")
    List<Employee> GetEmployeeList(@Param("name") String n,@Param("gender") Short g);
}
package com.jobs.dao.sql;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Param;

public class EmployeeDaoSQL {

    //在传递参数时,形参可以随便写,尽量使用 @Parm 注解给形参取一个有意义的别名,
    //比如给 nnn 这个形参,取个别名为 name,给 ggg 这个形参,取个别名为 gender
    //在拼接 SQL 语句时,要使用 @Parm 注解中的参数名称,这样可以防止 SQL 注入攻击
    public String getEmployeeListSQL(@Param("name") String nnn, @Param("gender") Short ggg) {
        StringBuilder sql = new StringBuilder();
        sql.append(" SELECT e_id,e_name,e_money,");
        sql.append(" (case e_gender when 1 then '男' when 0 then '女' ELSE '未知' END) AS e_gender,");
        sql.append(" DATE_FORMAT(e_birthday,'%Y-%m-%d') AS e_birthday FROM employee");

        if (StringUtils.isNotBlank(nnn) || ggg != -1) {
            sql.append(" where 1=1");
            if (StringUtils.isNotBlank(nnn)) {
                //在拼接 SQL 语句时,要使用 @Parm 注解中的参数名称,这样可以防止 SQL 注入攻击
                sql.append(" and (e_name like CONCAT('%',#{name},'%'))");
            }

            if (ggg != -1) {
                sql.append(" and e_gender=#{gender}");
            }
        }

        sql.append(" order by e_id");

        return sql.toString();
    }
}

然后就是业务层的细节:

package com.jobs.service;

import com.github.pagehelper.PageInfo;
import com.jobs.domain.Employee;
import org.springframework.transaction.annotation.Transactional;

//最好在接口上添加事务,而不是在接口的实现类上添加事务
//因为在接口上添加事务的话,后续该接口的其它实现类自动也具有事务

//可以在接口上添加整体事务,比如只读事务。在接口内具体的需要进行写操作的方法上添加写事务
//@Transactional(readOnly = true)
public interface EmployeeService {

    //开启只读事务
    @Transactional(readOnly = true)
    PageInfo<Employee> getEmployeeList
    (Integer pageIndex, Integer pageSize, String name, Short gender);

    //@Transactional(readOnly = false)
    //Integer addEmployee(Employee emp);

    //用户登录
    boolean Login(String name, String pwd);

    //用户退出
    void Logout(String name);

    //判断用户是否已经登录
    boolean CheckLogin(String name);
}
package com.jobs.service.impl;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.jobs.dao.EmployeeDao;
import com.jobs.domain.Employee;
import com.jobs.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class EmployeeImpl implements EmployeeService {

    @Autowired
    private EmployeeDao employeeDao;

    @Autowired
    private RedisTemplate redisTemplate;

    //通过姓名和性别查询员工,传入页码和每页条数,页码从 1 开始
    @Override
    public PageInfo<Employee> getEmployeeList(
        Integer pageIndex, Integer pageSize, String name, Short gender) {

        PageHelper.startPage(pageIndex, pageSize);
        List<Employee> list = employeeDao.GetEmployeeList(name, gender);
        return new PageInfo<>(list);
    }

    //用户登录
    @Override
    public boolean Login(String name, String pwd) {

        //实际业务中,需要从数据库中读取用户名和密码、
        //这里的 demo 就直接懒省事,预置了一些用户,密码都是 123456
        List<String> userlist = List.of("admin", "zhangsan", "lisi");

        if (userlist.contains(name) && "123456".equals(pwd)) {
            //登录成功后,将用户名记录到 Redis 中,并设置过期时间为 60 秒,便于测试
            //因为这个是 demo ,所以把过期时间设置的短一些
            redisTemplate.opsForValue().set(name,
                    "这里的 value 可以设置用户的角色或权限等额外信息", 60, TimeUnit.SECONDS);
            return true;
        } else {
            return false;
        }
    }

    //用户退出
    @Override
    public void Logout(String name) {
        //从 Redis 中删除用户
        redisTemplate.delete(name);
    }

    //判断用户是否已经登录了
    @Override
    public boolean CheckLogin(String name) {
        //如果在 redis 中能够找了 name 的键值对,则表明已经登录了
        Boolean b = redisTemplate.hasKey(name);
        if (b) {
            //再给已经登录的用户,延续 60 秒的时间
            redisTemplate.opsForValue().set(name,
                    "这里的 value 可以设置用户的角色或权限等额外信息", 60, TimeUnit.SECONDS);
            return true;
        } else {
            return false;
        }
    }
}

最后列出它们所使用的 Employee 实体类内容:

package com.jobs.domain;

import java.util.Date;

public class Employee {
    private Integer id;
    private String name;
    private String gender;
    private Integer money;
    private String birthday;

    public Employee() {
    }

    public Employee(Integer id, String name, String gender, Integer money, String birthday) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        this.money = money;
        this.birthday = birthday;
    }

    //此处省略 get 和 set 方法......

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", money=" + money +
                ", birthday='" + birthday + '\'' +
                '}';
    }
}

四、SpringMvc 细节

SpringMvc 的后端只提供接口,因此不需要导入 jsp 相关的 jar 包,EmployeeController 和返回数据的 Result 内容为:

package com.jobs.controller;

import com.github.pagehelper.PageInfo;
import com.jobs.controller.Results.Result;
import com.jobs.domain.Employee;
import com.jobs.service.EmployeeService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/emp")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    //用户登录
    @PostMapping("/login")
    public Result Login(String name, String pwd) {
        if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(pwd)) {
            boolean b = employeeService.Login(name, pwd);
            if (b) {
                return new Result(true, "登录成功");
            } else {
                return new Result(false, "用户名或密码输入不正确");
            }
        } else {
            return new Result(false, "用户名和密码不能为空");
        }
    }

    //用户退出
    @PostMapping("/logout")
    public Result Logout(HttpServletRequest request) {
        //从 cookie 中获取到用户名
        Cookie[] cookies = request.getCookies();
        if (cookies != null && cookies.length > 0) {
            String username = "";
            for (Cookie c : cookies) {
                if (c.getName().equals("username")) {
                    username = c.getValue();
                    break;
                }
            }

            if (StringUtils.isNotBlank(username)) {
                employeeService.Logout(username);
            }
        }

        return new Result(true, "退出成功");
    }

    //通过姓名和性别分页查询员工列表
    @PostMapping("/list/{pageIndex}/{pageSize}")
    public Result getEmployeeList(@PathVariable Integer pageIndex,
                                  @PathVariable Integer pageSize,
                                  String name, Short gender) {
        PageInfo<Employee> emplist =
               employeeService.getEmployeeList(pageIndex, pageSize, name, gender);
        return new Result(true,"查询成功", emplist);
    }
}
package com.jobs.controller.Results;

public class Result {
    boolean flag;
    String msg;
    Object data;

    public Result() {
    }

    public Result(boolean flag, String msg) {
        this.flag = flag;
        this.msg = msg;
    }

    public Result(boolean flag, String msg, Object data) {
        this.flag = flag;
        this.msg = msg;
        this.data = data;
    }

    //此处省略的 get 和 set 方法....

    @Override
    public String toString() {
        return "Result{" +
                "flag=" + flag +
                ", msg='" + msg + '\'' +
                ", data=" + data +
                '}';
    }
}

有些接口必须在用户登录了之后才能方法,因此我们需要使用拦截器进行验证(当开发好拦截器之后,需要在 SpringMvcConfig 中进行了添加拦截器,并配置拦截的地址),对于验证是否登录的拦截器,我们只使用 preHandle 方法即可,因为其在请求到达 controller 方法前先执行,具体内容为:

package com.jobs.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jobs.controller.Results.Result;
import com.jobs.service.EmployeeService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//验证用户是否登录的拦截器
public class CheckLoginInterceptor implements HandlerInterceptor {

    @Autowired
    private EmployeeService employeeService;

    //在请求 controller 之前执行用户是否登录的验证
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        //获取用户请求的 uri 地址
        String uri = request.getRequestURI().toLowerCase();
        if (uri.contains("emp/login") || uri.contains("emp/logout")) {
            //对于登录和退出的请求,不验证,直接放行
            return true;
        } else {
            //从 cookie 中获取到用户名
            Cookie[] cookies = request.getCookies();
            if (cookies != null && cookies.length > 0) {
                String username = "";
                for (Cookie c : cookies) {
                    if (c.getName().equals("username")) {
                        username = c.getValue();
                        break;
                    }
                }

                if (StringUtils.isNotBlank(username)) {
                    //如果 redis 中存在该 username 的键值对,则表明已经登录了
                    if (employeeService.CheckLogin(username)) {
                        return true;
                    }
                }
            }

            //如果
            Result result = new Result(false, "用户没有登录");
            //返回 json数据
            ObjectMapper objectMapper = new ObjectMapper();
            String json = objectMapper.writeValueAsString(result);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(json);

            //此处返回 false 后,将不会再执行 controller 中的方法
            return false;
        }
    }
}

为了统一记录整个项目的异常日志,并且在发生异常时给用户提供友好的信息,我们使用全局捕获和处理类,具体内容如下:

package com.jobs.exception;

import com.jobs.controller.Results.Result;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

@Component
@ControllerAdvice
public class GlobalExceptionHandler {

    //该方法捕获并处理所有的异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result doException(Exception ex) {
        //实际项目中,会将异常信息记录下来,比如存储到数据库或文本文件中
        System.out.println(ex);
        //实际项目中,不会将异常信息返回到前端,而是提示给用户友好的信息
        return new Result(false, "系统出现问题,请联系管理员");
    }
}

五、前端页面验证搭建成果

我简单制作了 3 个页面,login.html 是登录页面,list.html 是查询员工页面,prompt.html 是未登录用户如果在地址栏上直接访问 list.html 页面时,会自动跳转到的提示页面。具体内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>这里是登录页面 login.html</h1>
<fieldset>
    <legend>用户登录</legend>
    用户名:<input type="text" id="name"/><br/>
    密码:<input type="password" id="pwd"/><br/>
    <input type="button" value="登录" id="btnlogin">
</fieldset>
<script src="./js/jquery-3.6.0.min.js"></script>
<script src="./js/jquery.cookie-1.4.1.js"></script>
<script>
    $(function () {
        $('#btnlogin').click(function () {
            let nametext = $('#name').val();
            let pwdtext = $('#pwd').val();
            if ($.trim(nametext) == '' || $.trim(pwdtext) == '') {
                alert('用户名和密码不能为空');
                return false;
            }

            $.ajax({
                type: "post",
                url: "/emp/login",
                data: {name: nametext, pwd: pwdtext},
                dataType: "json",
                success: function (data) {
                    if (data.flag) {
                        //写cookie,有效期为 1 天,然后跳转到 list.html 页面
                        $.cookie("username", nametext, {path: "/", expires: 1})
                        location.href = "list.html";
                    } else {
                        alert(data.msg);
                    }
                }
            });
        });
    })
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>这里是 list.html 页面</h1>
<fieldset>
    <legend>查询员工列表</legend>
    员工姓名:<input type="text" id="name"/><br/>
    员工性别:<select id="gender">
    <option value="-1" selected>不限</option>
    <option value="1">男</option>
    <option value="0">女</option>
</select><br/>
    当前页码:<input type="number" value="1" step="1" id="pageIndex"/><br/>
    每页条数:<input type="number" value="10" step="1" id="pageSize"/><br/>
    <input type="button" value="查询" id="btnSearch"/><br/>
    <textarea rows="30" cols="100" id="txtResult"></textarea><br/>
    如果想退出登录,请点击这里:<input type="button" value="退出登录" id="btnLogout"/>
</fieldset>
<script src="./js/jquery-3.6.0.min.js"></script>
<script>
    $(function () {
        $('#btnSearch').click(function () {
            let name_val = $('#name').val();
            let gender_val = $('#gender').val();
            let pindex_val = $('#pageIndex').val();
            let psize_val = $('#pageSize').val();

            if (psize_val < 0) {
                alert('每页条数必须大于0');
                return false;
            }

            $.ajax({
                type: "post",
                url: "/emp/list/" + pindex_val + "/" + psize_val,
                data: {name: name_val, gender: gender_val},
                dataType: "json",
                xhrFields: {
                    //允许 ajax 请求携带 cookie
                    withCredentials: true
                },
                success: function (data) {
                    if (data.flag) {
                        $('#txtResult').val(JSON.stringify(data.data, null, 2));
                    } else {
                        alert(data.msg);
                        if (data.msg == "用户没有登录") {
                            location.href = "login.html";
                        }
                    }
                }
            });
        });

        $('#btnLogout').click(function () {
            $.ajax({
                type: "post",
                url: "/emp/logout",
                dataType: "json",
                xhrFields: {
                    //允许 ajax 请求携带 cookie
                    withCredentials: true
                },
                success: function (data) {
                    if (data.flag) {
                        alert("退出成功");
                    }
                    location.href = "login.html";
                }
            });
        });

        //页面加载完成后,自动查询一下数据
        $('#btnSearch').trigger("click");
    })
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="refresh" content="5;url=login.html">
    <title>未登录提示页面</title>
</head>
<body>
<h1 >这里是用户未登录提示页面 prompt.html</h1>
<h1 style="color:indigo">5秒钟将自动跳转到登录页面...</h1>
</body>
</html>


本博客 Demo 实现的具体细节为:

用户登录成功后,服务端会将用户名记录到 Redis 中,前端 jquery 会将用户名记录到 Cookie 中。前端后续每次请求服务端的接口时,都会携带 Cookie 提交给服务端的接口,SpringMvc 的拦截器会读取 Cookie 中的用户名,然后在 Redis 中查找是否存在,如果存在则认为已经登录了,如果不存在,则认为未登录。另外 Redis 中保存的用户名,设置了 1 分钟的有效期。如果一分钟内,前端有新的请求的话,拦截器中的代码会从请求时刻开始,为用户名在 Redis 中延长一分钟的有效期。

最后提供本 Demo 的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/Spring_SpringMvc_MyBatis.zip



posted @ 2022-05-03 22:20  乔京飞  阅读(10701)  评论(0编辑  收藏  举报