(轉)renren-fast解讀(二)


(二期)9、renren-fast項目解讀(二)

 

【課程九】jwt.xmind36.4KB

【課程九】動態數據源.xmind0.2MB

 

JWT
概要

JWT是一種用於雙方之間傳遞安全信息的簡潔的、URL安全的表述性聲明規范。JWT定義了一種簡潔的,自包含的方法用於通信雙方之間以Json對象的形式安全的傳遞信息。因為數字簽名的存在,這些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘鑰對進行簽名。

  • 簡潔(Compact): 可以通過URL,POST參數或者在HTTP header發送,因為數據量小,傳輸速度也很快
  • 自包含(Self-contained):負載中包含了所有用戶所需要的信息,避免了多次查詢數據庫
主要應用場景

身份認證在這種場景下,一旦用戶完成了登陸,在接下來的每個請求中包含JWT,可以用來驗證用戶身份以及對路由,服務和資源的訪問權限進行驗證。

 

由於它的開銷非常小,可以輕松的在不同域名的系統中傳遞,所有目前在單點登錄(SSO)中比較廣泛的使用了該技術。

 

信息交換在通信的雙方之間使用JWT對數據進行編碼是一種非常安全的方式,由於它的信息是經過簽名的,可以確保發送者發送的信息是沒有經過偽造的。

jwt消息結構

一個token分3部分,按順序為

  • 頭部(header)
  • 載荷(payload)
  • 簽證(signature)

由三部分生成token3部分之間用“.”號做分隔。

例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
頭部(header)

Jwt的頭部承載兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法 通常直接使用 HMAC SHA256
{ "alg": "HS256", "typ": "JWT"}

然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
載荷(payload)

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分

  • 標准中注冊的聲明
  • 公共的聲明
  • 私有的聲明

payload-標准中注冊的聲明 (建議但不強制使用) :

  • iss: jwt簽發者
  • sub: jwt所面向的用戶
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什么時間之前,該jwt都是不可用的.
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。

payload-公共的聲明 :

公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密。

 

payload-私有的聲明 :

私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。

 

定義一個payload:

{"name":"Free碼農","age":"28","org":"今日頭條"}

然后將其進行base64加密,得到Jwt的第二部分:

eyJvcmciOiLku4rml6XlpLTmnaEiLCJuYW1lIjoiRnJlZeeggeWGnCIsImV4cCI6MTUxNDM1NjEwMywiaWF0IjoxNTE0MzU2MDQzLCJhZ2UiOiIyOCJ9
簽證(signature)

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64后的)
  • payload (base64后的)
  • secret

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分:

49UF72vSkj-sA4aHHiYN5eoZ9Nb4w5Vb45PsLF7x_NY

密鑰secret是保存在服務端的,服務端會根據這個密鑰進行生成token和驗證,所以需要保護好。

 

說明
  • 在Web應用中,別再把JWT當做session使用,絕大多數情況下,傳統的cookie-session機制工作得更好
  • JWT適合一次性的命令認證,頒發一個有效期極短的JWT,即使暴露了危險也很小,由於每次操作都會生成新的JWT,因此也沒必要保存JWT,真正實現無狀態。

 

JWT的代碼實現

第一步、導入maven坐標

 

<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>#renren-fast用的是0.7.0版本
</dependency>

第二步、封裝一個util工具類統一頭部和載荷部分的信息,應包含生成jwt和校驗jwt。

io.renren.modules.app.utils.JwtUtils

@ConfigurationProperties(prefix = "renren.jwt")
@Component
public class JwtUtils {
    private Logger logger = LoggerFactory.getLogger(getClass());
 
          
    private String secret;
    private long expire;
    private String header;
 
          
    /**
     * 生成jwt token
     */
    public String generateToken(long userId) {
        Date nowDate = new Date();
        //過期時間
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
 
          
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId+"")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
 
          
    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            logger.debug("validate is token error ", e);
            return null;
        }
    }
    
    /**
     * token是否過期
     * @return  true:過期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
 
          
    
    ....getter、setter
}

第三步、為了區分需要攔截和不需要攔截的資源,項目添加了一個@login注解

/**
 * app登錄效驗
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017/9/23 14:30
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Login {
}

 

第四步、登錄成功后,生成一個jwt的 token,用於返回給前段。

/**
 * 登錄
 */
