springboot學(xué)習(xí)3

第10課:Spring Boot集成MyBatis

1. MyBatis 介紹

大家都知道妄迁,MyBatis 框架是一個持久層框架,是 Apache 下的頂級項目溜腐。Mybatis 可以讓開發(fā)者的主要精力放在 sql 上钱豁,通過 Mybatis 提供的映射方式,自由靈活的生成滿足需要的 sql 語句吉执。使用簡單的 XML 或注解來配置和映射原生信息疯淫,將接口和 Java 的 POJOs 映射成數(shù)據(jù)庫中的記錄,在國內(nèi)可謂是占據(jù)了半壁江山戳玫。本節(jié)課程主要通過兩種方式來對 Spring Boot 集成 MyBatis 做一講解熙掺。重點講解一下基于注解的方式。因為實際項目中使用注解的方式更多一點咕宿,更簡潔一點币绩,省去了很多 xml 配置(這不是絕對的蜡秽,有些項目組中可能也在使用 xml 的方式)。

2. MyBatis 的配置

2.1 依賴導(dǎo)入

Spring Boot 集成 MyBatis缆镣,需要導(dǎo)入 mybatis-spring-boot-starter 和 mysql 的依賴芽突,這里我們使用的版本時 1.3.2,如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

我們點開 mybatis-spring-boot-starter 依賴董瞻,可以看到我們之前使用 Spring 時候熟悉的依賴寞蚌,就像我在課程的一開始介紹的那樣,Spring Boot 致力于簡化編碼钠糊,使用 starter 系列將相關(guān)依賴集成在一起挟秤,開發(fā)者不需要關(guān)注繁瑣的配置,非常方便抄伍。

<!-- 省去其他 -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
</dependency>

2.2 properties.yml配置

我們再來看一下艘刚,集成 MyBatis 時需要在 properties.yml 配置文件中做哪些基本配置呢?

# 服務(wù)端口號
server:
  port: 8080

# 數(shù)據(jù)庫地址
datasource:
  url: localhost:3306/blog_test

spring:
  datasource: # 數(shù)據(jù)庫配置
    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 # 最大連接池數(shù)
      max-lifetime: 1770000

