第10课:Spring Boot集成MyBatis
1. MyBatis 介绍
大家都知道,MyBatis 框架是一个持久层框架,是 Apache 下的顶级项目。Mybatis 可以让开发者的主要精力放在 sql 上,通过 Mybatis 提供的映射方式,自由灵活的生成满足需要的 sql 语句。使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs 映射成数据库中的记录,在国内可谓是占据了半壁江山。本节课程主要通过两种方式来对 Spring Boot 集成 MyBatis 做一讲解。重点讲解一下基于注解的方式。因为实际项目中使用注解的方式更多一点,更简洁一点,省去了很多 xml 配置(这不是绝对的,有些项目组中可能也在使用 xml 的方式)。
2. MyBatis 的配置
2.1 依赖导入
Spring Boot 集成 MyBatis,需要导入 mybatis-spring-boot-starter
和 mysql 的依赖,这里我们使用的版本时 1.3.2,如下:
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
我们点开 mybatis-spring-boot-starter
依赖,可以看到我们之前使用 Spring 时候熟悉的依赖,就像我在课程的一开始介绍的那样,Spring Boot 致力于简化编码,使用 starter 系列将相关依赖集成在一起,开发者不需要关注繁琐的配置,非常方便。
org.mybatis
mybatis
org.mybatis
mybatis-spring
2.2 properties.yml配置
我们再来看一下,集成 MyBatis 时需要在 properties.yml 配置文件中做哪些基本配置呢?
# 服务端口号
server:
port: 8080
# 数据库地址
datasource:
url: localhost:3306/blog_test
spring:
datasource: # 数据库配置
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
username: root
password: 123456
hikari:
maximum-pool-size: 10 # 最大连接池数
max-lifetime: 1770000
mybatis:
# 指定别名设置的包为所有entity
type-aliases-package: com.itcodai.course10.entity
configuration:
map-underscore-to-camel-case: true # 驼峰命名规范
mapper-locations: # mapper映射文件位置
- classpath:mapper/*.xml
我们来简单介绍一下上面的这些配置:关于数据库的相关配置,我就不详细的解说了,这点相信大家已经非常熟练了,配置一下用户名、密码、数据库连接等等,这里使用的连接池是 Spring Boot 自带的 hikari,感兴趣的朋友可以去百度或者谷歌搜一搜,了解一下。
这里说明一下 map-underscore-to-camel-case: true
, 用来开启驼峰命名规范,这个比较好用,比如数据库中字段名为:user_name
, 那么在实体类中可以定义属性为 userName
(甚至可以写成 username
,也能映射上),会自动匹配到驼峰属性,如果不这样配置的话,针对字段名和属性名不同的情况,会映射不到。
3. 基于 xml 的整合
使用原始的 xml 方式,需要新建 UserMapper.xml 文件,在上面的 application.yml 配置文件中,我们已经定义了 xml 文件的路径:classpath:mapper/*.xml
,所以我们在 resources 目录下新建一个 mapper 文件夹,然后创建一个 UserMapper.xml 文件。
这和整合 Spring 一样的,namespace 中指定的是对应的 Mapper,
中指定对应的实体类,即 User。然后在内部指定表的字段和实体的属性相对应即可。这里我们写一个根据用户名查询用户的 sql。
实体类中有 id,username 和 password,我不在这贴代码,大家可以下载源码查看。UserMapper.java 文件中写一个接口即可:
User getUserByName(String username);
中间省略 service 的代码,我们写一个 Controller 来测试一下:
@RestController
public class TestController {
@Resource
private UserService userService;
@RequestMapping("/getUserByName/{name}")
public User getUserByName(@PathVariable String name) {
return userService.getUserByName(name);
}
}
启动项目,在浏览器中输入:http://localhost:8080/getUserByName/CSDN
即可查询到数据库表中用户名为 CSDN 的用户信息(事先搞两个数据进去即可):
{"id":2,"username":"CSDN","password":"123456"}
这里需要注意一下:Spring Boot 如何知道这个 Mapper 呢?一种方法是在上面的 mapper 层对应的类上面添加 @Mapper
注解即可,但是这种方法有个弊端,当我们有很多个 mapper 时,那么每一个类上面都得添加 @Mapper
注解。另一种比较简便的方法是在 Spring Boot 启动类上添加@MaperScan
注解,来扫描一个包下的所有 mapper。如下:
@SpringBootApplication
@MapperScan("com.itcodai.course10.dao")
public class Course10Application {
public static void main(String[] args) {
SpringApplication.run(Course10Application.class, args);
}
}
这样的话,com.itcodai.course10.dao
包下的所有 mapper 都会被扫描到了。
4. 基于注解的整合
基于注解的整合就不需要 xml 配置文件了,MyBatis 主要提供了 @Select
, @Insert
, @Update
, Delete
四个注解。这四个注解是用的非常多的,也很简单,注解后面跟上对应的 sql 语句即可,我们举个例子:
@Select("select * from user where id = #{id}")
User getUser(Long id);
这跟 xml 文件中写 sql 语句是一样的,这样就不需要 xml 文件了,但是有个问题,有人可能会问,如果是两个参数呢?如果是两个参数,我们需要使用 @Param
注解来指定每一个参数的对应关系,如下:
@Select("select * from user where id = #{id} and user_name=#{name}")
User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);
可以看出,@Param
指定的参数应该要和 sql 中 #{}
取的参数名相同,不同则取不到。可以在 controller 中自行测试一下,接口都在源码中,文章中我就不贴测试代码和结果了。
有个问题需要注意一下,一般我们在设计表字段后,都会根据自动生成工具生成实体类,这样的话,基本上实体类是能和表字段对应上的,最起码也是驼峰对应的,由于在上面配置文件中开启了驼峰的配置,所以字段都是能对的上的。但是,万一有对不上的呢?我们也有解决办法,使用 @Results
注解来解决。
@Select("select * from user where id = #{id}")
@Results({
@Result(property = "username", column = "user_name"),
@Result(property = "password", column = "password")
})
User getUser(Long id);
@Results
中的 @Result
注解是用来指定每一个属性和字段的对应关系,这样的话就可以解决上面说的这个问题了。
当然了,我们也可以 xml 和注解相结合使用,目前我们实际的项目中也是采用混用的方式,因为有时候 xml 方便,有时候注解方便,比如就上面这个问题来说,如果我们定义了上面的这个 UserMapper.xml,那么我们完全可以使用 @ResultMap
注解来替代 @Results
注解,如下:
@Select("select * from user where id = #{id}")
@ResultMap("BaseResultMap")
User getUser(Long id);
@ResultMap
注解中的值从哪来呢?对应的是 UserMapper.xml 文件中定义的
时对应的 id 值:
这种 xml 和注解结合着使用的情况也很常见,而且也减少了大量的代码,因为 xml 文件可以使用自动生成工具去生成,也不需要人为手动敲,所以这种使用方式也很常见。
5. 总结
本节课主要系统的讲解了 Spring Boot 集成 MyBatis 的过程,分为基于 xml 形式和基于注解的形式来讲解,通过实际配置手把手讲解了 Spring Boot 中 MyBatis 的使用方式,并针对注解方式,讲解了常见的问题已经解决方式,有很强的实战意义。在实际项目中,建议根据实际情况来确定使用哪种方式,一般 xml 和注解都在用。
课程源代码下载地址:戳我下载
第11课:Spring Boot事务配置管理
1. 事务相关
场景:我们在开发企业应用时,由于数据操作在顺序执行的过程中,线上可能有各种无法预知的问题,任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚。
事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。这很好理解,转账、购票等等,必须整个事件流程全部执行完才能人为该事件执行成功,不能转钱转到一半,系统死了,转账人钱没了,收款人钱还没到。
事务管理是 Spring Boot 框架中最为常用的功能之一,我们在实际应用开发时,基本上在 service 层处理业务逻辑的时候都要加上事务,当然了,有时候可能由于场景需要,也不用加事务(比如我们就要往一个表里插数据,相互没有影响,插多少是多少,不能因为某个数据挂了,把之前插的全部回滚)。
2. Spring Boot 事务配置
2.1 依赖导入
在 Spring Boot 中使用事务,需要导入 mysql 依赖:
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
导入了 mysql 依赖后,Spring Boot 会自动注入 DataSourceTransactionManager,我们不需要任何其他的配置就可以用 @Transactional
注解进行事务的使用。关于 mybatis 的配置,在上一节课中已经说明了,这里还是使用上一节课中的 mybatis 配置即可。
2.2 事务的测试
我们首先在数据库表中插入一条数据:
id | user_name | password |
---|---|---|
1 | 倪升武 | 123456 |
然后我们写一个插入的 mapper:
public interface UserMapper {
@Insert("insert into user (user_name, password) values (#{username}, #{password})")
Integer insertUser(User user);
}
OK,接下来我们来测试一下 Spring Boot 中的事务处理,在 service 层,我们手动抛出个异常来模拟实际中出现的异常,然后观察一下事务有没有回滚,如果数据库中没有新的记录,则说明事务回滚成功。
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void isertUser(User user) {
// 插入用户信息
userMapper.insertUser(user);
// 手动抛出异常
throw new RuntimeException();
}
}
我们来测试一下:
@RestController
public class TestController {
@Resource
private UserService userService;
@PostMapping("/adduser")
public String addUser(@RequestBody User user) throws Exception {
if (null != user) {
userService.isertUser(user);
return "success";
} else {
return "false";
}
}
}
我们使用 postman 调用一下该接口,因为在程序中抛出了个异常,会造成事务回滚,我们刷新一下数据库,并没有增加一条记录,说明事务生效了。事务很简单,我们平时在使用的时候,一般不会有多少问题,但是并不仅仅如此……
3. 常见问题总结
从上面的内容中可以看出,Spring Boot 中使用事务非常简单,@Transactional
注解即可解决问题,说是这么说,但是在实际项目中,是有很多小坑在等着我们,这些小坑是我们在写代码的时候没有注意到,而且正常情况下不容易发现这些小坑,等项目写大了,某一天突然出问题了,排查问题非常困难,到时候肯定是抓瞎,需要费很大的精力去排查问题。
这一小节,我专门针对实际项目中经常出现的,和事务相关的细节做一下总结,希望读者在读完之后,能够落实到自己的项目中,能有所受益。
3.1 异常并没有被 ”捕获“ 到
首先要说的,就是异常并没有被 ”捕获“ 到,导致事务并没有回滚。我们在业务层代码中,也许已经考虑到了异常的存在,或者编辑器已经提示我们需要抛出异常,但是这里面有个需要注意的地方:并不是说我们把异常抛出来了,有异常了事务就会回滚,我们来看一个例子:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional
public void isertUser2(User user) throws Exception {
// 插入用户信息
userMapper.insertUser(user);
// 手动抛出异常
throw new SQLException("数据库异常");
}
}
我们看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException
来模拟实际中操作数据库发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,读者可以使用我源码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据的。
那么问题出在哪呢?因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)和程序错误(Error)才会回滚。比如上面我们的例子中抛出的 RuntimeException 就没有问题,但是抛出 SQLException 就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在 @Transactional
注解中使用 rollbackFor
属性来指定异常,比如 @Transactional(rollbackFor = Exception.class)
,这样就没有问题了,所以在实际项目中,一定要指定异常。
3.2 异常被 ”吃“ 掉
这个标题很搞笑,异常怎么会被吃掉呢?还是回归到现实项目中去,我们在处理异常时,有两种方式,要么抛出去,让上一层来捕获处理;要么把异常 try catch 掉,在异常出现的地方给处理掉。就因为有这中 try...catch,所以导致异常被 ”吃“ 掉,事务无法回滚。我们还是看上面那个例子,只不过简单修改一下代码:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void isertUser3(User user) {
try {
// 插入用户信息
userMapper.insertUser(user);
// 手动抛出异常
throw new SQLException("数据库异常");
} catch (Exception e) {
// 异常处理逻辑
}
}
}
读者可以使用我源码中 controller 的接口,通过 postman 测试一下,就会发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。这个细节往往比上面那个坑更难以发现,因为我们的思维很容易导致 try...catch 代码的产生,一旦出现这种问题,往往排查起来比较费劲,所以我们平时在写代码时,一定要多思考,多注意这种细节,尽量避免给自己埋坑。
那这种怎么解决呢?直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
3.3 事务的范围
事务范围这个东西比上面两个坑埋的更深!我之所以把这个也写上,是因为这是我之前在实际项目中遇到的,该场景在这个课程中我就不模拟了,我写一个 demo 让大家看一下,把这个坑记住即可,以后在写代码时,遇到并发问题,就会注意这个坑了,那么这节课也就有价值了。
我来写个 demo:
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void isertUser4(User user) {
// 实际中的具体业务……
userMapper.insertUser(user);
}
}
可以看到,因为要考虑并发问题,我在业务层代码的方法上加了个 synchronized 关键字。我举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。
但是在压测时,就会出现上面的问题,数据库中确实有两条同一用户的信息,分析其原因,在于事务的范围和锁的范围问题。
从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行完了后,事务关闭。但是 synchronized 没有起作用,其实根本原因是因为事务的范围比锁的范围大。也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插入动作,导致了脏数据。
这个问题可以避免,第一,把事务去掉即可(不推荐);第二,在调用该 service 的地方加锁,保证锁的范围比事务的范围大即可。
4. 总结
本章主要总结了 Spring Boot 中如何使用事务,只要使用 @Transactional
注解即可使用,非常简单方便。除此之外,重点总结了三个在实际项目中可能遇到的坑点,这非常有意义,因为事务这东西不出问题还好,出了问题比较难以排查,所以总结的这三点注意事项,希望能帮助到开发中的朋友。
课程源代码下载地址:戳我下载
第12课:Spring Boot中使用监听器
1. 监听器介绍
什么是 web 监听器?web 监听器是一种 Servlet 中特殊的类,它们能帮助开发者监听 web 中特定的事件,比如 ServletContext, HttpSession, ServletRequest 的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控。
2. Spring Boot中监听器的使用
web 监听器的使用场景很多,比如监听 servlet 上下文用来初始化一些数据、监听 http session 用来获取当前在线的人数、监听客户端请求的 servlet request 对象来获取用户的访问信息等等。这一节中,我们主要通过这三个实际的使用场景来学习一下 Spring Boot 中监听器的使用。
2.1 监听Servlet上下文对象
监听 servlet 上下文对象可以用来初始化数据,用于缓存。什么意思呢?我举一个很常见的场景,比如用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话,用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。
针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活一点,可以再加个定时器,定期的来更新这个首页缓存。就类似与 CSDN 个人博客首页中排名的变化一样。
下面我们针对这个功能,来写一个 demo,在实际中,读者可以完全套用该代码,来实现自己项目中的相关逻辑。首先写一个 Service,模拟一下从数据库查询数据:
@Service
public class UserService {
/**
* 获取用户信息
* @return
*/
public User getUser() {
// 实际中会根据具体的业务场景,从数据库中查询对应的信息
return new User(1L, "倪升武", "123456");
}
}
然后写一个监听器,实现 ApplicationListener
接口,重写 onApplicationEvent
方法,将 ContextRefreshedEvent 对象传进去。如果我们想在加载或刷新应用上下文时,也重新刷新下我们预加载的资源,就可以通过监听 ContextRefreshedEvent 来做这样的事情。如下:
/**
* 使用ApplicationListener来初始化一些数据到application域中的监听器
* @author shengni ni
* @date 2018/07/05
*/
@Component
public class MyServletContextListener implements ApplicationListener {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 先获取到application上下文
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
// 获取对应的service
UserService userService = applicationContext.getBean(UserService.class);
User user = userService.getUser();
// 获取application域对象,将查到的信息放到application域中
ServletContext application = applicationContext.getBean(ServletContext.class);
application.setAttribute("user", user);
}
}
正如注释中描述的一样,首先通过 contextRefreshedEvent 来获取 application 上下文,再通过 application 上下文来获取 UserService 这个 bean,项目中可以根据实际业务场景,也可以获取其他的 bean,然后再调用自己的业务代码获取相应的数据,最后存储到 application 域中,这样前端在请求相应数据的时候,我们就可以直接从 application 域中获取信息,减少数据库的压力。下面写一个 Controller 直接从 application 域中获取 user 信息来测试一下。
@RestController
@RequestMapping("/listener")
public class TestController {
@GetMapping("/user")
public User getUser(HttpServletRequest request) {
ServletContext application = request.getServletContext();
return (User) application.getAttribute("user");
}
}
启动项目,在浏览器中输入 http://localhost:8080/listener/user
测试一下即可,如果正常返回 user 信息,那么说明数据已经缓存成功。不过 application 这种是缓存在内存中,对内存会有消耗,后面的课程中我会讲到 redis,到时候再给大家介绍一下 redis 的缓存。
2.2 监听HTTP会话 Session对象
监听器还有一个比较常用的地方就是用来监听 session 对象,来获取在线用户数量,现在有很多开发者都有自己的网站,监听 session 来获取当前在下用户数量是个很常见的使用场景,下面来介绍一下如何来使用。
/**
* 使用HttpSessionListener统计在线用户数的监听器
* @author shengwu ni
* @date 2018/07/05
*/
@Component
public class MyHttpSessionListener implements HttpSessionListener {
private static final Logger logger = LoggerFactory.getLogger(MyHttpSessionListener.class);
/**
* 记录在线的用户数量
*/
public Integer count = 0;
@Override
public synchronized void sessionCreated(HttpSessionEvent httpSessionEvent) {
logger.info("新用户上线了");
count++;
httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
}
@Override
public synchronized void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
logger.info("用户下线了");
count--;
httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
}
}
可以看出,首先该监听器需要实现 HttpSessionListener 接口,然后重写 sessionCreated
和 sessionDestroyed
方法,在 sessionCreated
方法中传递一个 HttpSessionEvent 对象,然后将当前 session 中的用户数量加1,sessionDestroyed
方法刚好相反,不再赘述。然后我们写一个 Controller 来测试一下。
@RestController
@RequestMapping("/listener")
public class TestController {
/**
* 获取当前在线人数,该方法有bug
* @param request
* @return
*/
@GetMapping("/total")
public String getTotalUser(HttpServletRequest request) {
Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:" + count;
}
}
该 Controller 中是直接获取当前 session 中的用户数量,启动服务器,在浏览器中输入 localhost:8080/listener/total
可以看到返回的结果是1,再打开一个浏览器,请求相同的地址可以看到 count 是 2 ,这没有问题。但是如果关闭一个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是 session 销毁的方法没有执行(可以在后台控制台观察日志打印情况),当重新打开时,服务器找不到用户原来的 session,于是又重新创建了一个 session,那怎么解决该问题呢?我们可以将上面的 Controller 方法改造一下:
@GetMapping("/total2")
public String getTotalUser(HttpServletRequest request, HttpServletResponse response) {
Cookie cookie;
try {
// 把sessionId记录在浏览器中
cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
cookie.setPath("/");
//设置cookie有效期为2天,设置长一点
cookie.setMaxAge( 48*60 * 60);
response.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:" + count;
}
可以看出,该处理逻辑是让服务器记得原来那个 session,即把原来的 sessionId 记录在浏览器中,下次再打开时,把这个 sessionId 传过去,这样服务器就不会重新再创建了。重启一下服务器,在浏览器中再次测试一下,即可避免上面的问题。
2.3 监听客户端请求Servlet Request对象
使用监听器获取用户的访问信息比较简单,实现 ServletRequestListener 接口即可,然后通过 request 对象获取一些信息。如下:
/**
* 使用ServletRequestListener获取访问信息
* @author shengwu ni
* @date 2018/07/05
*/
@Component
public class MyServletRequestListener implements ServletRequestListener {
private static final Logger logger = LoggerFactory.getLogger(MyServletRequestListener.class);
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
logger.info("session id为:{}", request.getRequestedSessionId());
logger.info("request url为:{}", request.getRequestURL());
request.setAttribute("name", "倪升武");
}
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
logger.info("request end");
HttpServletRequest request = (HttpServletRequest) servletRequestEvent.getServletRequest();
logger.info("request域中保存的name值为:{}", request.getAttribute("name"));
}
}
这个比较简单,不再赘述,接下来写一个 Controller 测试一下即可。
@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));
return "success";
}
3. Spring Boot中自定义事件监听
在实际项目中,我们往往需要自定义一些事件和监听器来满足业务场景,比如在微服务中会有这样的场景:微服务 A 在处理完某个逻辑之后,需要通知微服务 B 去处理另一个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景非常普遍,这个时候,我们可以自定义事件以及监听器来监听,一旦监听到微服务 A 中的某事件发生,就去通知微服务 B 处理对应的逻辑。
3.1 自定义事件
自定义事件需要继承 ApplicationEvent 对象,在事件中定义一个 User 对象来模拟数据,构造方法中将 User 对象传进来初始化。如下:
/**
* 自定义事件
* @author shengwu ni
* @date 2018/07/05
*/
public class MyEvent extends ApplicationEvent {
private User user;
public MyEvent(Object source, User user) {
super(source);
this.user = user;
}
// 省去get、set方法
}
3.2 自定义监听器
接下来,自定义一个监听器来监听上面定义的 MyEvent 事件,自定义监听器需要实现 ApplicationListener
接口即可。如下:
/**
* 自定义监听器,监听MyEvent事件
* @author shengwu ni
* @date 2018/07/05
*/
@Component
public class MyEventListener implements ApplicationListener {
@Override
public void onApplicationEvent(MyEvent myEvent) {
// 把事件中的信息获取到
User user = myEvent.getUser();
// 处理事件,实际项目中可以通知别的微服务或者处理其他逻辑等等
System.out.println("用户名:" + user.getUsername());
System.out.println("密码:" + user.getPassword());
}
}
然后重写 onApplicationEvent
方法,将自定义的 MyEvent 事件传进来,因为该事件中,我们定义了 User 对象(该对象在实际中就是需要处理的数据,在下文来模拟),然后就可以使用该对象的信息了。
OK,定义好了事件和监听器之后,需要手动发布事件,这样监听器才能监听到,这需要根据实际业务场景来触发,针对本文的例子,我写个触发逻辑,如下:
/**
* UserService
* @author shengwu ni
*/
@Service
public class UserService {
@Resource
private ApplicationContext applicationContext;
/**
* 发布事件
* @return
*/
public User getUser2() {
User user = new User(1L, "倪升武", "123456");
// 发布事件
MyEvent event = new MyEvent(this, user);
applicationContext.publishEvent(event);
return user;
}
}
在 service 中注入 ApplicationContext,在业务代码处理完之后,通过 ApplicationContext 对象手动发布 MyEvent 事件,这样我们自定义的监听器就能监听到,然后处理监听器中写好的业务逻辑。
最后,在 Controller 中写一个接口来测试一下:
@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
System.out.println("requestListener中的初始化的name数据:" + request.getAttribute("name"));
return "success";
}
在浏览器中输入 http://localhost:8080/listener/publish
,然后观察一下控制台打印的用户名和密码,即可说明自定义监听器已经生效。
4. 总结
本课系统的介绍了监听器原理,以及在 Spring Boot 中如何使用监听器,列举了监听器的三个常用的案例,有很好的实战意义。最后讲解了项目中如何自定义事件和监听器,并结合微服务中常见的场景,给出具体的代码模型,均能运用到实际项目中去,希望读者认真消化。
课程源代码下载地址:戳我下载
第13课:Spring Boot中使用拦截器
拦截器的原理很简单,是 AOP 的一种实现,专门拦截对动态资源的后台请求,即拦截对控制层的请求。使用场景比较多的是判断用户是否有权限请求后台,更拔高一层的使用场景也有,比如拦截器可以结合 websocket 一起使用,用来拦截 websocket 请求,然后做相应的处理等等。拦截器不会拦截静态资源,Spring Boot 的默认静态目录为 resources/static,该目录下的静态页面、js、css、图片等等,不会被拦截(也要看如何实现,有些情况也会拦截,我在下文会指出)。
1. 拦截器的快速使用
使用拦截器很简单,只需要两步即可:定义拦截器和配置拦截器。在配置拦截器中,Spring Boot 2.0 以后的版本和之前的版本有所不同,我会重点讲解一下这里可能出现的坑。
1.1 定义拦截器
定义拦截器,只需要实现 HandlerInterceptor
接口,HandlerInterceptor
接口是所有自定义拦截器或者 Spring Boot 提供的拦截器的鼻祖,所以,首先来了解下该接口。该接口中有三个方法: preHandle(……)
、postHandle(……)
和 afterCompletion(……)
。
preHandle(……)
方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在这个方法执行之前。所以preHandle(……)
方法可以决定是否将请求放行,这是通过返回值来决定的,返回 true 则放行,返回 false 则不会向后执行。
postHandle(……)
方法:该方法的执行时机是,当某个 url 已经匹配到对应的 Controller 中的某个方法,且在执行完了该方法,但是在 DispatcherServlet 视图渲染之前。所以在这个方法中有个 ModelAndView 参数,可以在此做一些修改动作。
afterCompletion(……)
方法:顾名思义,该方法是在整个请求处理完成后(包括视图渲染)执行,这时做一些资源的清理工作,这个方法只有在preHandle(……)
被成功执行后并且返回 true 才会被执行。
了解了该接口,接下来自定义一个拦截器。
/**
* 自定义拦截器
* @author shengwu ni
* @date 2018/08/03
*/
public class MyInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(MyInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 返回true才会继续执行,返回false则取消当前请求
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
logger.info("执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
logger.info("整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了");
}
}
OK,到此为止,拦截器已经定义完成,接下来就是对该拦截器进行拦截配置。
1.2 配置拦截器
在 Spring Boot 2.0 之前,我们都是直接继承 WebMvcConfigurerAdapter 类,然后重写 addInterceptors
方法来实现拦截器的配置。但是在 Spring Boot 2.0 之后,该方法已经被废弃了(当然,也可以继续用),取而代之的是 WebMvcConfigurationSupport 方法,如下:
@Configuration
public class MyInterceptorConfig extends WebMvcConfigurationSupport {
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
super.addInterceptors(registry);
}
}
在该配置中重写 addInterceptors
方法,将我们上面自定义的拦截器添加进去,addPathPatterns
方法是添加要拦截的请求,这里我们拦截所有的请求。这样就配置好拦截器了,接下来写一个 Controller 测试一下:
@Controller
@RequestMapping("/interceptor")
public class InterceptorController {
@RequestMapping("/test")
public String test() {
return "hello";
}
}
让其跳转到 hello.html 页面,直接在 hello.html 中输出 hello interceptor
即可。启动项目,在浏览器中输入 localhost:8080/interceptor/test
看一下控制台的日志:
====拦截到了方法:test,在该方法执行之前执行====
执行完方法之后进执行(Controller方法调用之后),但是此时还没进行视图渲染
整个请求都处理完咯,DispatcherServlet也渲染了对应的视图咯,此时我可以做一些清理的工作了
可以看出拦截器已经生效,并能看出其执行顺序。
1.3 解决静态资源被拦截问题
上文中已经介绍了拦截器的定义和配置,但是这样是否就没问题了呢?其实不然,如果使用上面这种配置的话,我们会发现一个缺陷,那就是静态资源被拦截了。可以在 resources/static/ 目录下放置一个图片资源或者 html 文件,然后启动项目直接访问,即可看到无法访问的现象。
也就是说,虽然 Spring Boot 2.0 废弃了WebMvcConfigurerAdapter,但是 WebMvcConfigurationSupport 又会导致默认的静态资源被拦截,这就需要我们手动将静态资源放开。
如何放开呢?除了在 MyInterceptorConfig 配置类中重写 addInterceptors
方法外,还需要再重写一个方法:addResourceHandlers
,将静态资源放开:
/**
* 用来指定静态资源不被拦截,否则继承WebMvcConfigurationSupport这种方式会导致静态资源无法直接访问
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
super.addResourceHandlers(registry);
}
这样配置好之后,重启项目,静态资源也可以正常访问了。如果你是个善于学习或者研究的人,那肯定不会止步于此,没错,上面这种方式的确能解决静态资源无法访问的问题,但是,还有更方便的方式来配置。
我们不继承 WebMvcConfigurationSupport 类,直接实现 WebMvcConfigurer 接口,然后重写 addInterceptors
方法,将自定义的拦截器添加进去即可,如下:
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 实现WebMvcConfigurer不会导致静态资源被拦截
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
这样就非常方便了,实现 WebMvcConfigure 接口的话,不会拦截 Spring Boot 默认的静态资源。
这两种方式都可以,具体他们之间的细节,感兴趣的读者可以做进一步的研究,由于这两种方式的不同,继承 WebMvcConfigurationSupport 类的方式可以用在前后端分离的项目中,后台不需要访问静态资源(就不需要放开静态资源了);实现 WebMvcConfigure 接口的方式可以用在非前后端分离的项目中,因为需要读取一些图片、css、js文件等等。
2. 拦截器使用实例
2.1 判断用户有没有登录
一般用户登录功能我们可以这么做,要么往 session 中写一个 user,要么针对每个 user 生成一个 token,第二种要更好一点,那么针对第二种方式,如果用户登录成功了,每次请求的时候都会带上该用户的 token,如果未登录,则没有该 token,服务端可以检测这个 token 参数的有无来判断用户有没有登录,从而实现拦截功能。我们改造一下 preHandle
方法,如下:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 判断用户有没有登陆,一般登陆之后的用户都有一个对应的token
String token = request.getParameter("token");
if (null == token || "".equals(token)) {
logger.info("用户未登录,没有权限执行……请登录");
return false;
}
// 返回true才会继续执行,返回false则取消当前请求
return true;
}
重启项目,在浏览器中输入 localhost:8080/interceptor/test
后查看控制台日志,发现被拦截,如果在浏览器中输入 localhost:8080/interceptor/test?token=123
即可正常往下走。
2.2 取消拦截操作
根据上文,如果我要拦截所有 /admin
开头的 url 请求的话,需要在拦截器配置中添加这个前缀,但是在实际项目中,可能会有这种场景出现:某个请求也是 /admin
开头的,但是不能拦截,比如 /admin/login
等等,这样的话又需要去配置。那么,可不可以做成一个类似于开关的东西,哪里不需要拦截,我就在哪里弄个开关上去,做成这种灵活的可插拔的效果呢?
是可以的,我们可以定义一个注解,该注解专门用来取消拦截操作,如果某个 Controller 中的方法我们不需要拦截掉,即可在该方法上加上我们自定义的注解即可,下面先定义一个注解:
/**
* 该注解用来指定某个方法不用拦截
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UnInterception {
}
然后在 Controller 中的某个方法上添加该注解,在拦截器处理方法中添加该注解取消拦截的逻辑,如下:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
String methodName = method.getName();
logger.info("====拦截到了方法:{},在该方法执行之前执行====", methodName);
// 通过方法,可以获取该方法上的自定义注解,然后通过注解来判断该方法是否要被拦截
// @UnInterception 是我们自定义的注解
UnInterception unInterception = method.getAnnotation(UnInterception.class);
if (null != unInterception) {
return true;
}
// 返回true才会继续执行,返回false则取消当前请求
return true;
}
Controller 中的方法代码可以参见源码,重启项目在浏览器中输入 http://localhost:8080/interceptor/test2?token=123
测试一下,可以看出,加了该注解的方法不会被拦截。
3. 总结
本节主要介绍了 Spring Boot 中拦截器的使用,从拦截器的创建、配置,到拦截器对静态资源的影响,都做了详细的分析。Spring Boot 2.0 之后拦截器的配置支持两种方式,可以根据实际情况选择不同的配置方式。最后结合实际中的使用,举了两个常用的场景,希望读者能够认真消化,掌握拦截器的使用。
课程源代码下载地址:戳我下载
第14课:Spring Boot 中集成Redis
1. Redis 介绍
Redis 是一种非关系型数据库(NoSQL),NoSQL 是以 key-value 的形式存储的,和传统的关系型数据库不一样,不一定遵循传统数据库的一些基本要求,比如说 SQL 标准,ACID 属性,表结构等等,这类数据库主要有以下特点:非关系型的、分布式的、开源的、水平可扩展的。
NoSQL 使用场景有:对数据高并发读写、对海量数据的高效率存储和访问、对数据的高可扩展性和高可用性等等。
Redis 的 key 可以是字符串、哈希、链表、集合和有序集合。value 类型很多,包括 String、list、set、zset。这些数据类型都支持 push/pop、add/remove、取交集和并集以及更多更丰富的操作,Redis 也支持各种不同方式的排序。为了保证效率,数据都是在缓存在内存中,它也可以周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件中。 有了 redis 有哪些好处呢?举个比较简单的例子,看下图:
Redis 集群和 Mysql 是同步的,首先会从 redis 中获取数据,如果 redis 挂了,再从 mysql 中获取数据,这样网站就不会挂掉。更多关于 redis 的介绍以及使用场景,可以谷歌和百度,在这就不赘述了。
2. Redis 安装
本课程是在 vmvare 虚拟机中来安装的 redis (centos 7),学习的时候如果有自己的阿里云服务器,也可以在阿里云中来安装 redis,都可以。只要能 ping 的通云主机或者虚拟机的 ip,然后在虚拟机或者云主机中放行对应的端口(或者关掉防火墙)即可访问 redis。下面来介绍一下 redis 的安装过程:
- 安装 gcc 编译
因为后面安装redis的时候需要编译,所以事先得先安装gcc编译。阿里云主机已经默认安装了 gcc,如果是自己安装的虚拟机,那么需要先安装一下 gcc:
yum install gcc-c++
- 下载 redis
有两种方式下载安装包,一种是去官网上下载(https://redis.io),然后将安装包考到 centos 中,另种方法是直接使用 wget 来下载:
wget http://download.redis.io/releases/redis-3.2.8.tar.gz
如果没有安装过 wget,可以通过如下命令安装:
yum install wget
- 解压安装
解压安装包:
tar –vzxf redis-3.2.8.tar.gz
然后将解压的文件夹 redis-3.2.8 放到 /usr/local/
下,一般安装软件都放在 /usr/local
下。然后进入 /usr/local/redis-3.2.8/
文件夹下,执行 make
命令即可完成安装。
【注】如果 make 失败,可以尝试如下命令:
make MALLOC=libc
make install
- 修改配置文件
安装成功之后,需要修改一下配置文件,包括允许接入的 ip,允许后台执行,设置密码等等。
打开 redis 配置文件:vi redis.conf
在命令模式下输入 /bind
来查找 bind 配置,按 n 来查找下一个,找到配置后,将 bind 配置成 0.0.0.0,允许任意服务器来访问 redis,即:
bind 0.0.0.0
使用同样的方法,将 daemonize 改成 yes (默认为 no),允许 redis 在后台执行。
将 requirepass 注释打开,并设置密码为 123456(密码自己设置)。
- 启动 redis
在 redis-3.2.8 目录下,指定刚刚修改好的配置文件 redis.conf 来启动 redis:
redis-server ./redis.conf
再启动 redis 客户端:
redis-cli
由于我们设置了密码,在启动客户端之后,输入 auth 123456
即可登录进入客户端。
然后我们来测试一下,往 redis 中插入一个数据:
set name CSDN
然后来获取 name
get name
如果正常获取到 CSDN,则说明没有问题。
3. Spring Boot 集成 Redis
3.1 依赖导入
Spring Boot 集成 redis 很方便,只需要导入一个 redis 的 starter 依赖即可。如下:
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba
fastjson
1.2.35
这里也导入阿里巴巴的 fastjson 是为了在后面我们要存一个实体,为了方便把实体转换成 json 字符串存进去。
3.2 Redis 配置
导入了依赖之后,我们在 application.yml 文件里配置 redis:
server:
port: 8080
spring:
#redis相关配置
redis:
database: 5
# 配置redis的主机地址,需要修改成自己的
host: 192.168.48.190
port: 6379
password: 123456
timeout: 5000
jedis:
pool:
# 连接池中的最大空闲连接,默认值也是8。
max-idle: 500
# 连接池中的最小空闲连接,默认值也是0。
min-idle: 50
# 如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)
max-active: 1000
# 等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException
max-wait: 2000
3.3 常用 api 介绍
Spring Boot 对 redis 的支持已经非常完善了,丰富的 api 已经足够我们日常的开发,这里我介绍几个最常用的供大家学习,其他 api 希望大家自己多学习,多研究。用到会去查即可。
有两个 redis 模板:RedisTemplate 和 StringRedisTemplate。我们不使用 RedisTemplate,RedisTemplate 提供给我们操作对象,操作对象的时候,我们通常是以 json 格式存储,但在存储的时候,会使用 Redis 默认的内部序列化器;导致我们存进里面的是乱码之类的东西。当然了,我们可以自己定义序列化,但是比较麻烦,所以使用 StringRedisTemplate 模板。StringRedisTemplate 主要给我们提供字符串操作,我们可以将实体类等转成 json 字符串即可,在取出来后,也可以转成相应的对象,这就是上面我导入了阿里 fastjson 的原因。
3.3.1 redis:string 类型
新建一个 RedisService,注入 StringRedisTemplate,使用 stringRedisTemplate.opsForValue()
可以获取 ValueOperations
对象,通过该对象即可读写 redis 数据库了。如下:
public class RedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* set redis: string类型
* @param key key
* @param value value
*/
public void setString(String key, String value){
ValueOperations valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set(key, value);
}
/**
* get redis: string类型
* @param key key
* @return
*/
public String getString(String key){
return stringRedisTemplate.opsForValue().get(key);
}
该对象操作的是 string,我们也可以存实体类,只需要将实体类转换成 json 字符串即可。下面来测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Course14ApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);
@Resource
private RedisService redisService;
@Test
public void contextLoads() {
//测试redis的string类型
redisService.setString("weichat","程序员私房菜");
logger.info("我的微信公众号为:{}", redisService.getString("weichat"));
// 如果是个实体,我们可以使用json工具转成json字符串,
User user = new User("CSDN", "123456");
redisService.setString("userInfo", JSON.toJSONString(user));
logger.info("用户信息:{}", redisService.getString("userInfo"));
}
}
先启动 redis,然后运行这个测试用例,观察控制台打印的日志如下:
我的微信公众号为:程序员私房菜
用户信息:{"password":"123456","username":"CSDN"}
3.3.2 redis:hash 类型
hash 类型其实原理和 string 一样的,但是有两个 key,使用 stringRedisTemplate.opsForHash()
可以获取 HashOperations
对象。比如我们要存储订单信息,所有订单信息都放在 order 下,针对不同用户的订单实体,可以通过用户的 id 来区分,这就相当于两个 key 了。
@Service
public class RedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* set redis: hash类型
* @param key key
* @param filedKey filedkey
* @param value value
*/
public void setHash(String key, String filedKey, String value){
HashOperations hashOperations = stringRedisTemplate.opsForHash();
hashOperations.put(key,filedKey, value);
}
/**
* get redis: hash类型
* @param key key
* @param filedkey filedkey
* @return
*/
public String getHash(String key, String filedkey){
return (String) stringRedisTemplate.opsForHash().get(key, filedkey);
}
}
可以看出,hash 和 string 没啥两样,只不过多了个参数,Spring Boot 中操作 redis 非常简单方便。来测试一下:
@SpringBootTest
public class Course14ApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);
@Resource
private RedisService redisService;
@Test
public void contextLoads() {
//测试redis的hash类型
redisService.setHash("user", "name", JSON.toJSONString(user));
logger.info("用户姓名:{}", redisService.getHash("user","name"));
}
}
3.3.3 redis:list 类型
使用 stringRedisTemplate.opsForList()
可以获取 ListOperations
redis 列表对象,该列表是个简单的字符串列表,可以支持从左侧添加,也可以支持从右侧添加,一个列表最多包含 2 ^ 32 -1 个元素。
@Service
public class RedisService {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* set redis:list类型
* @param key key
* @param value value
* @return
*/
public long setList(String key, String value){
ListOperations listOperations = stringRedisTemplate.opsForList();
return listOperations.leftPush(key, value);
}
/**
* get redis:list类型
* @param key key
* @param start start
* @param end end
* @return
*/
public List getList(String key, long start, long end){
return stringRedisTemplate.opsForList().range(key, start, end);
}
}
可以看出,这些 api 都是一样的形式,方便记忆也方便使用。具体的 api 细节我就不展开了,大家可以自己看 api 文档。其实,这些 api 根据参数和返回值也能知道它们是做什么用的。来测试一下:
@RunWith(SpringRunner.class)
@SpringBootTest
public class Course14ApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(Course14ApplicationTests.class);
@Resource
private RedisService redisService;
@Test
public void contextLoads() {
//测试redis的list类型
redisService.setList("list", "football");
redisService.setList("list", "basketball");
List valList = redisService.getList("list",0,-1);
for(String value :valList){
logger.info("list中有:{}", value);
}
}
}
4. 总结
本节主要介绍了 redis 的使用场景、安装过程,以及 Spring Boot 中集成 redis 的详细步骤。在实际项目中,通常都用 redis 作为缓存,在查询数据库的时候,会先从 redis 中查找,如果有信息,则从 redis 中取;如果没有,则从数据库中查,并且同步到 redis 中,下次 redis 中就有了。更新和删除也是如此,都需要同步到 redis。redis 在高并发场景下运用的很多。
课程源代码下载地址:戳我下载