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>
搭建后的最终工程如下图所示,有关具体包下的内容,一目了然,就不详细介绍了:
二、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