mybatis:
  # 指定別名設(shè)置的包為所有entity
  type-aliases-package: com.itcodai.course10.entity
  configuration:
    map-underscore-to-camel-case: true # 駝峰命名規(guī)范
  mapper-locations: # mapper映射文件位置
    - classpath:mapper/*.xml

我們來簡單介紹一下上面的這些配置:關(guān)于數(shù)據(jù)庫的相關(guān)配置截珍,我就不詳細的解說了攀甚,這點相信大家已經(jīng)非常熟練了,配置一下用戶名笛臣、密碼云稚、數(shù)據(jù)庫連接等等,這里使用的連接池是 Spring Boot 自帶的 hikari沈堡,感興趣的朋友可以去百度或者谷歌搜一搜静陈,了解一下。

這里說明一下 map-underscore-to-camel-case: true诞丽, 用來開啟駝峰命名規(guī)范鲸拥,這個比較好用,比如數(shù)據(jù)庫中字段名為:user_name僧免, 那么在實體類中可以定義屬性為 userName (甚至可以寫成 username刑赶,也能映射上),會自動匹配到駝峰屬性懂衩,如果不這樣配置的話撞叨,針對字段名和屬性名不同的情況,會映射不到浊洞。

3. 基于 xml 的整合

使用原始的 xml 方式牵敷,需要新建 UserMapper.xml 文件,在上面的 application.yml 配置文件中法希,我們已經(jīng)定義了 xml 文件的路徑:classpath:mapper/*.xml枷餐,所以我們在 resources 目錄下新建一個 mapper 文件夾,然后創(chuàng)建一個 UserMapper.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.itcodai.course10.dao.UserMapper">
  <resultMap id="BaseResultMap" type="com.itcodai.course10.entity.User">

    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="user_name" jdbcType="VARCHAR" property="username" />
    <result column="password" jdbcType="VARCHAR" property="password" />
  </resultMap>
  
   <select id="getUserByName" resultType="User" parameterType="String">
       select * from user where user_name = #{username}
  </select>
</mapper>

這和整合 Spring 一樣的毛肋,namespace 中指定的是對應(yīng)的 Mapper怨咪, <resultMap> 中指定對應(yīng)的實體類,即 User润匙。然后在內(nèi)部指定表的字段和實體的屬性相對應(yīng)即可诗眨。這里我們寫一個根據(jù)用戶名查詢用戶的 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 即可查詢到數(shù)據(jù)庫表中用戶名為 CSDN 的用戶信息(事先搞兩個數(shù)據(jù)進去即可):

{"id":2,"username":"CSDN","password":"123456"}

這里需要注意一下:Spring Boot 如何知道這個 Mapper 呢蟀苛?一種方法是在上面的 mapper 層對應(yīng)的類上面添加 @Mapper 注解即可,但是這種方法有個弊端逮诲,當(dāng)我們有很多個 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 四個注解。這四個注解是用的非常多的锭弊,也很簡單堪澎,注解后面跟上對應(yīng)的 sql 語句即可,我們舉個例子:

@Select("select * from user where id = #{id}")
User getUser(Long id);

這跟 xml 文件中寫 sql 語句是一樣的味滞,這樣就不需要 xml 文件了樱蛤,但是有個問題,有人可能會問剑鞍,如果是兩個參數(shù)呢昨凡?如果是兩個參數(shù),我們需要使用 @Param 注解來指定每一個參數(shù)的對應(yīng)關(guān)系攒暇,如下:

@Select("select * from user where id = #{id} and user_name=#{name}")
User getUserByIdAndName(@Param("id") Long id, @Param("name") String username);

可以看出土匀,@Param 指定的參數(shù)應(yīng)該要和 sql 中 #{} 取的參數(shù)名相同,不同則取不到形用【驮可以在 controller 中自行測試一下证杭,接口都在源碼中,文章中我就不貼測試代碼和結(jié)果了妒御。

有個問題需要注意一下解愤,一般我們在設(shè)計表字段后,都會根據(jù)自動生成工具生成實體類乎莉,這樣的話送讲,基本上實體類是能和表字段對應(yīng)上的,最起碼也是駝峰對應(yīng)的惋啃,由于在上面配置文件中開啟了駝峰的配置哼鬓,所以字段都是能對的上的。但是边灭,萬一有對不上的呢异希?我們也有解決辦法,使用 @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 注解是用來指定每一個屬性和字段的對應(yīng)關(guān)系称簿,這樣的話就可以解決上面說的這個問題了。

當(dāng)然了惰帽,我們也可以 xml 和注解相結(jié)合使用憨降,目前我們實際的項目中也是采用混用的方式,因為有時候 xml 方便该酗,有時候注解方便授药,比如就上面這個問題來說,如果我們定義了上面的這個 UserMapper.xml垂涯,那么我們完全可以使用 @ResultMap 注解來替代 @Results 注解烁焙,如下:

@Select("select * from user where id = #{id}")
@ResultMap("BaseResultMap")
User getUser(Long id);

@ResultMap 注解中的值從哪來呢?對應(yīng)的是 UserMapper.xml 文件中定義的 <resultMap> 時對應(yīng)的 id 值:

<resultMap id="BaseResultMap" type="com.itcodai.course10.entity.User">

這種 xml 和注解結(jié)合著使用的情況也很常見耕赘,而且也減少了大量的代碼骄蝇,因為 xml 文件可以使用自動生成工具去生成,也不需要人為手動敲操骡,所以這種使用方式也很常見九火。

5. 總結(jié)

本節(jié)課主要系統(tǒng)的講解了 Spring Boot 集成 MyBatis 的過程,分為基于 xml 形式和基于注解的形式來講解册招,通過實際配置手把手講解了 Spring Boot 中 MyBatis 的使用方式岔激,并針對注解方式,講解了常見的問題已經(jīng)解決方式是掰,有很強的實戰(zhàn)意義虑鼎。在實際項目中,建議根據(jù)實際情況來確定使用哪種方式,一般 xml 和注解都在用炫彩。

課程源代碼下載地址:戳我下載

第11課:Spring Boot事務(wù)配置管理

1. 事務(wù)相關(guān)

場景:我們在開發(fā)企業(yè)應(yīng)用時匾七,由于數(shù)據(jù)操作在順序執(zhí)行的過程中,線上可能有各種無法預(yù)知的問題江兢,任何一步操作都有可能發(fā)生異常昨忆,異常則會導(dǎo)致后續(xù)的操作無法完成。此時由于業(yè)務(wù)邏輯并未正確的完成杉允,所以在之前操作過數(shù)據(jù)庫的動作并不可靠邑贴,需要在這種情況下進行數(shù)據(jù)的回滾。

事務(wù)的作用就是為了保證用戶的每一個操作都是可靠的叔磷,事務(wù)中的每一步操作都必須成功執(zhí)行拢驾,只要有發(fā)生異常就回退到事務(wù)開始未進行操作的狀態(tài)。這很好理解世澜,轉(zhuǎn)賬独旷、購票等等,必須整個事件流程全部執(zhí)行完才能人為該事件執(zhí)行成功寥裂,不能轉(zhuǎn)錢轉(zhuǎn)到一半,系統(tǒng)死了案疲,轉(zhuǎn)賬人錢沒了封恰,收款人錢還沒到。

事務(wù)管理是 Spring Boot 框架中最為常用的功能之一褐啡,我們在實際應(yīng)用開發(fā)時诺舔,基本上在 service 層處理業(yè)務(wù)邏輯的時候都要加上事務(wù),當(dāng)然了备畦,有時候可能由于場景需要低飒,也不用加事務(wù)(比如我們就要往一個表里插數(shù)據(jù),相互沒有影響懂盐,插多少是多少褥赊,不能因為某個數(shù)據(jù)掛了,把之前插的全部回滾)莉恼。

2. Spring Boot 事務(wù)配置

2.1 依賴導(dǎo)入

在 Spring Boot 中使用事務(wù)拌喉,需要導(dǎo)入 mysql 依賴:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

導(dǎo)入了 mysql 依賴后,Spring Boot 會自動注入 DataSourceTransactionManager俐银,我們不需要任何其他的配置就可以用 @Transactional 注解進行事務(wù)的使用尿背。關(guān)于 mybatis 的配置,在上一節(jié)課中已經(jīng)說明了捶惜,這里還是使用上一節(jié)課中的 mybatis 配置即可田藐。

2.2 事務(wù)的測試

我們首先在數(shù)據(jù)庫表中插入一條數(shù)據(jù):

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 中的事務(wù)處理,在 service 層汽久,我們手動拋出個異常來模擬實際中出現(xiàn)的異常鹤竭,然后觀察一下事務(wù)有沒有回滾,如果數(shù)據(jù)庫中沒有新的記錄回窘,則說明事務(wù)回滾成功诺擅。

@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 調(diào)用一下該接口,因為在程序中拋出了個異常啡直,會造成事務(wù)回滾烁涌,我們刷新一下數(shù)據(jù)庫,并沒有增加一條記錄酒觅,說明事務(wù)生效了撮执。事務(wù)很簡單,我們平時在使用的時候舷丹,一般不會有多少問題抒钱,但是并不僅僅如此……

3. 常見問題總結(jié)

從上面的內(nèi)容中可以看出,Spring Boot 中使用事務(wù)非常簡單颜凯,@Transactional 注解即可解決問題谋币,說是這么說,但是在實際項目中症概,是有很多小坑在等著我們蕾额,這些小坑是我們在寫代碼的時候沒有注意到,而且正常情況下不容易發(fā)現(xiàn)這些小坑彼城,等項目寫大了诅蝶,某一天突然出問題了,排查問題非常困難募壕,到時候肯定是抓瞎调炬,需要費很大的精力去排查問題。

這一小節(jié)舱馅,我專門針對實際項目中經(jīng)常出現(xiàn)的缰泡,和事務(wù)相關(guān)的細節(jié)做一下總結(jié),希望讀者在讀完之后习柠,能夠落實到自己的項目中匀谣,能有所受益。

3.1 異常并沒有被 ”捕獲“ 到

首先要說的资溃,就是異常并沒有被 ”捕獲“ 到武翎,導(dǎo)致事務(wù)并沒有回滾。我們在業(yè)務(wù)層代碼中溶锭,也許已經(jīng)考慮到了異常的存在宝恶,或者編輯器已經(jīng)提示我們需要拋出異常,但是這里面有個需要注意的地方:并不是說我們把異常拋出來了,有異常了事務(wù)就會回滾垫毙,我們來看一個例子:

@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("數(shù)據(jù)庫異常");
    }
}

我們看上面這個代碼霹疫,其實并沒有什么問題,手動拋出一個 SQLException 來模擬實際中操作數(shù)據(jù)庫發(fā)生的異常综芥,在這個方法中丽蝎,既然拋出了異常,那么事務(wù)應(yīng)該回滾膀藐,實際卻不如此屠阻,讀者可以使用我源碼中 controller 的接口,通過 postman 測試一下额各,就會發(fā)現(xiàn)国觉,仍然是可以插入一條用戶數(shù)據(jù)的。

那么問題出在哪呢虾啦?因為 Spring Boot 默認(rèn)的事務(wù)規(guī)則是遇到運行異常(RuntimeException)和程序錯誤(Error)才會回滾麻诀。比如上面我們的例子中拋出的 RuntimeException 就沒有問題,但是拋出 SQLException 就無法回滾了傲醉。針對非運行時異常蝇闭,如果要進行事務(wù)回滾的話,可以在 @Transactional 注解中使用 rollbackFor 屬性來指定異常硬毕,比如 @Transactional(rollbackFor = Exception.class)丁眼,這樣就沒有問題了,所以在實際項目中昭殉,一定要指定異常。

3.2 異常被 ”吃“ 掉

這個標(biāo)題很搞笑藐守,異常怎么會被吃掉呢挪丢?還是回歸到現(xiàn)實項目中去,我們在處理異常時卢厂,有兩種方式乾蓬,要么拋出去,讓上一層來捕獲處理慎恒;要么把異常 try catch 掉任内,在異常出現(xiàn)的地方給處理掉。就因為有這中 try...catch融柬,所以導(dǎo)致異常被 ”吃“ 掉死嗦,事務(wù)無法回滾。我們還是看上面那個例子粒氧,只不過簡單修改一下代碼:

@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("數(shù)據(jù)庫異常");
        } catch (Exception e) {
            // 異常處理邏輯
        }
    }
}

讀者可以使用我源碼中 controller 的接口越除,通過 postman 測試一下,就會發(fā)現(xiàn),仍然是可以插入一條用戶數(shù)據(jù)摘盆,說明事務(wù)并沒有因為拋出異常而回滾翼雀。這個細節(jié)往往比上面那個坑更難以發(fā)現(xiàn),因為我們的思維很容易導(dǎo)致 try...catch 代碼的產(chǎn)生孩擂,一旦出現(xiàn)這種問題狼渊,往往排查起來比較費勁,所以我們平時在寫代碼時类垦,一定要多思考狈邑,多注意這種細節(jié),盡量避免給自己埋坑护锤。

那這種怎么解決呢官地?直接往上拋,給上一層來處理即可烙懦,千萬不要在事務(wù)中把異常自己 ”吃“ 掉驱入。

3.3 事務(wù)的范圍

事務(wù)范圍這個東西比上面兩個坑埋的更深!我之所以把這個也寫上氯析,是因為這是我之前在實際項目中遇到的亏较,該場景在這個課程中我就不模擬了,我寫一個 demo 讓大家看一下掩缓,把這個坑記住即可雪情,以后在寫代碼時,遇到并發(fā)問題你辣,就會注意這個坑了巡通,那么這節(jié)課也就有價值了。

我來寫個 demo:

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public synchronized void isertUser4(User user) {
        // 實際中的具體業(yè)務(wù)……
        userMapper.insertUser(user);
    }
}

可以看到舍哄,因為要考慮并發(fā)問題宴凉,我在業(yè)務(wù)層代碼的方法上加了個 synchronized 關(guān)鍵字。我舉個實際的場景表悬,比如一個數(shù)據(jù)庫中弥锄,針對某個用戶,只有一條記錄蟆沫,下一個插入動作過來然想,會先判斷該數(shù)據(jù)庫中有沒有相同的用戶叼丑,如果有就不插入悠菜,就更新轧膘,沒有才插入,所以理論上但绕,數(shù)據(jù)庫中永遠就一條同一用戶信息救崔,不會出現(xiàn)同一數(shù)據(jù)庫中插入了兩條相同用戶的信息惶看。

但是在壓測時,就會出現(xiàn)上面的問題六孵,數(shù)據(jù)庫中確實有兩條同一用戶的信息纬黎,分析其原因,在于事務(wù)的范圍和鎖的范圍問題劫窒。

從上面方法中可以看到本今,方法上是加了事務(wù)的,那么也就是說主巍,在執(zhí)行該方法開始時冠息,事務(wù)啟動,執(zhí)行完了后孕索,事務(wù)關(guān)閉逛艰。但是 synchronized 沒有起作用,其實根本原因是因為事務(wù)的范圍比鎖的范圍大搞旭。也就是說散怖,在加鎖的那部分代碼執(zhí)行完之后,鎖釋放掉了肄渗,但是事務(wù)還沒結(jié)束镇眷,此時另一個線程進來了,事務(wù)沒結(jié)束的話翎嫡,第二個線程進來時欠动,數(shù)據(jù)庫的狀態(tài)和第一個線程剛進來是一樣的。即由于mysql Innodb引擎的默認(rèn)隔離級別是可重復(fù)讀(在同一個事務(wù)里惑申,SELECT的結(jié)果是事務(wù)開始時時間點的狀態(tài))具伍,線程二事務(wù)開始的時候,線程一還沒提交完成圈驼,導(dǎo)致讀取的數(shù)據(jù)還沒更新沿猜。第二個線程也做了插入動作,導(dǎo)致了臟數(shù)據(jù)碗脊。

這個問題可以避免,第一橄妆,把事務(wù)去掉即可(不推薦)衙伶;第二,在調(diào)用該 service 的地方加鎖害碾,保證鎖的范圍比事務(wù)的范圍大即可矢劲。

4. 總結(jié)

本章主要總結(jié)了 Spring Boot 中如何使用事務(wù),只要使用 @Transactional 注解即可使用慌随,非常簡單方便芬沉。除此之外躺同,重點總結(jié)了三個在實際項目中可能遇到的坑點,這非常有意義丸逸,因為事務(wù)這東西不出問題還好蹋艺,出了問題比較難以排查,所以總結(jié)的這三點注意事項黄刚,希望能幫助到開發(fā)中的朋友捎谨。

課程源代碼下載地址:戳我下載

第12課:Spring Boot中使用監(jiān)聽器

1. 監(jiān)聽器介紹

什么是 web 監(jiān)聽器?web 監(jiān)聽器是一種 Servlet 中特殊的類憔维,它們能幫助開發(fā)者監(jiān)聽 web 中特定的事件涛救,比如 ServletContext, HttpSession, ServletRequest 的創(chuàng)建和銷毀;變量的創(chuàng)建业扒、銷毀和修改等检吆。可以在某些動作前后增加處理程储,實現(xiàn)監(jiān)控蹭沛。

2. Spring Boot中監(jiān)聽器的使用

web 監(jiān)聽器的使用場景很多,比如監(jiān)聽 servlet 上下文用來初始化一些數(shù)據(jù)虱肄、監(jiān)聽 http session 用來獲取當(dāng)前在線的人數(shù)致板、監(jiān)聽客戶端請求的 servlet request 對象來獲取用戶的訪問信息等等。這一節(jié)中咏窿,我們主要通過這三個實際的使用場景來學(xué)習(xí)一下 Spring Boot 中監(jiān)聽器的使用斟或。

2.1 監(jiān)聽Servlet上下文對象

監(jiān)聽 servlet 上下文對象可以用來初始化數(shù)據(jù),用于緩存集嵌。什么意思呢萝挤?我舉一個很常見的場景,比如用戶在點擊某個站點的首頁時根欧,一般都會展現(xiàn)出首頁的一些信息怜珍,而這些信息基本上或者大部分時間都保持不變的,但是這些信息都是來自數(shù)據(jù)庫凤粗。如果用戶的每次點擊酥泛,都要從數(shù)據(jù)庫中去獲取數(shù)據(jù)的話,用戶量少還可以接受嫌拣,如果用戶量非常大的話柔袁,這對數(shù)據(jù)庫也是一筆很大的開銷。

針對這種首頁數(shù)據(jù)异逐,大部分都不常更新的話捶索,我們完全可以把它們緩存起來,每次用戶點擊的時候灰瞻,我們都直接從緩存中拿腥例,這樣既可以提高首頁的訪問速度辅甥,又可以降低服務(wù)器的壓力。如果做的更加靈活一點燎竖,可以再加個定時器璃弄,定期的來更新這個首頁緩存。就類似與 CSDN 個人博客首頁中排名的變化一樣底瓣。

下面我們針對這個功能谢揪,來寫一個 demo,在實際中捐凭,讀者可以完全套用該代碼拨扶,來實現(xiàn)自己項目中的相關(guān)邏輯。首先寫一個 Service茁肠,模擬一下從數(shù)據(jù)庫查詢數(shù)據(jù):

@Service
public class UserService {

    /**
     * 獲取用戶信息
     * @return
     */
    public User getUser() {
        // 實際中會根據(jù)具體的業(yè)務(wù)場景患民,從數(shù)據(jù)庫中查詢對應(yīng)的信息
        return new User(1L, "倪升武", "123456");
    }
}