@PostMapping("login")
@ApiOperation("登錄")
public R login(@RequestBody LoginForm form){
    //表單校驗
    ValidatorUtils.validateEntity(form);
 
          
    //用戶登錄
    long userId = userService.login(form);
 
          
    //生成token
    String token = jwtUtils.generateToken(userId);
 
          
    Map<String, Object> map = new HashMap<>();
    map.put("token", token);
    map.put("expire", jwtUtils.getExpire());
 
          
    return R.ok(map);
}

第五步、編寫一個攔截器,攔截所有需要校驗的資源模塊的url(有加了@login注解的),訪問前校驗jwt是否合法。

/**
 * 權限(Token)驗證
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 15:38
 */
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private JwtUtils jwtUtils;
 
          
    public static final String USER_KEY = "userId";
 
          
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Login annotation;
        if(handler instanceof HandlerMethod) {
            annotation = ((HandlerMethod) handler).getMethodAnnotation(Login.class);
        }else{
            return true;
        }
 
          
        if(annotation == null){
            return true;
        }
 
          
        //獲取用戶憑證
        String token = request.getHeader(jwtUtils.getHeader());
        if(StringUtils.isBlank(token)){
            token = request.getParameter(jwtUtils.getHeader());
        }
 
          
        //憑證為空
        if(StringUtils.isBlank(token)){
            throw new RRException(jwtUtils.getHeader() + "不能為空", HttpStatus.UNAUTHORIZED.value());
        }
 
          
        Claims claims = jwtUtils.getClaimByToken(token);
        if(claims == null || jwtUtils.isTokenExpired(claims.getExpiration())){
            throw new RRException(jwtUtils.getHeader() + "失效,請重新登錄", HttpStatus.UNAUTHORIZED.value());
        }
 
          
        //設置userId到request里,后續根據userId,獲取用戶信息
        request.setAttribute(USER_KEY, Long.parseLong(claims.getSubject()));
 
          
        return true;
    }
}

 

獲取用戶信息

項目用了一個一種特殊的方法來獲取用戶信息,一般我們再baseController中獲取用戶信息,但renren-fast使用了注解的形式,@loginUser

/**
 * 登錄用戶信息
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 20:39
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
 
          
}
 
          

運用是這樣的,參數中添加@LoginUser UserEntity user作為參數:

@RestController
@RequestMapping("/app")
@Api("APP測試接口")
public class AppTestController {
 
          
    @Login
    @GetMapping("userInfo")
    @ApiOperation("獲取用戶信息")
    public R userInfo(@LoginUser UserEntity user){
        return R.ok().put("user", user);
    }
}

然后寫一個全局解析注解的類:其中HandlerMethodArgumentResolver是用來為處理器解析參數的。

HandlerMethodArgumentResolver的接口定義如下:

(1)supportsParameter 用於判斷是否支持對某種參數的解析

(2)resolveArgument  將請求中的參數值解析為某種對象

 
          
/**
 * 有@LoginUser注解的方法參數,注入當前登錄用戶
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-03-23 22:02
 */
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Autowired
    private UserService userService;
 
          
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(UserEntity.class) && parameter.hasParameterAnnotation(LoginUser.class);
    }
 
          
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
        //獲取用戶ID
        Object object = request.getAttribute(AuthorizationInterceptor.USER_KEY, RequestAttributes.SCOPE_REQUEST);
        if(object == null){
            return null;
        }
 
          
        //獲取用戶信息
        UserEntity user = userService.selectById((Long)object);
 
          
        return user;
    }
}

最后在mvc配置中添加這個解析器

