SSM + Shiro 整合 (2)- 实现 Spring 集成 MyBatis

时间:2021-09-07 20:12:28

项目源码:https://github.com/weimingge14/Shiro-project
演示地址:http://liweiblog.duapp.com/Shiro-project/login

本节目标:实现 Spring 集成 MyBatis 的整合,并且完成单元测试。

步骤1:创建数据库表(数据库脚本附在本文后,或者在本项目的 GitHub 仓库中下载)

步骤2:编写 MyBatis 的配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

<!--配置全局属性-->
<settings>
<!--使用 jdbc 的 getGeneratekeys 获取自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!--使用列别名替换别名  默认true
select name as title form table;
-->

<setting name="useColumnLabel" value="true"/>

<!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>

</configuration>

同样地,该配置文件如何编写,文档约束片段也应该到 MyBatis 的官方网站或官方文档上去拷贝。

步骤3:创建 Spring 关于数据访问层的配置文件 spring/spring-dao.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"
>


<!-- 该配置文件是 Dao 层的配置文件,实现了 Spring 与 MyBatis 框架的整合 -->

<!-- 1、将数据库连接参数化,
建议键值带 jdbc. 前缀,否则如果使用 username ,Spring 框架会优先使用系统变量 username
而不会使用我们在配置文件中使用的键 username -->

<context:property-placeholder location="classpath:jdbc.properties"/>

<!-- 配置数据源 -->
<!-- 使用的数据库连接池产品是 Driud -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写, Druid 默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}"/>
<property name="minIdle" value="${jdbc.pool.minIdle}"/>
<property name="maxActive" value="${jdbc.pool.maxActive}"/>

<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="60000"/>

<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="60000"/>

<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="300000"/>

<property name="validationQuery" value="${jdbc.testSql}"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>

<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用)
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" /> -->


<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat"/>
</bean>

<!-- MyBatis 集成 Spring 必须配置的项 1 、配置 SqlSessionFactory 实例 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"/>
<!-- 扫描 sql 配置文件,即 mapper 对应的 xml 文件 -->
<property name="mapperLocations" value="classpath:mappers/*.xml"/>
<!-- 扫描 entity 包,这样在 mapper 中就可以使用简单类名,多个用 ; 隔开 -->
<property name="typeAliasesPackage" value="com/liwei/shiro/model"/>
<!-- 配置 MyBatis 全局配置文件 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
</bean>

<!-- MyBatis 集成 Spring 必须配置的项 2,可以不配置 id -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 给出须要被扫描的 Dao 接口-->
<property name="basePackage" value="com.liwei.shiro.dao"/>
<!-- 注入 SqlSessionFactory -->
<!-- 这是推荐配置的项,不要去配置 sqlSessionFactory ,已经被弃用了-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

</beans>

步骤4:创建 MyBatis 的接口和配置文件(xml文件)

说明:接口代码应该是上面的配置文件中 MapperScannerConfigurer 部分的 basePackage 属性所指明的包下的接口。
接口的实例化由 Spring 帮助我们完成。

public interface UserDao {

Integer add(User user);

Integer update(User user);

Integer delete(Integer id);

User load(Integer id);

List<User> listUser();

User loadByUserName(String username);

/**
* 根据角色 id 查询所有是该角色的用户列表
* @param rid
* @return
*/

List<User> listByRole(Integer rid);

List<Resource> listAllResources(Integer uid);

List<String> listRoleSnByUser(Integer uid);

List<Role> listUserRole(Integer uid);
}

步骤5:创建与上面接口对应的 xml 文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.liwei.shiro.dao.UserDao">