然后寫一個監(jiān)聽器,實現(xiàn) ApplicationListener<ContextRefreshedEvent> 接口垦梆,重寫 onApplicationEvent 方法匹颤,將 ContextRefreshedEvent 對象傳進去。如果我們想在加載或刷新應(yīng)用上下文時托猩,也重新刷新下我們預(yù)加載的資源印蓖,就可以通過監(jiān)聽 ContextRefreshedEvent 來做這樣的事情。如下:

/**
 * 使用ApplicationListener來初始化一些數(shù)據(jù)到application域中的監(jiān)聽器
 * @author shengni ni
 * @date 2018/07/05
 */
@Component
public class MyServletContextListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        // 先獲取到application上下文
        ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
        // 獲取對應(yīng)的service
        UserService userService = applicationContext.getBean(UserService.class);
        User user = userService.getUser();
        // 獲取application域?qū)ο缶┬龋瑢⒉榈降男畔⒎诺絘pplication域中
        ServletContext application = applicationContext.getBean(ServletContext.class);
        application.setAttribute("user", user);
    }
}

正如注釋中描述的一樣赦肃,首先通過 contextRefreshedEvent 來獲取 application 上下文,再通過 application 上下文來獲取 UserService 這個 bean公浪,項目中可以根據(jù)實際業(yè)務(wù)場景他宛,也可以獲取其他的 bean,然后再調(diào)用自己的業(yè)務(wù)代碼獲取相應(yīng)的數(shù)據(jù)欠气,最后存儲到 application 域中厅各,這樣前端在請求相應(yīng)數(shù)據(jù)的時候,我們就可以直接從 application 域中獲取信息预柒,減少數(shù)據(jù)庫的壓力队塘。下面寫一個 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 信息,那么說明數(shù)據(jù)已經(jīng)緩存成功顾翼。不過 application 這種是緩存在內(nèi)存中,對內(nèi)存會有消耗奈泪,后面的課程中我會講到 redis适贸,到時候再給大家介紹一下 redis 的緩存灸芳。

