每當出去找工作的時候關於Spring的依賴注入一般面試官都會或多或少問一下,一般同學們回答無非就是setter方法注入或者是構造器注入,此時如果你可以回答出lookup-method與 replaced-method 方法注入相關細節,或許可以讓你在衆多面試者中脫引而出。
需求
在開發中大部分使用到的Bean對象都是單例的,如果有一單例對象依賴一多實例對象時。由於Spring容器在啓動後就初始化好了單實例對象,所以依賴的多實例對象也會進行創建好,但是這樣會造成一個問題即:單實例對象有且僅有一次機會裝配這個多實例對象。
上述問題可以自定義一個接口實現Spring的ApplicationContextAware,調用其applicationContext的getBean方法即可如下所示:
@Component
public class SpringUtils implements ApplicationContextAware {
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtils.applicationContext == null) {
SpringUtils.applicationContext = applicationContext;
}
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通過name獲取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通過class獲取Bean.
public static <T> T getBean(Class<T> clazz){
return getApplicationContext().getBean(clazz);
}
//通過name,以及Clazz返回指定的Bean
public static <T> T getBean(String name,Class<T> clazz){
return getApplicationContext().getBean(name, clazz);
}
}
但是這有一個問題就是深度依賴了Spring的接口,造成了一定程度的耦合。Spring框架爲我們提供了兩種特殊的方法lookup-method、replaced-method去解決這個問題。
lookup-method 注入
lookup-method 注入底層是依賴了CGLIB 庫提供的方法可以實現動態Bean的實現,下面是其簡單示例需求:
有一電子工廠可以生產電子產品例如手機、電腦等,電子經銷商希望每次進貨的時候都可以拿到最新生產的產品。
- 抽象工廠類
public abstract class AbstractFactory {
// 獲取商品的抽象方法
protected abstract Product getProduct();
}
- 產品類
@Data
public class Product {
private String productName;
private String productPrice;
private String productAddress;
}
- 繼承產品的手機類
public class Phone extends Product {
public Phone() {
System.out.println("生產的是手機");
}
}
- 繼承產品的電腦類
public class Computer extends Product {
public Computer() {
System.out.println("生產的是電腦");
}
}
- xml配置
<bean id="phone" class="com.codegeek.ioc.day2.lookup.Phone" scope="prototype"/>
<bean id="computer" class="com.codegeek.ioc.day2.lookup.Computer" scope="prototype"/>
<bean class="com.codegeek.ioc.day2.lookup.AbstractFactory">
<!--name屬性指定抽象工廠的抽象方法名。而bean的值即bean的id值-->
<lookup-method bean="phone" name="createProduct"></lookup-method>
</bean>
- 測試類
@Test
public void testLookup() {
// 獲取工廠
AbstractFactory bean = (AbstractFactory) applicationContext.getBean(AbstractFactory.class);
// 調用生產產品的方法
bean.createProduct();
}
輸出結果如下所示:
上述使用的xml配置進行實現,也可以使用註解去實現如下所示:
- 修改手機類與電腦類如下
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Phone extends Product {
public Phone() {
System.out.println("生產的是手機");
}
}
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Computer extends Product {
public Computer() {
System.out.println("生產的是電腦");
}
}
- 抽象工廠如下:
@Component
public abstract class AbstractFactory {
// 在指定方法上指定創建Bean的名稱即可
@Lookup("computer")
public abstract Product createProduct();
}
輸出結果如下所示:
注意事項
以上我們並沒有實現此抽象方法但是運行結果依然可以生產手機或者電腦,這是由於Spring底層使用CGLIB代理動態生成了此抽象工廠的子類以及重寫實現了其抽象方法。這裏需要注意的是代理的對象不能是final修飾,其方法也不能是final修飾。否則Spring無法使用CGLIB代理動態生成子類方法創建對象。所以一般我們將被代理的類設置爲抽象類,被代理類的方法設置爲抽象方法,而且除此之外需要注意的是一般注入的對象的scope 設置爲多實例的,否則每次生成的都是同一對象。
replace-method
replace-method是Spring 動態藉助CGLIB改變bean中的方法,通過改變方法邏輯注入對象,該方法的使用需要依賴Spring提供的MethodReplacer
接口實現。
定義一個打印輸出用戶名的方法然後使用replace-method 改變方法的輸出值
- 定義接口以及原生實現
public interface UserService {
void findUserNameById(String userId);
}
public class UserServiceImpl implements UserService {
@Override
public void findUserNameById(String userId) {
String desc = userId == "1" ? "主角" : "路人";
System.out.println(desc);
}
}
- 定義MethodReplacer實現
public class UserReplaceMethod implements MethodReplacer {
/***
*
* @Override
* public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
*
* return proxy;
* }
*/
@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("執行的方法:" + method.getName());
System.out.println("參數:"+ Arrays.stream(args).findFirst().get()) ;
System.out.println("我是MethodReplacer......替換後的方法");
return obj;
}
}
- xml中配置
<bean id="userService" class="com.codegeek.ioc.day2.replacemethod.UserServiceImpl">
<replaced-method name="findUserNameById" replacer="replaceMethod">
<arg-type>java.lang.String</arg-type>
</replaced-method>
</bean>
<!-- ====================replace-method屬性注入==================== -->
<bean id="replaceMethod" class="com.codegeek.ioc.day2.replacemethod.UserReplaceMethod"/>
- 測試
@Test
public void testReplaceMethod() {
UserService bean = applicationContext.getBean("userService",UserServiceImpl.class);
bean.findUserNameById("1");
}
輸出結果:
注意事項
看到MethodReplacer 的實現是不是感覺和JDK的InvocationHandler接口非常類似呢?其實可以知道Spring的實現也是使用了反射以及底層CGLIB的實現完成方法替換。
lookup-method與@Autowired依賴注入的區別
Autowired用於給一個單例對象注入另一個單例對象。 但是無法注入另外一個多實例對象,這是由於單例的bean只會初始化一次,所以這個多實例bean實際上可以看成是一個“單例bean”。除了可以使用applicationContext.getBean去獲取最新的實例對象,最完美的方式是使用lookup-method 完成注入,由於採用CGLIB底層動態實現類以及重寫類方法可以完美做到零耦合,開發中建議使用此種方式完成方法注入。