/**
 * MVC配置
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017-04-20 22:30
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
 
          
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
    }
}

 

多數據源模塊
預備知識-ThreadLocal

ThreadLocal是線程局部變量,所謂的線程局部變量,就是僅僅只能被本線程訪問,不能在線程之間進行共享訪問的變量。

關鍵抽象類-AbstractRoutingDataSource

官方注釋如下:

* Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.

大概意思是:

就是getConnection()根據查找lookup key鍵對不同目標數據源的調用,通常是通過(但不一定)某些線程綁定的事物上下文來實現。

 

通過這我們知道可以實現:

  • - 多數據源的動態切換,在程序運行時,把數據源數據源動態織入到程序中,靈活的進行數據源切換。
  • - 基於多數據源的動態切換,我們可以實現讀寫分離,這么做缺點也很明顯,無法動態的增加數據源。
邏輯思路
  • DynamicDataSource繼承AbstractRoutingDataSource類,並實現了determineCurrentLookupKey()方法。
  • 我們配置的多個數據源會放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通過afterPropertiesSet()方法將數據源分別進行復制到resolvedDataSources和resolvedDefaultDataSource中。
  • AbstractRoutingDataSource的getConnection()的方法的時候,先調用determineTargetDataSource()方法返回DataSource在進行getConnection()。
實現多數據源

步驟1,在spring boot中,增加多數據源的配置

步驟2,擴展Spring的AbstractRoutingDataSource抽象類,

AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是實現多數據

源的核心,並對該方法進行Override

步驟3,配置DataSource,指定數據源的信息

步驟4,通過注解,實現多數據源

步驟5、配置加上(exclude={DataSourceAutoConfiguration.class})

 

關於事務

只支持單庫事務,也就是說切換數據源要在開啟事務之前執行。 spring DataSourceTransactionManager進行事務管理,開啟事務,會將數據源緩存到DataSourceTransactionObject對象中進行后續的commit rollback等事務操作。

 

使用經驗

出現多數據源動態切換失敗的原因是因為在事務開啟后,數據源就不能再進行隨意切換了,也就是說,一個事務對應一個數據源。那么傳統的Spring管理事務是放在Service業務層操作的,所以更換數據源的操作要放在這個操作之前進行。也就是切換數據源操作放在Controller層,可是這樣操作會造成Controller層代碼混亂的結果。故而想到的解決方案是將事務管理在數據持久 (Dao層) 開啟,切換數據源的操作放在業務層進行操作,就可在事務開啟之前順利進行數據源切換,不會再出現切換失敗了。

一個動態數據源DEMO
第一步:做增刪改查

新建:

暫時先導入兩個包

 

因為項目還需要用到aop、mybatis plus、druid,所以先把maven包導入

<!--aop-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
 
          
<!--mybatis plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.0.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
 
          
<!--druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>

然后先使用mybatis plus做一個增刪改查例子。這里我們使用自動生成工具~

MyGenerator.java3.8KB

注意,這里使用的數據庫就是renren-fast項目的數據庫,所以sql文件在renren-fast項目中找哈。

運行MyGenerator的main方法,然后會自動生成sys_user表的一些基本service等~,然后復制到項目中,效果如下:

因為是springboot項目,所以還要手動添加掃描mapper的注解,加到springboot的啟動類上。

@MapperScan("com.example.mapper")
@SpringBootApplication
public class DatasourceDemoApplication {
 
          
    public static void main(String[] args) {
        SpringApplication.run(DatasourceDemoApplication.class, args);
    }
}

把配置文件修改成yml格式application.yml。然后添加數據庫的信息。我們測試一下生成的代碼是否有用!

# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/renren_fast
    username: root
    password: admin

編寫測試類:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DatasourceDemoApplicationTests {
 
          
    @Autowired
    SysUserService userService;
 
          
    @Test
    public void contextLoads() {
 
          
        SysUser user = userService.getById(1);
        System.out.println(user.toString());
 
          
    }
}

測試contextLoads()方法,這時候可以看到測試結果啦~~

 

以上表明:生成的代碼沒毛病,可以查數據庫了!

 

第二步:集成動態數據源模塊

首先我們先梳理一下集成邏輯。

步驟1,在spring boot中,增加多數據源的配置
步驟2,擴展Spring的AbstractRoutingDataSource抽象類,
AbstractRoutingDataSource中的抽象方法determineCurrentLookupKey是實現多數據
源的核心,並對該方法進行Override
步驟3,配置DataSource,指定數據源的信息
步驟4,通過注解,實現多數據源
步驟5、配置加上(exclude={DataSourceAutoConfiguration.class})

 

下面我們按照上面的步驟一步步集成代碼!

 

步驟一、配置多數據源的信息

首先在yml配置文件中添加數據源的信息,這里我新建了一個renren_fast2的數據庫,然后把sys_user表的id為1的記錄的名稱改為了admin222。因為我們用到的druid的連接池,所以按照格式編寫鏈接信息:如下

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driverClassName: com.mysql.jdbc.Driver
    druid:
      first:
        url: jdbc:mysql://localhost:3306/renren_fast?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
        username: root
        password: admin
      second:
        url: jdbc:mysql://localhost:3306/renren_fast2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
        username: root
        password: admin

 

新建一個datasource包。用於放數據源模塊的代碼。

然后寫一個配置類DynamicDataSourceConfig ,配置多數據源的信息,生成兩個數據源:

/**
 * 配置多數據源
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017/8/19 0:41
 */