2.2 監(jiān)聽HTTP會話 Session對象

監(jiān)聽器還有一個比較常用的地方就是用來監(jiān)聽 session 對象,來獲取在線用戶數(shù)量拜姿,現(xiàn)在有很多開發(fā)者都有自己的網(wǎng)站烙样,監(jiān)聽 session 來獲取當(dāng)前在下用戶數(shù)量是個很常見的使用場景,下面來介紹一下如何來使用蕊肥。

/**
 * 使用HttpSessionListener統(tǒng)計在線用戶數(shù)的監(jiān)聽器
 * @author shengwu ni
 * @date 2018/07/05
 */
@Component
public class MyHttpSessionListener implements HttpSessionListener {

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

    /**
     * 記錄在線的用戶數(shù)量
     */
    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);
    }
}

可以看出谒获,首先該監(jiān)聽器需要實現(xiàn) HttpSessionListener 接口,然后重寫 sessionCreatedsessionDestroyed 方法壁却,在 sessionCreated 方法中傳遞一個 HttpSessionEvent 對象批狱,然后將當(dāng)前 session 中的用戶數(shù)量加1,sessionDestroyed 方法剛好相反展东,不再贅述赔硫。然后我們寫一個 Controller 來測試一下。

@RestController
@RequestMapping("/listener")
public class TestController {

    /**
     * 獲取當(dāng)前在線人數(shù)盐肃,該方法有bug
     * @param request
     * @return
     */
    @GetMapping("/total")
    public String getTotalUser(HttpServletRequest request) {
        Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
        return "當(dāng)前在線人數(shù):" + count;
    }
}

該 Controller 中是直接獲取當(dāng)前 session 中的用戶數(shù)量爪膊,啟動服務(wù)器,在瀏覽器中輸入 localhost:8080/listener/total 可以看到返回的結(jié)果是1砸王,再打開一個瀏覽器推盛,請求相同的地址可以看到 count 是 2 ,這沒有問題谦铃。但是如果關(guān)閉一個瀏覽器再打開耘成,理論上應(yīng)該還是2,但是實際測試卻是 3荷辕。原因是 session 銷毀的方法沒有執(zhí)行(可以在后臺控制臺觀察日志打印情況)凿跳,當(dāng)重新打開時,服務(wù)器找不到用戶原來的 session疮方,于是又重新創(chuàng)建了一個 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("/");
        //設(shè)置cookie有效期為2天骡显,設(shè)置長一點
        cookie.setMaxAge( 48*60 * 60);
        response.addCookie(cookie);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }
    Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
    return "當(dāng)前在線人數(shù):" + count;
}

可以看出疆栏,該處理邏輯是讓服務(wù)器記得原來那個 session,即把原來的 sessionId 記錄在瀏覽器中惫谤,下次再打開時壁顶,把這個 sessionId 傳過去,這樣服務(wù)器就不會重新再創(chuàng)建了溜歪。重啟一下服務(wù)器若专,在瀏覽器中再次測試一下,即可避免上面的問題蝴猪。

2.3 監(jiān)聽客戶端請求Servlet Request對象