<insert id="add" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES(#{username},#{password},#{nickname},#{status});
</insert>

<!-- 动态更新对象的写法 -->
<update id="update" parameterType="User">
UPDATE t_user
<set>
<if test="username != null">
username = #{username},
</if>
<if test="password != null">
`password` = #{password},
</if>
<if test="nickname != null">
nickname = #{nickname},
</if>
<if test="status != null">
`status` = #{status}
</if>
</set>
WHERE
id = #{id}
</update>

<delete id="delete" parameterType="int">
DELETE FROM t_user
where id = #{id}
</delete>


<select id="load" parameterType="int" resultType="User">
SELECT
id,
username,
`password`,
nickname,
`status`
FROM
t_user
WHERE
id = #{id}
</select>

<select id="listUser" resultType="User">
SELECT
id,
username,
`password`,
nickname,
`status`
FROM
t_user
</select>

<select id="loadByUserName" parameterType="string" resultType="User">
SELECT
id,
username,
`password`,
nickname,
`status`
FROM
t_user
WHERE
username = #{username}
</select>

<select id="listByRole" parameterType="int" resultType="User">
SELECT
tu.id,
tu.username,
tu.`password`,
tu.nickname,
tu.`status`
FROM
t_user tu
LEFT JOIN t_user_role tur ON tu.id = tur.user_id
LEFT JOIN t_role tr ON tur.role_id = tr.id
WHERE tr.id =#{rid}
</select>

<!-- 根据用户 id 查询这个用户拥有的所有资源(这里的资源就代表权限) -->
<select id="listAllResources" resultType="Resource" parameterType="int">
SELECT tr.`id`,tr.`name`,tr.`permission`,tr.`url`
FROM `t_resource` tr
LEFT JOIN `t_role_resource` trr ON tr.`id` = trr.`resource_id`
LEFT JOIN `t_user_role` tur ON trr.`role_id` = tur.role_id
WHERE tur.user_id = #{uid}
</select>

<!-- 根据用户 id 查询用户所具有的角色字符串表示 -->
<select id="listRoleSnByUser" parameterType="int" resultType="string">
SELECT
tr.sn
FROM `t_role` tr
LEFT JOIN `t_user_role` tur ON tr.`id` = tur.role_id
LEFT JOIN `t_user` tu ON tur.`user_id` = tu.id
WHERE tu.`id` = #{uid}
</select>

<!-- 根据用户 id 查询用户所具有的角色对象表示 -->
<select id="listUserRole" parameterType="int" resultType="Role">
SELECT
tr.`id`,
tr.`name`,
tr.`sn`
FROM `t_role` tr
LEFT JOIN `t_user_role` tur ON tr.`id` = tur.role_id
LEFT JOIN `t_user` tu ON tur.`user_id` = tu.id
WHERE tu.`id` = #{uid}
</select>

</mapper>

我们可以发现 Dao 接口的代码和 mapper(xml)文件其实是一致的。
mapper(xml)文件 <mapper namespace="com.liwei.shiro.dao.UserDao">
中 namespace 属性值就是对应的 Dao 接口文件的全类名。

步骤 6:编写 Service 层代码

接口代码

public interface IUserService {

/**
* 添加单个用户
* @param user
*/

Integer add(User user);

/**
* 批量添加用户角色关联表数据
* @param user
* @param rids
*/

void add(User user,List<Integer> rids);

/**
* 根据 user_id 删除用户数据
* @param id
*/

void delete(int id);

/**
* // TODO: 2016/9/18 应该设置为一个事务
* 更新用户数据
* 1、更新用户基本信息
* 2、更新用户所属角色
* (1)先删除所有的角色
* (2)再添加绑定的角色
* @param user
* @param rids
*/

void update(User user,List<Integer> rids);

/**
* 更新单个用户信息
* @param user
* @return
*/

Integer update(User user);

/**
* 根据主键 id 加载用户对象
* @param id
* @return
*/

User load(int id);

/**
* 根据用户名加载用户对象(用于登录使用)
* @param username
* @return
*/

User loadByUsername(String username);

/**
* 登录逻辑
* 1、先根据用户名查询用户对象
* 2、如果有用户对象,则继续匹配密码
* 如果没有用户对象,则抛出异常
* @param username
* @param password
* @return
*/

User login(String username,String password);

/**
* 查询所有的用户对象列表
* @return
*/

List<User> list();

/**
* 根据角色 id 查询是这个角色的所有用户
* @param id
* @return
*/

List<User> listByRole(int id);

/**
* 查询指定用户 id 所拥有的权限
* @param uid
* @return
*/

List<Resource> listAllResource(int uid);

/**
* 查询指定用户所指定的角色字符串列表
* @param uid
* @return
*/

List<String> listRoleSnByUser(int uid);

/**
* 查询指定用户所绑定的角色列表
* @param uid
* @return
*/

List<Role> listUserRole(int uid);

}

实现类代码:

@Service
public class UserService implements IUserService {

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

@Autowired
private UserDao userDao;

@Autowired
private RoleDao roleDao;

/**
* 返回新插入用户数据的主键
* @param user
* @return
*/

@Override
public Integer add(User user) {
// 使用用户名作为盐值,MD5 算法加密
user.setPassword(ShiroKit.md5(user.getPassword(),user.getUsername()));
userDao.add(user);
Integer userId = user.getId();
return userId;
}

/**
* 为单个用户设置多个角色
* @param user
* @param rids
*/

@Override
public void add(User user, List<Integer> rids) {
Integer userId = this.add(user);
roleDao.addUserRoles(userId,rids);
}

/**
* 根据 user_id 删除用户数据
* @param id
*/

@Override
public void delete(int id) {
userDao.delete(id);
}

/**
* // TODO: 2016/9/18 应该设置为一个事务
* 更新用户数据
* 1、更新用户基本信息
* 2、更新用户所属角色
* (1)先删除所有的角色
* (2)再添加绑定的角色
* @param user
* @param rids
*/

@Override
public void update(User user, List<Integer> rids) {
Integer userId = user.getId();
roleDao.deleteUserRoles(userId);
roleDao.addUserRoles(userId,rids);
this.update(user);
}

/**
* 更新单个用户信息
* @param user
* @return
*/

@Override
public Integer update(User user) {
String password = user.getPassword();
if(password!=null){
user.setPassword(ShiroKit.md5(user.getPassword(),user.getUsername()));
}
return userDao.update(user);
}

/**
* 根据主键 id 加载用户对象
* @param id
* @return
*/

@Override
public User load(int id) {
return userDao.load(id);
}

/**
* 根据用户名加载用户对象(用于登录使用)
* @param username
* @return
*/

@Override
public User loadByUsername(String username) {
return userDao.loadByUserName(username);
}

/**
* 登录逻辑
* 1、先根据用户名查询用户对象
* 2、如果有用户对象,则继续匹配密码
* 如果没有用户对象,则抛出异常
* @param username
* @param password
* @return
*/

@Override
public User login(String username, String password) {
User user = userDao.loadByUserName(username);
if(user == null){
// 抛出对象不存在异常
// TODO: 2016/9/18 应该使用 Shiro 框架的登录方式,暂时先这样
logger.debug("用户名不存在");
throw new UnknownAccountException("用户名和密码不匹配");
}else if(false){
// !user.getPassword().equals(password)
logger.debug("密码错误");
// 抛出密码不匹配异常
throw new IncorrectCredentialsException("用户名和密码不匹配");
}else if(user.getStatus() == 0){
throw new LockedAccountException("用户已经被锁定,请联系管理员启动");
}
return user;
}

/**
* 查询所有的用户对象列表
* @return
*/

@Override
public List<User> list() {
return userDao.listUser();
}

/**
* 根据角色 id 查询是这个角色的所有用户
* @param id
* @return
*/

@Override
public List<User> listByRole(int id) {
return userDao.listByRole(id);
}

/**
* 查询指定用户 id 所拥有的权限
* @param uid
* @return
*/

@Override
public List<Resource> listAllResource(int uid) {
return userDao.listAllResources(uid);
}

/**
* 查询指定用户所指定的角色字符串列表
* @param uid
* @return
*/

@Override
public List<String> listRoleSnByUser(int uid) {
return userDao.listRoleSnByUser(uid);
}

/**
* 查询指定用户所绑定的角色列表
* @param uid
* @return
*/

@Override
public List<Role> listUserRole(int uid) {
return userDao.listUserRole(uid);
}

}

说明:在 Service 实现类中可以直接注入 Dao 层。即可以在 private UserDao userDao; 上标注 @Autowire 注解。

步骤7:编写测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:spring/spring-service.xml",
"classpath:spring/spring-dao.xml"})
public class UserServiceTest {

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

@Autowired
private IUserService userService;


@Test
public void add() throws Exception {
User user = new User();
user.setUsername("zhouguang");
user.setPassword("666666");
user.setNickname("周光1");
user.setStatus(1);
userService.add(user);
logger.debug("返回自增长的主键:" + user.getId());
}

}