@Configuration
public class DynamicDataSourceConfig {
 
          
    @Bean
    @ConfigurationProperties("spring.datasource.druid.first")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
 
          
    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
}

另外,第五步驟中,其實現在就可以去做了,因為我們數據源是自己生成的,所以要去掉原先springboot啟動時候自動裝配的數據源配置。為了方便記憶,我們這里也導入自己寫的配置。

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@Import({DynamicDataSourceConfig.class})

致此,第一步完成!效果:

 

步驟二、擴展Spring的AbstractRoutingDataSource抽象類,重寫lookupkey方法

/**
 * 動態數據源
 * determineCurrentLookupKey()決定使用哪個數據源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
 
          
    @Override
    protected Object determineCurrentLookupKey() {
        return null;
    }
}

 

為了方便我們使用aop注解時候的得到的lookupkey參數能傳遞到這里。所以需要建一個線程安全的ThreadLocal變量,用於傳參,避免復雜的參數傳遞過程。那么,我們獲取這個lookupkey就從這個ThreadLocal里面去獲取,所以determineCurrentLookupKey()直接返回getDataSource()。

 

/**
 * 動態數據源
 * determineCurrentLookupKey()決定使用哪個數據源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
 
          
    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }
    
    /*
     *ThreadLocal 用於提供線程局部變量,在多線程環境可以保證各個線程里的變量獨立於其它線程里的變量。
     * 也就是說 ThreadLocal 可以為每個線程創建一個【單獨的變量副本】
     * 相當於線程的 private static 類型變量。
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }
    public static String getDataSource() {
        return contextHolder.get();
    }
    public static void clearDataSource() {
        contextHolder.remove();
    }
 
          
}
 
          

但是上面步驟還不夠,因為自動裝載數據源的這個過程我們在前面已經去掉了,所以需要我們自己手動去裝配數據源的信息。調用determineCurrentLookupKey()方法的determineTargetDataSource()方法可以看到,里面有用到一些變量,比如resolvedDataSources,它是一個map,查看調用地方可以看到,它的初始化實在afterPropertiesSet()方法中,這初始化方法會用到一些變量,比如:targetDataSources、defaultTargetDataSource,然后你回發現,這個兩個參數,都是一個set方法初始化的,setTargetDataSources(Map<Object, Object> targetDataSources)、setDefaultTargetDataSource(Object defaultTargetDataSource),這兩個就是所有數據源信息,和默認數據源信息,所以我們需要手動把我們配置的數據源信息set進去。

 

 

方法很多,因為涉及到一個順序問題,調用determineCurrentLookupKey()前一定要把數據源的信息初始化化好,所以我們可以寫一個DynamicDataSource的構造方法。兩個參數,一個默認數據源,一個所有數據源。然后調用afterPropertiesSet(),初始化必要的參數。

/**
 * 決定使用哪個數據源之前需要把多個數據源的信息以及默認數據源信息配置好
 * @param defaultTargetDataSource
 * @param targetDataSources
 */