使用監(jiān)聽器獲取用戶的訪問信息比較簡單调衰,實現(xiàn) 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數(shù)據(jù):" + request.getAttribute("name"));
    return "success";
}

3. Spring Boot中自定義事件監(jiān)聽

在實際項目中趋箩,我們往往需要自定義一些事件和監(jiān)聽器來滿足業(yè)務(wù)場景赃额,比如在微服務(wù)中會有這樣的場景:微服務(wù) A 在處理完某個邏輯之后,需要通知微服務(wù) B 去處理另一個邏輯叫确,或者微服務(wù) A 處理完某個邏輯之后跳芳,需要將數(shù)據(jù)同步到微服務(wù) B,這種場景非常普遍启妹,這個時候筛严,我們可以自定義事件以及監(jiān)聽器來監(jiān)聽,一旦監(jiān)聽到微服務(wù) A 中的某事件發(fā)生饶米,就去通知微服務(wù) B 處理對應(yīng)的邏輯桨啃。

3.1 自定義事件

自定義事件需要繼承 ApplicationEvent 對象,在事件中定義一個 User 對象來模擬數(shù)據(jù)檬输,構(gòu)造方法中將 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 自定義監(jiān)聽器

接下來丧慈,自定義一個監(jiān)聽器來監(jiān)聽上面定義的 MyEvent 事件析命,自定義監(jiān)聽器需要實現(xiàn) ApplicationListener 接口即可。如下:

/**
 * 自定義監(jiān)聽器逃默,監(jiān)聽MyEvent事件
 * @author shengwu ni
 * @date 2018/07/05
 */
@Component
public class MyEventListener implements ApplicationListener<MyEvent> {
    @Override
    public void onApplicationEvent(MyEvent myEvent) {
        // 把事件中的信息獲取到
        User user = myEvent.getUser();
        // 處理事件鹃愤,實際項目中可以通知別的微服務(wù)或者處理其他邏輯等等
        System.out.println("用戶名:" + user.getUsername());
        System.out.println("密碼:" + user.getPassword());

    }
}

然后重寫 onApplicationEvent 方法,將自定義的 MyEvent 事件傳進來完域,因為該事件中软吐,我們定義了 User 對象(該對象在實際中就是需要處理的數(shù)據(jù),在下文來模擬)吟税,然后就可以使用該對象的信息了凹耙。

OK,定義好了事件和監(jiān)聽器之后肠仪,需要手動發(fā)布事件肖抱,這樣監(jiān)聽器才能監(jiān)聽到,這需要根據(jù)實際業(yè)務(wù)場景來觸發(fā)异旧,針對本文的例子意述,我寫個觸發(fā)邏輯,如下:

/**
 * UserService
 * @author shengwu ni
 */
@Service
public class UserService {

    @Resource
    private ApplicationContext applicationContext;

    /**
     * 發(fā)布事件
     * @return
     */
    public User getUser2() {
        User user = new User(1L, "倪升武", "123456");
        // 發(fā)布事件
        MyEvent event = new MyEvent(this, user);
        applicationContext.publishEvent(event);
        return user;
    }
}

在 service 中注入 ApplicationContext,在業(yè)務(wù)代碼處理完之后荤崇,通過 ApplicationContext 對象手動發(fā)布 MyEvent 事件镐依,這樣我們自定義的監(jiān)聽器就能監(jiān)聽到,然后處理監(jiān)聽器中寫好的業(yè)務(wù)邏輯天试。

最后,在 Controller 中寫一個接口來測試一下:

@GetMapping("/request")
public String getRequestInfo(HttpServletRequest request) {
    System.out.println("requestListener中的初始化的name數(shù)據(jù):" + request.getAttribute("name"));
    return "success";
}

在瀏覽器中輸入 http://localhost:8080/listener/publish然低,然后觀察一下控制臺打印的用戶名和密碼喜每,即可說明自定義監(jiān)聽器已經(jīng)生效。

4. 總結(jié)

本課系統(tǒng)的介紹了監(jiān)聽器原理雳攘,以及在 Spring Boot 中如何使用監(jiān)聽器带兜,列舉了監(jiān)聽器的三個常用的案例,有很好的實戰(zhàn)意義吨灭。最后講解了項目中如何自定義事件和監(jiān)聽器刚照,并結(jié)合微服務(wù)中常見的場景,給出具體的代碼模型喧兄,均能運用到實際項目中去无畔,希望讀者認(rèn)真消化。

課程源代碼下載地址:戳我下載

第13課:Spring Boot中使用攔截器

攔截器的原理很簡單吠冤,是 AOP 的一種實現(xiàn)浑彰,專門攔截對動態(tài)資源的后臺請求,即攔截對控制層的請求拯辙。使用場景比較多的是判斷用戶是否有權(quán)限請求后臺郭变,更拔高一層的使用場景也有,比如攔截器可以結(jié)合 websocket 一起使用涯保,用來攔截 websocket 請求诉濒,然后做相應(yīng)的處理等等。攔截器不會攔截靜態(tài)資源夕春,Spring Boot 的默認(rèn)靜態(tài)目錄為 resources/static未荒,該目錄下的靜態(tài)頁面、js撇他、css茄猫、圖片等等,不會被攔截(也要看如何實現(xiàn)困肩,有些情況也會攔截,我在下文會指出)勇劣。

1. 攔截器的快速使用

使用攔截器很簡單,只需要兩步即可:定義攔截器和配置攔截器幻捏。在配置攔截器中命咐,Spring Boot 2.0 以后的版本和之前的版本有所不同醋奠,我會重點講解一下這里可能出現(xiàn)的坑榛臼。

1.1 定義攔截器

定義攔截器,只需要實現(xiàn) HandlerInterceptor 接口窜司,HandlerInterceptor 接口是所有自定義攔截器或者 Spring Boot 提供的攔截器的鼻祖沛善,所以,首先來了解下該接口塞祈。該接口中有三個方法: preHandle(……)金刁、postHandle(……)afterCompletion(……)

preHandle(……) 方法:該方法的執(zhí)行時機是议薪,當(dāng)某個 url 已經(jīng)匹配到對應(yīng)的 Controller 中的某個方法尤蛮,且在這個方法執(zhí)行之前。所以 preHandle(……) 方法可以決定是否將請求放行笙蒙,這是通過返回值來決定的抵屿,返回 true 則放行,返回 false 則不會向后執(zhí)行捅位。
postHandle(……) 方法:該方法的執(zhí)行時機是轧葛,當(dāng)某個 url 已經(jīng)匹配到對應(yīng)的 Controller 中的某個方法,且在執(zhí)行完了該方法艇搀,但是在 DispatcherServlet 視圖渲染之前尿扯。所以在這個方法中有個 ModelAndView 參數(shù),可以在此做一些修改動作焰雕。
afterCompletion(……) 方法:顧名思義衷笋,該方法是在整個請求處理完成后(包括視圖渲染)執(zhí)行,這時做一些資源的清理工作辟宗,這個方法只有在 preHandle(……) 被成功執(zhí)行后并且返回 true 才會被執(zhí)行。

