• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

SSM(Spring SpringMvc Mybatis)纯注释集成示例


Recommended Posts

Spring系列,SpringMvc系列,Mybatis系列的博客已经发表的比较早了。是时候把它们整合起来,形成一个完整的技术,可以用在实际开发中。SSM是一个优秀的集成开发框架,轻松解决实际开发过程中遇到的各种问题,提高开发效率,降低开发成本。SSM框架的理论知识这里就不详细介绍了。我相信大家最关心的是如何通过代码来构建和实现,这才是最重要的。

这篇博客通过一个非常简单的需求(用户必须登录才能查询员工信息)演示了SSM的代码构建和实现,尽可能多地使用了以前博客中发表的技术。如果你不能理解相关的技术点,请回头看看我之前的博客。由于我个人喜欢纯注释的方式,这个博客的Demo就是纯注释搭建的。当然,在这个博客的最后,会提供演示源代码的下载。

一、搭建工程

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

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

属国

!-

导入Spring和SpringMvc的jar包

导入与jackson相关的jar包

-

属国

groupIdorg.springframework/groupId

artifactId spring-context/artifactId

版本5 . 3 . 18/版本

/依赖关系

属国

groupIdorg.springframework/groupId

artifactId spring-web MVC/artifactId

版本5 . 3 . 18/版本

/依赖关系

属国

groupId com . faster XML . Jackson . core/groupId

artifactId Jackson-databind/artifactId

版本2 . 13 . 1/版本

/依赖关系

!-

导入用于操作数据库的相关jar包

导入查询数据分页助手的jar包

-

属国

groupIdorg.springframework/groupId

artifactId spring-JDBC/artifactId

版本5 . 3 . 17/版本

/依赖关系

属国

groupIdcom.alibaba/groupId

artifactIddruid/artifactId

版本1 . 2 . 8/版本

/依赖关系

属国

groupIdmysql/groupId

artifactId MySQL-连接器-java/artifactId

版本8 . 0 . 28/版本

/依赖关系

属国

groupIdorg.mybatis/groupId

artifactIdmybatis/artifactId

版本3 . 5 . 9/版本

/依赖关系

属国

groupIdorg.mybatis/groupId

gt; <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



Link to comment
Share on other sites