這是了解Spring代理機制的第一篇,嘗試了解Spring如何實現Bean的注冊和代理。這篇文章會拋出問題:Spring注冊Bean,都會用Jdk代理或cglib創建代理對象嗎?
1 項目准備
1.1 創建 Spring Boot 項目
創建一個使用 jpa 訪問數據庫的 Spring Boot 項目。
1.1.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>tech.codestory.research</groupId>
<artifactId>research-spring-boot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>research-spring-boot</name>
<properties>
<java.version>1.8</java.version>
<fastjson.version>1.2.62</fastjson.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.1.2 application.yml
src/main/resources/application.yml
server:
port: 9080
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:h2test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
platform: h2
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
properties:
hibernate:
show_sql: true
use_sql_comments: tru
h2:
console:
enabled: true
path: /console
settings:
trace: false
web-allow-others: false
logging:
level:
root: INFO
1.1.3 ResearchSpringBootApplication.java
主程序
package tech.codestory.research.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Research Spring Boot Demo Application
*
* @author javacodestory@gmail.com
*/
@SpringBootApplication
public class ResearchSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(ResearchSpringBootApplication.class, args);
}
}
1.2 監控 Spring 注冊的 Bean
為了方便了解 Spring 啟動過程,先創建一個類用於在日志中輸出生成的 Bean。可以使用 BeanPostProcessor 接口。它設計的作用,如果需要在 Spring 容器完成 Bean 的實例化、配置和其他的初始化前后添加一些自己的邏輯處理,就可以定義一個或者多個 BeanPostProcessor 接口的實現,然后注冊到容器中。
我們實現一個接口,只是打印一下注冊的 Bean 信息,代碼如下:
package tech.codestory.research.boot.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
/**
* 創建一個 BeanPostProcessor , 為了方便查看Spring 注冊的 Bean
*
* @author javacodestory@gmail.com
*/
@Component
@Slf4j
public class SpringBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
log.info("完成 初始化Bean {} : {}", beanName, bean.getClass().getName());
return bean;
}
}
啟動項目,在控制台就可以看到一些日志輸出(對輸出做了一些調整):
完成 初始化Bean dataSource : com.zaxxer.hikari.HikariDataSource 完成 初始化Bean entityManagerFactoryBuilder : org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder 完成 初始化Bean researchSpringBootApplication : tech.codestory.research.boot.ResearchSpringBootApplication$$EnhancerBySpringCGLIB$$e4d04c1b 完成 初始化Bean transactionManager : org.springframework.orm.jpa.JpaTransactionManager 完成 初始化Bean jdbcTemplate : org.springframework.jdbc.core.JdbcTemplate 完成 初始化Bean namedParameterJdbcTemplate : org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate
注意看bean:researchSpringBootApplication,實例類名中有字符串$$EnhancerBySpringCGLIB$$,這表示 Spring 使用 cglib 實現其代理類。
2 創建一個基本的 Service
在代碼中創建一個 service,觀察 Spring 注冊 Bean 的信息。
2.1 數據對象 model
package tech.codestory.research.boot.model;
import lombok.Data;
/**
* 用戶實體
*
* @author javacodestory@gmail.com
*/
@Data
public class UserInfo {
/**
* 賬號
*/
private String account;
/**
* 密碼
*/
private String password;
/**
* 姓名
*/
private String name;
}
2.2 service 接口
package tech.codestory.research.boot.service;
import tech.codestory.research.boot.model.UserInfo;
/**
* 定義 Service 接口
*
* @author javacodestory@gmail.com
*/
public interface UserInfoFirstService {
/**
* 獲取一個用戶信息
*
* @param account
* @return
*/
UserInfo getUserInfo(String account);
}
2.3 無其他注解的 service 實現
package tech.codestory.research.boot.service.impl;
import org.springframework.stereotype.Service;
import tech.codestory.research.boot.model.UserInfo;
import tech.codestory.research.boot.service.UserInfoFirstService;
/**
* 沒有添加其他注解的實現類
*
* @author javacodestory@gmail.com
*/
@Service
public class UserInfoFirstServiceImpl implements UserInfoFirstService {
/**
* 獲取一個用戶信息
*
* @param account
* @return
*/
@Override
public UserInfo getUserInfo(String account) {
return null;
}
}
2.4 查看 Bean 注冊信息
重新啟動項目,再日志中查看 Bean 注冊信息,可以看到注冊的 beanName 是 userInfoFirstServiceImpl
完成 初始化Bean userInfoFirstServiceImpl : tech.codestory.research.boot.service.impl.UserInfoFirstServiceImpl
2.5 測試 Bean 引用
2.5.1 測試類 UserInfoFirstServiceTest
注意,我在測試代碼中同時注入了 UserInfoFirstService 和 UserInfoFirstServiceImpl
package tech.codestory.research.boot.service;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import tech.codestory.research.boot.service.impl.UserInfoFirstServiceImpl;
/**
* 測試UserInfoFirstService
*
* @author javacodestory@gmail.com
*/
@SpringBootTest
@Slf4j
public class UserInfoFirstServiceTest {
@Autowired
UserInfoFirstService firstService;
@Autowired
UserInfoFirstServiceImpl firstServiceImpl;
@Test
public void testServiceInstances() {
log.info("firstService = {}", firstService);
assert firstService != null;
log.info("firstServiceImpl = {}", firstServiceImpl);
assert firstServiceImpl != null;
// 是同一個實例
log.info("firstService 和 firstServiceImpl 是同一個Bean = {}", firstService == firstServiceImpl);
assert firstService == firstServiceImpl;
}
}
2.5.2 測試結果
在項目目錄執行 maven 命令
mvn clean test
關鍵測試輸出
t.c.r.b.s.UserInfoFirstServiceTest : firstService = tech.codestory.research.boot.service.impl.UserInfoFirstServiceImpl@4899799b t.c.r.b.s.UserInfoFirstServiceTest : firstServiceImpl = tech.codestory.research.boot.service.impl.UserInfoFirstServiceImpl@4899799b t.c.r.b.s.UserInfoFirstServiceTest : firstService 和 firstServiceImpl 是同一個Bean = true [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.01 s - in tech.codestory.research.boot.service.UserInfoFirstServiceTest
從測試結果看,兩個依賴注入都正常引用了同一個對象。
2.6 問題來了
2.6.1 問題 1
通常我們理解Spring 注冊Bean,會使用JDK代理或cglib。但在本例中,注冊UserInfoFirstServiceImpl 時,為什么沒有創建代理對象?
2.6.2 問題 2
注冊 beanName 是userInfoFirstServiceImpl,為什么用接口和實現類定義變量卻都能正常注入?
【未完待續】續篇不知要到猴年馬月
敬請關注公眾號 《程序猿講故事》 codestory