了解了該接口容客,接下來自定義一個攔截器缩挑。

/**
 * 自定義攔截器
 * @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("====攔截到了方法:{}谨湘,在該方法執(zhí)行之前執(zhí)行====", methodName);
        // 返回true才會繼續(xù)執(zhí)行,返回false則取消當(dāng)前請求
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        logger.info("執(zhí)行完方法之后進執(zhí)行(Controller方法調(diào)用之后),但是此時還沒進行視圖渲染");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        logger.info("整個請求都處理完咯赤拒,DispatcherServlet也渲染了對應(yīng)的視圖咯挎挖,此時我可以做一些清理的工作了");
    }
}

OK,到此為止始衅,攔截器已經(jīng)定義完成,接下來就是對該攔截器進行攔截配置诸老。

1.2 配置攔截器

在 Spring Boot 2.0 之前,我們都是直接繼承 WebMvcConfigurerAdapter 類忧额,然后重寫 addInterceptors 方法來實現(xiàn)攔截器的配置轴脐。但是在 Spring Boot 2.0 之后恬涧,該方法已經(jīng)被廢棄了(當(dāng)然,也可以繼續(xù)用)提揍,取而代之的是 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";
    }
}

讓其跳轉(zhuǎn)到 hello.html 頁面,直接在 hello.html 中輸出 hello interceptor 即可秘血。啟動項目灰粮,在瀏覽器中輸入 localhost:8080/interceptor/test 看一下控制臺的日志:

====攔截到了方法:test粘舟,在該方法執(zhí)行之前執(zhí)行====  
執(zhí)行完方法之后進執(zhí)行(Controller方法調(diào)用之后)霞揉,但是此時還沒進行視圖渲染  
整個請求都處理完咯,DispatcherServlet也渲染了對應(yīng)的視圖咯秽荞,此時我可以做一些清理的工作了

可以看出攔截器已經(jīng)生效,并能看出其執(zhí)行順序胁住。

1.3 解決靜態(tài)資源被攔截問題

上文中已經(jīng)介紹了攔截器的定義和配置娱挨,但是這樣是否就沒問題了呢?其實不然柴钻,如果使用上面這種配置的話,我們會發(fā)現(xiàn)一個缺陷毫蚓,那就是靜態(tài)資源被攔截了◆娓牛可以在 resources/static/ 目錄下放置一個圖片資源或者 html 文件牍鞠,然后啟動項目直接訪問,即可看到無法訪問的現(xiàn)象龄广。

也就是說,雖然 Spring Boot 2.0 廢棄了WebMvcConfigurerAdapter,但是 WebMvcConfigurationSupport 又會導(dǎo)致默認(rèn)的靜態(tài)資源被攔截紧武,這就需要我們手動將靜態(tài)資源放開阻星。

如何放開呢?除了在 MyInterceptorConfig 配置類中重寫 addInterceptors 方法外,還需要再重寫一個方法:addResourceHandlers,將靜態(tài)資源放開:

/**
 * 用來指定靜態(tài)資源不被攔截贝搁,否則繼承WebMvcConfigurationSupport這種方式會導(dǎo)致靜態(tài)資源無法直接訪問
 * @param registry
 */
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
    super.addResourceHandlers(registry);
}

這樣配置好之后芽偏,重啟項目雷逆,靜態(tài)資源也可以正常訪問了。如果你是個善于學(xué)習(xí)或者研究的人污尉,那肯定不會止步于此膀哲,沒錯往产,上面這種方式的確能解決靜態(tài)資源無法訪問的問題,但是仿村,還有更方便的方式來配置壶谒。

我們不繼承 WebMvcConfigurationSupport 類,直接實現(xiàn) WebMvcConfigurer 接口,然后重寫 addInterceptors 方法,將自定義的攔截器添加進去即可,如下:

@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 實現(xiàn)WebMvcConfigurer不會導(dǎo)致靜態(tài)資源被攔截
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
    }
}

這樣就非常方便了拆魏,實現(xiàn) WebMvcConfigure 接口的話,不會攔截 Spring Boot 默認(rèn)的靜態(tài)資源随闪。

這兩種方式都可以,具體他們之間的細節(jié)梯浪,感興趣的讀者可以做進一步的研究励堡,由于這兩種方式的不同,繼承 WebMvcConfigurationSupport 類的方式可以用在前后端分離的項目中,后臺不需要訪問靜態(tài)資源(就不需要放開靜態(tài)資源了)秕重;實現(xiàn) WebMvcConfigure 接口的方式可以用在非前后端分離的項目中,因為需要讀取一些圖片、css雏节、js文件等等媚狰。

2. 攔截器使用實例

2.1 判斷用戶有沒有登錄

一般用戶登錄功能我們可以這么做,要么往 session 中寫一個 user拓轻,要么針對每個 user 生成一個 token帕膜,第二種要更好一點寺董,那么針對第二種方式,如果用戶登錄成功了处坪,每次請求的時候都會帶上該用戶的 token,如果未登錄架专,則沒有該 token同窘,服務(wù)端可以檢測這個 token 參數(shù)的有無來判斷用戶有沒有登錄,從而實現(xiàn)攔截功能部脚。我們改造一下 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("====攔截到了方法:{},在該方法執(zhí)行之前執(zhí)行====", methodName);

    // 判斷用戶有沒有登陸委刘,一般登陸之后的用戶都有一個對應(yīng)的token
    String token = request.getParameter("token");
    if (null == token || "".equals(token)) {
        logger.info("用戶未登錄丧没,沒有權(quán)限執(zhí)行……請登錄");
        return false;
    }

    // 返回true才會繼續(xù)執(zhí)行,返回false則取消當(dāng)前請求
    return true;
}

重啟項目锡移,在瀏覽器中輸入 localhost:8080/interceptor/test 后查看控制臺日志呕童,發(fā)現(xiàn)被攔截,如果在瀏覽器中輸入 localhost:8080/interceptor/test?token=123 即可正常往下走淆珊。

2.2 取消攔截操作

根據(jù)上文夺饲,如果我要攔截所有 /admin 開頭的 url 請求的話,需要在攔截器配置中添加這個前綴施符,但是在實際項目中往声,可能會有這種場景出現(xiàn):某個請求也是 /admin 開頭的,但是不能攔截戳吝,比如 /admin/login 等等浩销,這樣的話又需要去配置。那么骨坑,可不可以做成一個類似于開關(guān)的東西撼嗓,哪里不需要攔截,我就在哪里弄個開關(guān)上去欢唾,做成這種靈活的可插拔的效果呢且警?