public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
    super.setDefaultTargetDataSource(defaultTargetDataSource);
    super.setTargetDataSources(targetDataSources);
    super.afterPropertiesSet();
}

因為這數據源的信息啟動時候就需要初始化,因為后面事務等類的初始化都需要依賴數據源bean,所以在DynamicDataSourceConfig配置中,我們生成一個DynamicDataSource的bean。

@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
    Map<Object, Object> targetDataSources = new HashMap<>();
    targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
    targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
    return new DynamicDataSource(firstDataSource, targetDataSources);
}

 

@Primary 優先考慮,優先考慮被注解的對象注入。

因為數據源的名稱在我們后面注解的時候經常會用到,所以我們作為一個enum常亮來用。

/**
 * 增加多數據源,在此配置
 *
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017/8/18 23:46
 */
public interface DataSourceNames {
    String FIRST = "first";
    String SECOND = "second";
 
          
}

通過上面的配置,我們可以說已經完成了手動裝配我們自定義的多數據源的過程了。接下來的工作就是我們自己去指定ThreadLocal里面的值就行。

 

我們采用aop的方式,在需要修改數據源的地方使用注解方式去切換,然后切面修改ThreadLocal的內容。這里比較簡單,我就直接貼代碼:

注解:

/**
 * 多數據源注解
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017/9/16 22:16
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String name() default "";
}
 
          

切面處理邏輯:

/**
 * 多數據源,切面處理類
 * @author chenshun
 * @email sunlightcs@gmail.com
 * @date 2017/9/16 22:20
 */
@Aspect
@Component
public class DataSourceAspect implements Ordered {
    protected Logger logger = LoggerFactory.getLogger(getClass());
 
          
    @Pointcut("@annotation(com.example.datasource.DataSource)")
    public void dataSourcePointCut() {
 
          
    }
 
          
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
 
          
        DataSource ds = method.getAnnotation(DataSource.class);
        if(ds == null){
            DynamicDataSource.setDataSource(DataSourceNames.FIRST);
            logger.debug("set datasource is " + DataSourceNames.FIRST);
        }else {
            DynamicDataSource.setDataSource(ds.name());
            logger.debug("set datasource is " + ds.name());
        }
 
          
        try {
            return point.proceed();
        } finally {
            DynamicDataSource.clearDataSource();
            logger.debug("clean datasource");
        }
    }
 
          
    @Override
    public int getOrder() {
        return 1;
    }
}

 

 

致此,完成了我們需要的所有准備工作,接下來我們只需要測試一下即可~

 

測試:

service中定義兩個查詢,分別查兩個數據庫:

public interface SysUserService extends IService<SysUser> {
 
          
    SysUser findUserByFirstDb(long id);
 
          
    SysUser findUserBySecondDb(long id);
    
}

實現類:因為默認是一,所以不用注解,數據庫二需要添加注解@DataSource(name = DataSourceNames.SECOND)。

 

@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    
    @Override
    public SysUser findUserByFirstDb(long id) {
        return this.baseMapper.selectById(id);
    }
 
          
    @DataSource(name = DataSourceNames.SECOND)
    @Override
    public SysUser findUserBySecondDb(long id) {
        return this.baseMapper.selectById(id);
    }
 
          
}

 

測試方法:test()

@RunWith(SpringRunner.class)
@SpringBootTest
public class DatasourceDemoApplicationTests {
 
          
    @Autowired
    SysUserService userService;
 
          
    @Test
    public void contextLoads() {
 
          
        SysUser user = userService.getById(1);
        System.out.println(user.toString());
 
          
    }
 
          
    @Test
    public void test() {
        SysUser user = userService.findUserByFirstDb(1);
        System.out.println("第one個數據庫---------》" + user.toString());
 
          
 
          
        SysUser user2 = userService.findUserBySecondDb(1);
        System.out.println("第二個數據庫---------》" + user2.toString());
    }
 
          
}

運行結果:

 

ok,大功告成~~~~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM