前面已经发布了 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>
搭建后的最终工程如下图所示,有关具体包下的内容,一目了然,就不详细介绍了:
在 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 的后端只提供接口,因此不需要导入 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