是可以的,我們可以定義一個注解礁遣,該注解專門用來取消攔截操作斑芜,如果某個 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("====攔截到了方法:{}沸呐,在該方法執(zhí)行之前執(zhí)行====", methodName);

    // 通過方法醇王,可以獲取該方法上的自定義注解,然后通過注解來判斷該方法是否要被攔截
    // @UnInterception 是我們自定義的注解
    UnInterception unInterception = method.getAnnotation(UnInterception.class);
    if (null != unInterception) {
        return true;
    }
    // 返回true才會繼續(xù)執(zhí)行崭添,返回false則取消當(dāng)前請求
    return true;
}

Controller 中的方法代碼可以參見源碼寓娩,重啟項目在瀏覽器中輸入 http://localhost:8080/interceptor/test2?token=123 測試一下,可以看出呼渣,加了該注解的方法不會被攔截棘伴。

3. 總結(jié)

本節(jié)主要介紹了 Spring Boot 中攔截器的使用,從攔截器的創(chuàng)建屁置、配置焊夸,到攔截器對靜態(tài)資源的影響,都做了詳細的分析蓝角。Spring Boot 2.0 之后攔截器的配置支持兩種方式阱穗,可以根據(jù)實際情況選擇不同的配置方式。最后結(jié)合實際中的使用使鹅,舉了兩個常用的場景颇象,希望讀者能夠認(rèn)真消化,掌握攔截器的使用并徘。

課程源代碼下載地址:戳我下載

第14課:Spring Boot 中集成Redis

1. Redis 介紹

Redis 是一種非關(guān)系型數(shù)據(jù)庫(NoSQL)遣钳,NoSQL 是以 key-value 的形式存儲的,和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫不一樣麦乞,不一定遵循傳統(tǒng)數(shù)據(jù)庫的一些基本要求蕴茴,比如說 SQL 標(biāo)準(zhǔn),ACID 屬性姐直,表結(jié)構(gòu)等等倦淀,這類數(shù)據(jù)庫主要有以下特點:非關(guān)系型的、分布式的声畏、開源的撞叽、水平可擴展的姻成。
NoSQL 使用場景有:對數(shù)據(jù)高并發(fā)讀寫、對海量數(shù)據(jù)的高效率存儲和訪問愿棋、對數(shù)據(jù)的高可擴展性和高可用性等等科展。
Redis 的 key 可以是字符串、哈希糠雨、鏈表才睹、集合和有序集合。value 類型很多甘邀,包括 String琅攘、list、set松邪、zset坞琴。這些數(shù)據(jù)類型都支持 push/pop、add/remove逗抑、取交集和并集以及更多更豐富的操作置济,Redis 也支持各種不同方式的排序。為了保證效率锋八,數(shù)據(jù)都是在緩存在內(nèi)存中浙于,它也可以周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件中。 有了 redis 有哪些好處呢挟纱?舉個比較簡單的例子育谬,看下圖:

Redis使用場景

Redis 集群和 Mysql 是同步的空盼,首先會從 redis 中獲取數(shù)據(jù),如果 redis 掛了,再從 mysql 中獲取數(shù)據(jù)略贮,這樣網(wǎng)站就不會掛掉明场。更多關(guān)于 redis 的介紹以及使用場景减余,可以谷歌和百度累驮,在這就不贅述了。

2. Redis 安裝

本課程是在 vmvare 虛擬機中來安裝的 redis (centos 7)煎饼,學(xué)習(xí)的時候如果有自己的阿里云服務(wù)器讹挎,也可以在阿里云中來安裝 redis,都可以吆玖。只要能 ping 的通云主機或者虛擬機的 ip筒溃,然后在虛擬機或者云主機中放行對應(yīng)的端口(或者關(guān)掉防火墻)即可訪問 redis。下面來介紹一下 redis 的安裝過程:

  • 安裝 gcc 編譯

因為后面安裝redis的時候需要編譯沾乘,所以事先得先安裝gcc編譯怜奖。阿里云主機已經(jīng)默認(rèn)安裝了 gcc翅阵,如果是自己安裝的虛擬機歪玲,那么需要先安裝一下 gcc:

yum install gcc-c++
  • 下載 redis

有兩種方式下載安裝包迁央,一種是去官網(wǎng)上下載(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/ 文件夾下,執(zhí)行 make 命令即可完成安裝募强。
【注】如果 make 失敗株灸,可以嘗試如下命令:

make MALLOC=libc
make install
  • 修改配置文件

安裝成功之后,需要修改一下配置文件擎值,包括允許接入的 ip慌烧,允許后臺執(zhí)行,設(shè)置密碼等等鸠儿。
打開 redis 配置文件:vi redis.conf
在命令模式下輸入 /bind 來查找 bind 配置屹蚊,按 n 來查找下一個,找到配置后进每,將 bind 配置成 0.0.0.0汹粤,允許任意服務(wù)器來訪問 redis,即:

bind 0.0.0.0

使用同樣的方法田晚,將 daemonize 改成 yes (默認(rèn)為 no)嘱兼,允許 redis 在后臺執(zhí)行。
將 requirepass 注釋打開贤徒,并設(shè)置密碼為 123456(密碼自己設(shè)置)芹壕。

  • 啟動 redis

在 redis-3.2.8 目錄下,指定剛剛修改好的配置文件 redis.conf 來啟動 redis:

redis-server ./redis.conf

再啟動 redis 客戶端:

redis-cli

由于我們設(shè)置了密碼接奈,在啟動客戶端之后踢涌,輸入 auth 123456 即可登錄進入客戶端。
然后我們來測試一下序宦,往 redis 中插入一個數(shù)據(jù):

set name CSDN

然后來獲取 name

get name

如果正常獲取到 CSDN睁壁,則說明沒有問題。

3. Spring Boot 集成 Redis

3.1 依賴導(dǎo)入

Spring Boot 集成 redis 很方便互捌,只需要導(dǎo)入一個 redis 的 starter 依賴即可堡僻。如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--阿里巴巴fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.35</version>
</dependency>

這里也導(dǎo)入阿里巴巴的 fastjson 是為了在后面我們要存一個實體,為了方便把實體轉(zhuǎn)換成 json 字符串存進去疫剃。

3.2 Redis 配置

導(dǎo)入了依賴之后钉疫,我們在 application.yml 文件里配置 redis:

server:
  port: 8080
spring:
  #redis相關(guān)配置
  redis:
    database: 5
    # 配置redis的主機地址,需要修改成自己的
    host: 192.168.48.190
    port: 6379
    password: 123456
    timeout: 5000
    jedis:
      pool:
        # 連接池中的最大空閑連接巢价,默認(rèn)值也是8牲阁。
        max-idle: 500
        # 連接池中的最小空閑連接固阁,默認(rèn)值也是0。
        min-idle: 50
        # 如果賦值為-1城菊,則表示不限制备燃;如果pool已經(jīng)分配了maxActive個jedis實例,則此時pool的狀態(tài)為exhausted(耗盡)
        max-active: 1000
        # 等待可用連接的最大時間凌唬,單位毫秒并齐,默認(rèn)值為-1,表示永不超時客税。如果超過等待時間况褪,則直接拋出JedisConnectionException
        max-wait: 2000

3.3 常用 api 介紹

Spring Boot 對 redis 的支持已經(jīng)非常完善了,豐富的 api 已經(jīng)足夠我們?nèi)粘5拈_發(fā)更耻,這里我介紹幾個最常用的供大家學(xué)習(xí)测垛,其他 api 希望大家自己多學(xué)習(xí),多研究秧均。用到會去查即可食侮。