附:数据库脚本:

drop database ssm_shiro;
# 创建数据库 ssm_shiro
CREATE DATABASE ssm_shiro DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
# 使用数据库 ssm_shiro
USE ssm_shiro;
# 创建数据表 t_user
CREATE TABLE t_user(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '用户 ID',
username VARCHAR(30) NOT NULL comment '用户名',
`password` VARCHAR(32) NOT NULL comment '密码',
nickname VARCHAR(30) NOT NULL comment '昵称',
`status` TINYINT not null comment '状态:1 启用,2 禁用'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户信息表';


# 创建数据用于测试
INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('admin','a66abb5684c45962d887564f08346e8d','超级管理员',1);

INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('dev','c43812121e594f158520698ba706118f','开发工程师',1);

INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('test','47ec2dd791e31e2ef2076caf64ed9b3d','测试工程师',1);

INSERT INTO t_user(username,`password`,nickname,`status`)
VALUES('doc','5afd1e481507a2a181decc3860b32d15','文档工程师',1);


# 创建数据表 t_role
# name 字段用于显示给人看, sn 字段用在代码中做角色匹配
CREATE TABLE t_role(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '角色表 ID',
`name` VARCHAR(20) NOT NULL comment '角色名称',
sn VARCHAR(20) NOT NULL comment '角色字符串'
)engine=innodb auto_increment=1 DEFAULT charset=utf8 comment='角色信息表';


# 创建数据用于测试
INSERT INTO t_role(`name`,`sn`) VALUES('管理员','admin'),('开发工程师','dev'),('测试工程师','test'),('文档工程师','doc');

# 创建数据表 t_user_role
CREATE TABLE t_user_role(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '用户角色关联表 ID',
user_id TINYINT NOT NULL,
role_id TINYINT NOT NULL
)engine=innodb auto_increment=1 charset=utf8 comment='用户角色关联表';


# 创建数据用于测试
INSERT INTO `t_user_role`(`user_id`,`role_id`)
VALUES(1,1),(2,2),(3,3),(4,4);


# 创建资源表 t_resource
# 资源在本项目中的含义就是 "权限"
CREATE TABLE t_resource(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '资源 ID',
`name` VARCHAR(20) NOT NULL comment '资源名称,一般是中文名称(给人看的)',
permission VARCHAR(40) NOT NULL comment '资源权限字符串,一般是 Shiro 默认的通配符表示(给人看的)',
url VARCHAR(40) NOT NULL comment '资源 url 表示,我们设计的系统让 Shiro 通过这个路径字符串去匹配浏览器中显示的路径'
)engine=innodb auto_increment=1 charset=utf8 comment='资源表';


# 创建数据用于测试
INSERT INTO t_resource(`name`,permission,url)
VALUES('系统管理','admin:*','/admin/**'),
('用户管理','user:*','/admin/user/**'),
('用户添加','user:add','/admin/user/add'),
('用户删除','user:delete','/admin/user/delete'),
('用户修改','user:update','/admin/user/update'),
('用户查询','user:list','/admin/user/list'),
('用户资源查询','user:resources:*','/admin/user/resources/*'),
('角色管理','role:*','/admin/role/**'),
('角色添加','role:add','/admin/role/add'),
('角色删除','role:delete','/admin/role/delete'),
('角色修改','role:update','/admin/role/update'),
('角色查询','role:list','/admin/role/list'),
('角色资源查询','role:resources:*','/admin/role/resources/*'),
('资源管理','resource:*','/admin/resource/**'),
('资源增加','resource:add','/admin/resource/add'),
('资源删除','resource:delete','/admin/resource/delete'),
('资源修改','resource:update','/admin/resource/update'),
('资源查询','resource:list','/admin/resource/list');



# 创建角色资源关联表
CREATE TABLE t_role_resource(
id TINYINT PRIMARY KEY AUTO_INCREMENT comment '角色资源关联 ID',
role_id TINYINT not null comment '角色 id',
resource_id TINYINT not null comment '资源 id'
)engine=innodb auto_increment=1 charset=utf8 comment='角色资源关联表';


# 创建数据用于测试
INSERT INTO t_role_resource(role_id,resource_id)
VALUES(1,1),
(2,3),(2,5),(2,6),(2,7),(2,9),(2,11),(2,12),(2,13),(2,15),(2,17),(2,18),
(3,6),(3,7),(3,8),(3,14),
(4,6),(4,7),(4,12),(4,18);