有兩個 redis 模板:RedisTemplate 和 StringRedisTemplate。我們不使用 RedisTemplate目胡,RedisTemplate 提供給我們操作對象锯七,操作對象的時候,我們通常是以 json 格式存儲誉己,但在存儲的時候起胰,會使用 Redis 默認(rèn)的內(nèi)部序列化器;導(dǎo)致我們存進里面的是亂碼之類的東西巫延。當(dāng)然了效五,我們可以自己定義序列化,但是比較麻煩炉峰,所以使用 StringRedisTemplate 模板畏妖。StringRedisTemplate 主要給我們提供字符串操作,我們可以將實體類等轉(zhuǎn)成 json 字符串即可疼阔,在取出來后戒劫,也可以轉(zhuǎn)成相應(yīng)的對象,這就是上面我導(dǎo)入了阿里 fastjson 的原因婆廊。

3.3.1 redis:string 類型

新建一個 RedisService迅细,注入 StringRedisTemplate,使用 stringRedisTemplate.opsForValue() 可以獲取 ValueOperations<String, String> 對象淘邻,通過該對象即可讀寫 redis 數(shù)據(jù)庫了茵典。如下:

public class RedisService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * set redis: string類型
     * @param key key
     * @param value value
     */
    public void setString(String key, String value){
        ValueOperations<String, String> 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,我們也可以存實體類宾舅,只需要將實體類轉(zhuǎn)換成 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工具轉(zhuǎn)成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<String, Object, Object> 對象哥谷。比如我們要存儲訂單信息,所有訂單信息都放在 order 下麻献,針對不同用戶的訂單實體们妥,可以通過用戶的 id 來區(qū)分,這就相當(dāng)于兩個 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<String, Object, Object> 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 沒啥兩樣破镰,只不過多了個參數(shù)餐曼,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<String, String> listOperations redis 列表對象鲜漩,該列表是個簡單的字符串列表源譬,可以支持從左側(cè)添加,也可以支持從右側(cè)添加孕似,一個列表最多包含 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<String, String> listOperations = stringRedisTemplate.opsForList();
        return listOperations.leftPush(key, value);
    }

    /**
     * get redis:list類型
     * @param key key
     * @param start start
     * @param end end
     * @return
     */
    public List<String> getList(String key, long start, long end){
        return stringRedisTemplate.opsForList().range(key, start, end);
    }
}

可以看出,這些 api 都是一樣的形式喉祭,方便記憶也方便使用养渴。具體的 api 細節(jié)我就不展開了,大家可以自己看 api 文檔泛烙。其實理卑,這些 api 根據(jù)參數(shù)和返回值也能知道它們是做什么用的。來測試一下:

@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<String> valList = redisService.getList("list",0,-1);
        for(String value :valList){
            logger.info("list中有:{}", value);
        }
    }
}

4. 總結(jié)

本節(jié)主要介紹了 redis 的使用場景蔽氨、安裝過程藐唠,以及 Spring Boot 中集成 redis 的詳細步驟。在實際項目中鹉究,通常都用 redis 作為緩存宇立,在查詢數(shù)據(jù)庫的時候,會先從 redis 中查找自赔,如果有信息妈嘹,則從 redis 中取绍妨;如果沒有蟋滴,則從數(shù)據(jù)庫中查染厅,并且同步到 redis 中,下次 redis 中就有了津函。更新和刪除也是如此肖粮,都需要同步到 redis。redis 在高并發(fā)場景下運用的很多尔苦。

課程源代碼下載地址:戳我下載

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涩馆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子允坚,更是在濱河造成了極大的恐慌魂那,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稠项,死亡現(xiàn)場離奇詭異涯雅,居然都是意外死亡,警方通過查閱死者的電腦和手機展运,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評論 3 396
  • 文/潘曉璐 我一進店門活逆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拗胜,你說我怎么就攤上這事蔗候。” “怎么了埂软?”我有些...
    開封第一講書人閱讀 165,933評論 0 356
  • 文/不壞的土叔 我叫張陵锈遥,是天一觀的道長。 經(jīng)常有香客問我勘畔,道長所灸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評論 1 295
  • 正文 為了忘掉前任炫七,我火速辦了婚禮爬立,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诉字。我一直安慰自己懦尝,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,999評論 6 393
  • 文/花漫 我一把揭開白布壤圃。 她就那樣靜靜地躺著陵霉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪伍绳。 梳的紋絲不亂的頭發(fā)上踊挠,一...
    開封第一講書人閱讀 51,775評論 1 307
  • 那天,我揣著相機與錄音,去河邊找鬼效床。 笑死睹酌,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的剩檀。 我是一名探鬼主播憋沿,決...
    沈念sama閱讀 40,474評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼沪猴!你這毒婦竟也來了辐啄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評論 0 276
  • 序言:老撾萬榮一對情侶失蹤运嗜,失蹤者是張志新(化名)和其女友劉穎壶辜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體担租,經(jīng)...
    沈念sama閱讀 45,854評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡砸民,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,007評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奋救。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岭参。...
    茶點故事閱讀 40,146評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖菠镇,靈堂內(nèi)的尸體忽然破棺而出冗荸,到底是詐尸還是另有隱情承璃,我是刑警寧澤利耍,帶...
    沈念sama閱讀 35,826評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站盔粹,受9級特大地震影響隘梨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舷嗡,卻給世界環(huán)境...
    茶點故事閱讀 41,484評論 3 331
  • 文/蒙蒙 一轴猎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧进萄,春花似錦捻脖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至援雇,卻和暖如春矛渴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惫搏。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評論 1 272
  • 我被黑心中介騙來泰國打工具温, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚕涤,地道東北人。 一個月前我還...
    沈念sama閱讀 48,420評論 3 373
  • 正文 我出身青樓铣猩,卻偏偏與公主長得像揖铜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子达皿,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,107評論 2 356