本文已經收錄到Github倉庫,該倉庫包含計算機基礎、Java基礎、多執行緒、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~
Spring的優點
- 透過控制反轉和依賴注入實現松耦合。
- 支援面向切面的程式設計,並且把應用業務邏輯和系統服務分開。
- 透過切面和模板減少樣板式程式碼。
- 宣告式事務的支援。可以從單調繁冗的事務管理程式碼中解脫出來,透過宣告式方式靈活地進行事務的管理,提高開發效率和質量。
- 方便整合各種優秀框架。內部提供了對各種優秀框架的直接支援(如:Hessian、Quartz、MyBatis等)。
- 方便程式的測試。Spring支援Junit4,新增註解便可以測試Spring程式。
Spring 用到了哪些設計模式?
1、簡單工廠模式:BeanFactory
就是簡單工廠模式的體現,根據傳入一個唯一標識來獲得 Bean 物件。
@Override
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
2、工廠方法模式:FactoryBean
就是典型的工廠方法模式。spring在使用getBean()
呼叫獲得該bean時,會自動呼叫該bean的getObject()
方法。每個 Bean 都會對應一個 FactoryBean
,如 SqlSessionFactory
對應 SqlSessionFactoryBean
。
3、單例模式:一個類僅有一個例項,提供一個訪問它的全域性訪問點。Spring 建立 Bean 例項預設是單例的。
4、介面卡模式:SpringMVC中的介面卡HandlerAdatper
。由於應用會有多個Controller實現,如果需要直接呼叫Controller方法,那麼需要先判斷是由哪一個Controller處理請求,然後呼叫相應的方法。當增加新的 Controller,需要修改原來的邏輯,違反了開閉原則(對修改關閉,對擴充套件開放)。
為此,Spring提供了一個介面卡介面,每一種 Controller 對應一種 HandlerAdapter
實現類,當請求過來,SpringMVC會呼叫getHandler()
獲取相應的Controller,然後獲取該Controller對應的 HandlerAdapter
,最後呼叫HandlerAdapter
的handle()
方法處理請求,實際上呼叫的是Controller的handleRequest()
。每次新增新的 Controller 時,只需要增加一個介面卡類就可以,無需修改原有的邏輯。
常用的處理器介面卡:SimpleControllerHandlerAdapter
,HttpRequestHandlerAdapter
,AnnotationMethodHandlerAdapter
。
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
public class HttpRequestHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {//handler是被適配的物件,這裡使用的是物件的介面卡模式
return (handler instanceof HttpRequestHandler);
}
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
}
5、代理模式:spring 的 aop 使用了動態代理,有兩種方式JdkDynamicAopProxy
和Cglib2AopProxy
。
6、觀察者模式:spring 中 observer 模式常用的地方是 listener 的實現,如ApplicationListener
。
7、模板模式: Spring 中 jdbcTemplate
、hibernateTemplate
等,就使用到了模板模式。
什麼是AOP?
面向切面程式設計,作為物件導向的一種補充,將公共邏輯(事務管理、日誌、快取等)封裝成切面,跟業務程式碼進行分離,可以減少系統的重複程式碼和降低模組之間的耦合度。切面就是那些與業務無關,但所有業務模組都會呼叫的公共邏輯。
AOP有哪些實現方式?
AOP有兩種實現方式:靜態代理和動態代理。
靜態代理
靜態代理:代理類在編譯階段生成,在編譯階段將通知織入Java位元組碼中,也稱編譯時增強。AspectJ使用的是靜態代理。
缺點:代理物件需要與目標物件實現一樣的介面,並且實現介面的方法,會有冗餘程式碼。同時,一旦介面增加方法,目標物件與代理物件都要維護。
動態代理
動態代理:代理類在程式執行時建立,AOP框架不會去修改位元組碼,而是在記憶體中臨時生成一個代理物件,在執行期間對業務方法進行增強,不會生成新類。
Spring AOP的實現原理
Spring
的AOP
實現原理其實很簡單,就是透過動態代理實現的。如果我們為Spring
的某個bean
配置了切面,那麼Spring
在建立這個bean
的時候,實際上建立的是這個bean
的一個代理物件,我們後續對bean
中方法的呼叫,實際上呼叫的是代理類重寫的代理方法。而Spring
的AOP
使用了兩種動態代理,分別是JDK的動態代理,以及CGLib的動態代理。
JDK動態代理和CGLIB動態代理的區別?
Spring AOP中的動態代理主要有兩種方式:JDK動態代理和CGLIB動態代理。
JDK動態代理
如果目標類實現了介面,Spring AOP會選擇使用JDK動態代理目標類。代理類根據目標類實現的介面動態生成,不需要自己編寫,生成的動態代理類和目標類都實現相同的介面。JDK動態代理的核心是InvocationHandler
介面和Proxy
類。
缺點:目標類必須有實現的介面。如果某個類沒有實現介面,那麼這個類就不能用JDK動態代理。
CGLIB動態代理
透過繼承實現。如果目標類沒有實現介面,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library)可以在執行時動態生成類的位元組碼,動態建立目標類的子類物件,在子類物件中增強目標類。
CGLIB是透過繼承的方式做的動態代理,因此如果某個類被標記為final
,那麼它是無法使用CGLIB做動態代理的。
優點:目標類不需要實現特定的介面,更加靈活。
什麼時候採用哪種動態代理?
- 如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP
- 如果目標物件實現了介面,可以強制使用CGLIB實現AOP
- 如果目標物件沒有實現了介面,必須採用CGLIB庫
兩者的區別:
- jdk動態代理使用jdk中的類Proxy來建立代理物件,它使用反射技術來實現,不需要匯入其他依賴。cglib需要引入相關依賴:
asm.jar
,它使用位元組碼增強技術來實現。 - 當目標類實現了介面的時候Spring Aop預設使用jdk動態代理方式來增強方法,沒有實現介面的時候使用cglib動態代理方式增強方法。
Spring AOP相關術語
(1)切面(Aspect):切面是通知和切點的結合。通知和切點共同定義了切面的全部內容。
(2)連線點(Join point):指方法,在Spring AOP中,一個連線點總是代表一個方法的執行。連線點是在應用執行過程中能夠插入切面的一個點。這個點可以是呼叫方法時、丟擲異常時、甚至修改一個欄位時。切面程式碼可以利用這些點插入到應用的正常流程之中,並新增新的行為。
(3)通知(Advice):在AOP術語中,切面的工作被稱為通知。
(4)切入點(Pointcut):切點的定義會匹配通知所要織入的一個或多個連線點。我們通常使用明確的類和方法名稱,或是利用正規表示式定義所匹配的類和方法名稱來指定這些切點。
(5)引入(Introduction):引入允許我們向現有類新增新方法或屬性。
(6)目標物件(Target Object): 被一個或者多個切面(aspect)所通知(advise)的物件。它通常是一個代理物件。
(7)織入(Weaving):織入是把切面應用到目標物件並建立新的代理物件的過程。在目標物件的生命週期裡有以下時間點可以進行織入:
- 編譯期:切面在目標類編譯時被織入。AspectJ的織入編譯器是以這種方式織入切面的。
- 類載入期:切面在目標類載入到JVM時被織入。需要特殊的類載入器,它可以在目標類被引入應用之前增強該目標類的位元組碼。AspectJ5的載入時織入就支援以這種方式織入切面。
- 執行期:切面在應用執行的某個時刻被織入。一般情況下,在織入切面時,AOP容器會為目標物件動態地建立一個代理物件。SpringAOP就是以這種方式織入切面。
Spring通知有哪些型別?
在AOP術語中,切面的工作被稱為通知。通知實際上是程式執行時要透過Spring AOP框架來觸發的程式碼段。
Spring切面可以應用5種型別的通知:
- 前置通知(Before):在目標方法被呼叫之前呼叫通知功能;
- 後置通知(After):在目標方法完成之後呼叫通知,此時不會關心方法的輸出是什麼;
- 返回通知(After-returning ):在目標方法成功執行之後呼叫通知;
- 異常通知(After-throwing):在目標方法丟擲異常後呼叫通知;
- 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法呼叫之前和呼叫之後執行自定義的邏輯。
什麼是依賴注入?
在Spring建立物件的過程中,把物件依賴的屬性注入到物件中。依賴注入主要有兩種方式:構造器注入和屬性注入。什麼是IOC?
IOC:控制反轉,由Spring容器管理bean的整個生命週期。透過反射實現對其他物件的控制,包括初始化、建立、銷燬等,解放手動建立物件的過程,同時降低類之間的耦合度。
IOC的好處?
ioc的思想最核心的地方在於,資源不由使用資源者管理,而由不使用資源的第三方管理,這可以帶來很多好處。第一,資源集中管理,實現資源的可配置和易管理。第二,降低了使用資源雙方的依賴程度,也就是我們說的耦合度。
也就是說,甲方要達成某種目的不需要直接依賴乙方,它只需要達到的目的告訴第三方機構就可以了,比如甲方需要一雙襪子,而乙方它賣一雙襪子,它要把襪子賣出去,並不需要自己去直接找到一個賣家來完成襪子的賣出。它也只需要找第三方,告訴別人我要賣一雙襪子。這下好了,甲乙雙方進行交易活動,都不需要自己直接去找賣家,相當於程式內部開放介面,賣家由第三方作為引數傳入。甲乙互相不依賴,而且只有在進行交易活動的時候,甲才和乙產生聯絡。反之亦然。這樣做什麼好處麼呢,甲乙可以在對方不真實存在的情況下獨立存在,而且保證不交易時候無聯絡,想交易的時候可以很容易的產生聯絡。甲乙交易活動不需要雙方見面,避免了雙方的互不信任造成交易失敗的問題。因為交易由第三方來負責聯絡,而且甲乙都認為第三方可靠。那麼交易就能很可靠很靈活的產生和進行了。
這就是ioc的核心思想。生活中這種例子比比皆是,支付寶在整個淘寶體系裡就是龐大的ioc容器,交易雙方之外的第三方,提供可靠性可依賴可靈活變更交易方的資源管理中心。另外人事代理也是,僱傭機構和個人之外的第三方。
參考連結:https://www.zhihu.com/question/23277575/answer/24259844
IOC容器初始化過程?
- 從XML中讀取配置檔案。
- 將bean標籤解析成 BeanDefinition,如解析 property 元素, 並注入到 BeanDefinition 例項中。
- 將 BeanDefinition 註冊到容器 BeanDefinitionMap 中。
- BeanFactory 根據 BeanDefinition 的定義資訊建立例項化和初始化 bean。
單例bean的初始化以及依賴注入一般都在容器初始化階段進行,只有懶載入(lazy-init為true)的單例bean是在應用第一次呼叫getBean()時進行初始化和依賴注入。
// AbstractApplicationContext
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
多例bean 在容器啟動時不例項化,即使設定 lazy-init 為 false 也沒用,只有呼叫了getBean()才進行例項化。
loadBeanDefinitions
採用了模板模式,具體載入 BeanDefinition
的邏輯由各個子類完成。
Bean的生命週期
1.呼叫bean的構造方法建立Bean
2.透過反射呼叫setter方法進行屬性的依賴注入
3.如果Bean實現了BeanNameAware
介面,Spring將呼叫setBeanName
(),設定 Bean
的name(xml檔案中bean標籤的id)
4.如果Bean實現了BeanFactoryAware
介面,Spring將呼叫setBeanFactory()
把bean factory設定給Bean
5.如果存在BeanPostProcessor
,Spring將呼叫它們的postProcessBeforeInitialization
(預初始化)方法,在Bean初始化前對其進行處理
6.如果Bean實現了InitializingBean
介面,Spring將呼叫它的afterPropertiesSet
方法,然後呼叫xml定義的 init-method 方法,兩個方法作用類似,都是在初始化 bean 的時候執行
7.如果存在BeanPostProcessor
,Spring將呼叫它們的postProcessAfterInitialization
(後初始化)方法,在Bean初始化後對其進行處理
8.Bean初始化完成,供應用使用,這裡分兩種情況:
8.1 如果Bean為單例的話,那麼容器會返回Bean給使用者,並存入快取池。如果Bean實現了DisposableBean
介面,Spring將呼叫它的destory
方法,然後呼叫在xml中定義的 destory-method
方法,這兩個方法作用類似,都是在Bean例項銷燬前執行。
8.2 如果Bean是多例的話,容器將Bean返回給使用者,剩下的生命週期由使用者控制。
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
public interface InitializingBean {
void afterPropertiesSet() throws Exception;
}
BeanFactory和FactoryBean的區別?
BeanFactory:管理Bean的容器,Spring中生成的Bean都是由這個介面的實現來管理的。
FactoryBean:通常是用來建立比較複雜的bean,一般的bean 直接用xml配置即可,但如果一個bean的建立過程中涉及到很多其他的bean 和複雜的邏輯,直接用xml配置比較麻煩,這時可以考慮用FactoryBean,可以隱藏例項化複雜Bean的細節。
當配置檔案中bean標籤的class屬性配置的實現類是FactoryBean時,透過 getBean()方法返回的不是FactoryBean本身,而是呼叫FactoryBean#getObject()方法所返回的物件,相當於FactoryBean#getObject()代理了getBean()方法。如果想得到FactoryBean必須使用 '&' + beanName 的方式獲取。
Mybatis 提供了 SqlSessionFactoryBean
,可以簡化 SqlSessionFactory
的配置:
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
@Override
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
//複雜邏輯
}
@Override
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
}
在 xml 配置 SqlSessionFactoryBean:
<bean id="tradeSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="trade" />
<property name="mapperLocations" value="classpath*:mapper/trade/*Mapper.xml" />
<property name="configLocation" value="classpath:mybatis-config.xml" />
<property name="typeAliasesPackage" value="com.bytebeats.mybatis3.domain.trade" />
</bean>
Spring 將會在應用啟動時建立 SqlSessionFactory
,並使用 sqlSessionFactory
這個名字儲存起來。
BeanFactory和ApplicationContext有什麼區別?
BeanFactory和ApplicationContext是Spring的兩大核心介面,都可以當做Spring的容器。其中ApplicationContext是BeanFactory的子介面。
兩者區別如下:
1、功能上的區別。BeanFactory是Spring裡面最底層的介面,包含了各種Bean的定義,讀取bean配置文件,管理bean的載入、例項化,控制bean的生命週期,維護bean之間的依賴關係。
ApplicationContext介面作為BeanFactory的派生,除了提供BeanFactory所具有的功能外,還提供了更完整的框架功能,如繼承MessageSource、支援國際化、統一的資原始檔訪問方式、同時載入多個配置檔案等功能。
2、載入方式的區別。BeanFactroy採用的是延遲載入形式來注入Bean的,即只有在使用到某個Bean時(呼叫getBean()),才對該Bean進行載入例項化。這樣,我們就不能發現一些存在的Spring的配置問題。如果Bean的某一個屬性沒有注入,BeanFacotry載入後,直至第一次使用呼叫getBean方法才會丟擲異常。
而ApplicationContext是在容器啟動時,一次性建立了所有的Bean。這樣,在容器啟動時,我們就可以發現Spring中存在的配置錯誤,這樣有利於檢查所依賴屬性是否注入。 ApplicationContext啟動後預載入所有的單例Bean,那麼在需要的時候,不需要等待建立bean,因為它們已經建立好了。
相對於基本的BeanFactory,ApplicationContext 唯一的不足是佔用記憶體空間。當應用程式配置Bean較多時,程式啟動較慢。
3、建立方式的區別。BeanFactory通常以程式設計的方式被建立,ApplicationContext還能以宣告的方式建立,如使用ContextLoader。
4、註冊方式的區別。BeanFactory和ApplicationContext都支援BeanPostProcessor、BeanFactoryPostProcessor的使用,但兩者之間的區別是:BeanFactory需要手動註冊,而ApplicationContext則是自動註冊。
Bean注入容器有哪些方式?
1、@Configuration + @Bean
@Configuration用來宣告一個配置類,然後使用 @Bean 註解,用於宣告一個bean,將其加入到Spring容器中。
@Configuration
public class MyConfiguration {
@Bean
public Person person() {
Person person = new Person();
person.setName("大彬");
return person;
}
}
2、透過包掃描特定註解的方式
@ComponentScan放置在我們的配置類上,然後可以指定一個路徑,進行掃描帶有特定註解的bean,然後加至容器中。
特定註解包括@Controller、@Service、@Repository、@Component
@Component
public class Person {
//...
}
@ComponentScan(basePackages = "com.dabin.test.*")
public class Demo1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);
Person bean = applicationContext.getBean(Person.class);
System.out.println(bean);
}
}
3、@Import註解匯入
@Import註解平時開發用的不多,但是也是非常重要的,在進行Spring擴充套件時經常會用到,它經常搭配自定義註解進行使用,然後往容器中匯入一個配置檔案。
@ComponentScan
/*把用到的資源匯入到當前容器中*/
@Import({Person.class})
public class App {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
System.out.println(context.getBean(Person.class));
context.close();
}
}
4、實現BeanDefinitionRegistryPostProcessor進行後置處理。
在Spring容器啟動的時候會執行 BeanDefinitionRegistryPostProcessor 的 postProcessBeanDefinitionRegistry 方法,就是等beanDefinition載入完畢之後,對beanDefinition進行後置處理,可以在此進行調整IOC容器中的beanDefinition,從而干擾到後面進行初始化bean。
在下面的程式碼中,我們手動向beanDefinitionRegistry中註冊了person的BeanDefinition。最終成功將person加入到applicationContext中。
public class Demo1 {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
MyBeanDefinitionRegistryPostProcessor beanDefinitionRegistryPostProcessor = new MyBeanDefinitionRegistryPostProcessor();
applicationContext.addBeanFactoryPostProcessor(beanDefinitionRegistryPostProcessor);
applicationContext.refresh();
Person bean = applicationContext.getBean(Person.class);
System.out.println(bean);
}
}
class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Person.class).getBeanDefinition();
registry.registerBeanDefinition("person", beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
}
5、使用FactoryBean介面
如下圖程式碼,使用@Configuration + @Bean的方式將 PersonFactoryBean 加入到容器中,這裡沒有向容器中直接注入 Person,而是注入 PersonFactoryBean,然後從容器中拿Person這個型別的bean。
@Configuration
public class Demo1 {
@Bean
public PersonFactoryBean personFactoryBean() {
return new PersonFactoryBean();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Demo1.class);
Person bean = applicationContext.getBean(Person.class);
System.out.println(bean);
}
}
class PersonFactoryBean implements FactoryBean<Person> {
@Override
public Person getObject() throws Exception {
return new Person();
}
@Override
public Class<?> getObjectType() {
return Person.class;
}
}
Bean的作用域
1、singleton:單例,Spring中的bean預設都是單例的。
2、prototype:每次請求都會建立一個新的bean例項。
3、request:每一次HTTP請求都會產生一個新的bean,該bean僅在當前HTTP request內有效。
4、session:每一次HTTP請求都會產生一個新的bean,該bean僅在當前HTTP session內有效。
5、global-session:全域性session作用域。
Spring自動裝配的方式有哪些?
Spring的自動裝配有三種模式:byType(根據型別),byName(根據名稱)、constructor(根據建構函式)。
byType
找到與依賴型別相同的bean注入到另外的bean中,這個過程需要藉助setter注入來完成,因此必須存在set方法,否則注入失敗。
當xml檔案中存在多個相同型別名稱不同的例項Bean時,Spring容器依賴注入仍然會失敗,因為存在多種適合的選項,Spring容器無法知道該注入那種,此時我們需要為Spring容器提供幫助,指定注入那個Bean例項。可以透過<bean>
標籤的autowire-candidate設定為false來過濾那些不需要注入的例項Bean
<bean id="userDao" class="com.zejian.spring.springIoc.dao.impl.UserDaoImpl" />
<!-- autowire-candidate="false" 過濾該型別 -->
<bean id="userDao2" autowire-candidate="false" class="com.zejian.spring.springIoc.dao.impl.UserDaoImpl" />
<!-- byType 根據型別自動裝配userDao-->
<bean id="userService" autowire="byType" class="com.zejian.spring.springIoc.service.impl.UserServiceImpl" />
byName
將屬性名與bean名稱進行匹配,如果找到則注入依賴bean。
<bean id="userDao" class="com.zejian.spring.springIoc.dao.impl.UserDaoImpl" />
<bean id="userDao2" class="com.zejian.spring.springIoc.dao.impl.UserDaoImpl" />
<!-- byName 根據名稱自動裝配,找到UserServiceImpl名為 userDao屬性並注入-->
<bean id="userService" autowire="byName" class="com.zejian.spring.springIoc.service.impl.UserServiceImpl" />
constructor
存在單個例項則優先按型別進行引數匹配(無論名稱是否匹配),當存在多個型別相同例項時,按名稱優先匹配,如果沒有找到對應名稱,則注入失敗。
@Autowired和@Resource的區別?
Autowire是spring的註解。預設情況下@Autowired是按型別匹配的(byType)。如果需要按名稱(byName)匹配的話,可以使用@Qualifier註解與@Autowired結合。@Autowired 可以傳遞一個required=false
的屬性,false指明當userDao例項存在就注入不存就忽略,如果為true,就必須注入,若userDao例項不存在,就丟擲異常。
public class UserServiceImpl implements UserService {
//標註成員變數
@Autowired
@Qualifier("userDao1")
private UserDao userDao;
}
Resource是j2ee的註解,預設按 byName模式自動注入。@Resource有兩個中重要的屬性:name和type。name屬性指定bean的名字,type屬性則指定bean的型別。因此使用name屬性,則按byName模式的自動注入策略,如果使用type屬性,則按 byType模式自動注入策略。倘若既不指定name也不指定type屬性,Spring容器將透過反射技術預設按byName模式注入。
@Resource(name="userDao")
private UserDao userDao;//用於成員變數
//也可以用於set方法標註
@Resource(name="userDao")
public void setUserDao(UserDao userDao) {
this.userDao= userDao;
}
上述兩種自動裝配的依賴注入並不適合簡單值型別,如int、boolean、long、String以及Enum等,對於這些型別,Spring容器也提供了@Value注入的方式。
@Value和@Autowired、@Resource類似,也是用來對屬性進行注入的,只不過@Value是用來從Properties檔案中來獲取值的,並且@Value可以解析SpEL(Spring表示式)。
比如,jdbc.properties檔案如下:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&allowMultiQueries=true
jdbc.username=root
jdbc.password=root
利用註解@Value獲取jdbc.url和jdbc.username的值,實現如下:
public class UserServiceImpl implements UserService {
//佔位符方式
@Value("${jdbc.url}")
private String url;
//SpEL表達方式,其中代表xml配置檔案中的id值configProperties
@Value("#{configProperties['jdbc.username']}")
private String userName;
}
@Qualifier 註解有什麼作用
當需要建立多個相同型別的 bean 並希望僅使用屬性裝配其中一個 bean 時,可以使用@Qualifier
註解和 @Autowired
透過指定應該裝配哪個 bean 來消除歧義。
@Bean和@Component有什麼區別?
都是使用註解定義 Bean。@Bean 是使用 Java 程式碼裝配 Bean,@Component 是自動裝配 Bean。
@Component 註解用在類上,表明一個類會作為元件類,並告知Spring要為這個類建立bean,每個類對應一個 Bean。
@Bean 註解用在方法上,表示這個方法會返回一個 Bean。@Bean 需要在配置類中使用,即類上需要加上@Configuration註解。
@Component
public class Student {
private String name = "lkm";
public String getName() {
return name;
}
}
@Configuration
public class WebSocketConfig {
@Bean
public Student student(){
return new Student();
}
}
@Bean 註解更加靈活。當需要將第三方類裝配到 Spring 容器中,因為沒辦法原始碼上新增@Component註解,只能使用@Bean 註解的方式,當然也可以使用 xml 的方式。
@Component、@Controller、@Repositor和@Service 的區別?
@Component:最普通的元件,可以被注入到spring容器進行管理。
@Controller:將類標記為 Spring Web MVC 控制器。
@Service:將類標記為業務層元件。
@Repository:將類標記為資料訪問元件,即DAO元件。
Spring 事務實現方式有哪些?
事務就是一系列的操作原子執行。Spring事務機制主要包括宣告式事務和程式設計式事務。
- 程式設計式事務:透過程式設計的方式管理事務,這種方式帶來了很大的靈活性,但很難維護。
- 宣告式事務:將事務管理程式碼從業務方法中分離出來,透過aop進行封裝。Spring宣告式事務使得我們無需要去處理獲得連線、關閉連線、事務提交和回滾等這些操作。使用
@Transactional
註解開啟宣告式事務。
@Transactional
相關屬性如下:
屬性 | 型別 | 描述 |
---|---|---|
value | String | 可選的限定描述符,指定使用的事務管理器 |
propagation | enum: Propagation | 可選的事務傳播行為設定 |
isolation | enum: Isolation | 可選的事務隔離級別設定 |
readOnly | boolean | 讀寫或只讀事務,預設讀寫 |
timeout | int (in seconds granularity) | 事務超時時間設定 |
rollbackFor | Class物件陣列,必須繼承自Throwable | 導致事務回滾的異常類陣列 |
rollbackForClassName | 類名陣列,必須繼承自Throwable | 導致事務回滾的異常類名字陣列 |
noRollbackFor | Class物件陣列,必須繼承自Throwable | 不會導致事務回滾的異常類陣列 |
noRollbackForClassName | 類名陣列,必須繼承自Throwable | 不會導致事務回滾的異常類名字陣列 |
有哪些事務傳播行為?
在TransactionDefinition介面中定義了七個事務傳播行為:
PROPAGATION_REQUIRED
如果存在一個事務,則支援當前事務。如果沒有事務則開啟一個新的事務。如果巢狀呼叫的兩個方法都加了事務註解,並且執行在相同執行緒中,則這兩個方法使用相同的事務中。如果執行在不同執行緒中,則會開啟新的事務。PROPAGATION_SUPPORTS
如果存在一個事務,支援當前事務。如果沒有事務,則非事務的執行。PROPAGATION_MANDATORY
如果已經存在一個事務,支援當前事務。如果不存在事務,則丟擲異常IllegalTransactionStateException
。PROPAGATION_REQUIRES_NEW
總是開啟一個新的事務。需要使用JtaTransactionManager作為事務管理器。PROPAGATION_NOT_SUPPORTED
總是非事務地執行,並掛起任何存在的事務。需要使用JtaTransactionManager作為事務管理器。PROPAGATION_NEVER
總是非事務地執行,如果存在一個活動事務,則丟擲異常。PROPAGATION_NESTED
如果一個活動的事務存在,則執行在一個巢狀的事務中。如果沒有活動事務, 則按PROPAGATION_REQUIRED 屬性執行。
PROPAGATION_NESTED 與PROPAGATION_REQUIRES_NEW的區別:
使用PROPAGATION_REQUIRES_NEW
時,內層事務與外層事務是兩個獨立的事務。一旦內層事務進行了提交後,外層事務不能對其進行回滾。兩個事務互不影響。
使用PROPAGATION_NESTED
時,外層事務的回滾可以引起內層事務的回滾。而內層事務的異常並不會導致外層事務的回滾,它是一個真正的巢狀事務。
Spring事務在什麼情況下會失效?
1.訪問許可權問題
java的訪問許可權主要有四種:private、default、protected、public,它們的許可權從左到右,依次變大。
如果事務方法的訪問許可權不是定義成public,這樣會導致事務失效,因為spring要求被代理方法必須是public
的。
翻開原始碼,可以看到,在AbstractFallbackTransactionAttributeSource
類的computeTransactionAttribute
方法中有個判斷,如果目標方法不是public,則返回null,即不支援事務。
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
...
}
2. 方法用final修飾
如果事務方法用final修飾,將會導致事務失效。因為spring事務底層使用了aop,也就是透過jdk動態代理或者cglib,幫我們生成了代理類,在代理類中實現的事務功能。
但如果某個方法用final修飾了,那麼在它的代理類中,就無法重寫該方法,而新增事務功能。
同理,如果某個方法是static的,同樣無法透過動態代理,變成事務方法。
3.物件沒有被spring管理
使用spring事務的前提是:物件要被spring管理,需要建立bean例項。如果類沒有加@Controller、@Service、@Component、@Repository等註解,即該類沒有交給spring去管理,那麼它的方法也不會生成事務。
4.表不支援事務
如果MySQL使用的儲存引擎是myisam,這樣的話是不支援事務的。因為myisam儲存引擎不支援事務。
5.方法內部呼叫
如下程式碼所示,update方法上面沒有加 @Transactional
註解,呼叫有 @Transactional
註解的 updateOrder 方法,updateOrder 方法上的事務會失效。
因為發生了自身呼叫,呼叫該類自己的方法,而沒有經過 Spring 的代理類,只有在外部呼叫事務才會生效。
@Service
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
this.updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
解決方法:
1、再宣告一個service,將內部呼叫改為外部呼叫
2、使用程式設計式事務
3、使用AopContext.currentProxy()獲取代理物件
@Servcie
public class OrderServiceImpl implements OrderService {
public void update(Order order) {
((OrderService)AopContext.currentProxy()).updateOrder(order);
}
@Transactional
public void updateOrder(Order order) {
// update order
}
}
6.未開啟事務
如果是spring專案,則需要在配置檔案中手動配置事務相關引數。如果忘了配置,事務肯定是不會生效的。
如果是springboot專案,那麼不需要手動配置。因為springboot已經在DataSourceTransactionManagerAutoConfiguration
類中幫我們開啟了事務。
7.吞了異常
有時候事務不會回滾,有可能是在程式碼中手動catch了異常。因為開發者自己捕獲了異常,又沒有手動丟擲,把異常吞掉了,這種情況下spring事務不會回滾。
如果想要spring事務能夠正常回滾,必須丟擲它能夠處理的異常。如果沒有拋異常,則spring認為程式是正常的。
Spring怎麼解決迴圈依賴的問題?
首先,有兩種Bean注入的方式。
構造器注入和屬性注入。
對於構造器注入的迴圈依賴,Spring處理不了,會直接丟擲BeanCurrentlylnCreationException
異常。
對於屬性注入的迴圈依賴(單例模式下),是透過三級快取處理來迴圈依賴的。
而非單例物件的迴圈依賴,則無法處理。
下面分析單例模式下屬性注入的迴圈依賴是怎麼處理的:
首先,Spring單例物件的初始化大略分為三步:
createBeanInstance
:例項化bean,使用構造方法建立物件,為物件分配記憶體。populateBean
:進行依賴注入。initializeBean
:初始化bean。
Spring為了解決單例的迴圈依賴問題,使用了三級快取:
singletonObjects
:完成了初始化的單例物件map,bean name --> bean instance
earlySingletonObjects
:完成例項化未初始化的單例物件map,bean name --> bean instance
singletonFactories
: 單例物件工廠map,bean name --> ObjectFactory,單例物件例項化完成之後會加入singletonFactories。
在呼叫createBeanInstance進行例項化之後,會呼叫addSingletonFactory,將單例物件放到singletonFactories中。
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}
假如A依賴了B的例項物件,同時B也依賴A的例項物件。
- A首先完成了例項化,並且將自己新增到singletonFactories中
- 接著進行依賴注入,發現自己依賴物件B,此時就嘗試去get(B)
- 發現B還沒有被例項化,對B進行例項化
- 然後B在初始化的時候發現自己依賴了物件A,於是嘗試get(A),嘗試一級快取singletonObjects和二級快取earlySingletonObjects沒找到,嘗試三級快取singletonFactories,由於A初始化時將自己新增到了singletonFactories,所以B可以拿到A物件,然後將A從三級快取中移到二級快取中
- B拿到A物件後順利完成了初始化,然後將自己放入到一級快取singletonObjects中
- 此時返回A中,A此時能拿到B的物件順利完成自己的初始化
由此看出,屬性注入的迴圈依賴主要是透過將例項化完成的bean新增到singletonFactories來實現的。而使用構造器依賴注入的bean在例項化的時候會進行依賴注入,不會被新增到singletonFactories中。比如A和B都是透過構造器依賴注入,A在呼叫構造器進行例項化的時候,發現自己依賴B,B沒有被例項化,就會對B進行例項化,此時A未例項化完成,不會被新增到singtonFactories。而B依賴於A,B會去三級快取尋找A物件,發現不存在,於是又會例項化A,A例項化了兩次,從而導致拋異常。
總結:1、利用快取識別已經遍歷過的節點; 2、利用Java引用,先提前設定物件地址,後完善物件。
Spring啟動過程
- 讀取web.xml檔案。
- 建立 ServletContext,為 ioc 容器提供宿主環境。
- 觸發容器初始化事件,呼叫 contextLoaderListener.contextInitialized()方法,在這個方法會初始化一個應用上下文WebApplicationContext,即 Spring 的 ioc 容器。ioc 容器初始化完成之後,會被儲存到 ServletContext 中。
- 初始化web.xml中配置的Servlet。如DispatcherServlet,用於匹配、處理每個servlet請求。
Spring 的單例 Bean 是否有併發安全問題?
當多個使用者同時請求一個服務時,容器會給每一個請求分配一個執行緒,這時多個執行緒會併發執行該請求對應的業務邏輯,如果業務邏輯有對單例狀態的修改(體現為此單例的成員屬性),則必須考慮執行緒安全問題。
無狀態bean和有狀態bean
- 有例項變數的bean,可以儲存資料,是非執行緒安全的。
- 沒有例項變數的bean,不能儲存資料,是執行緒安全的。
在Spring中無狀態的Bean適合用單例模式,這樣可以共享例項提高效能。有狀態的Bean在多執行緒環境下不安全,一般用Prototype
模式或者使用ThreadLocal
解決執行緒安全問題。
Spring Bean如何保證併發安全?
Spring的Bean預設都是單例的,某些情況下,單例是併發不安全的。
以 Controller
舉例,假如我們在 Controller
中定義了成員變數。當多個請求來臨,進入的都是同一個單例的 Controller
物件,並對此成員變數的值進行修改操作,因此會互相影響,會有併發安全的問題。
應該怎麼解決呢?
為了讓多個HTTP請求之間不互相影響,可以採取以下措施:
1、單例變原型
對 web 專案,可以 Controller
類上加註解 @Scope("prototype")
或 @Scope("request")
,對非 web 專案,在 Component
類上新增註解 @Scope("prototype")
。
這種方式實現起來非常簡單,但是很大程度上增大了 Bean 建立例項化銷燬的伺服器資源開銷。
2、儘量避免使用成員變數
在業務允許的條件下,可以將成員變數替換為方法中的區域性變數。這種方式個人認為是最恰當的。
3、使用併發安全的類
如果非要在單例Bean中使用成員變數,可以考慮使用併發安全的容器,如 ConcurrentHashMap
、ConcurrentHashSet
等等,將我們的成員變數包裝到這些併發安全的容器中進行管理即可。
4、分散式或微服務的併發安全
如果還要進一步考慮到微服務或分散式服務的影響,方式3便不合適了。這種情況下可以藉助於可以共享某些資訊的分散式快取中介軟體,如Redis等。這樣即可保證同一種服務的不同服務例項都擁有同一份共享資訊了。
@Async註解的原理
當我們呼叫第三方介面或者方法的時候,我們不需要等待方法返回才去執行其它邏輯,這時如果響應時間過長,就會極大的影響程式的執行效率。所以這時就需要使用非同步方法來並行執行我們的邏輯。在springboot中可以使用@Async註解實現非同步操作。
使用@Async註解實現非同步操作的步驟:
1.首先在啟動類上新增 @EnableAsync 註解。
@Configuration
@EnableAsync
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new
AnnotationConfigApplicationContext(App.class);
MyAsync service = ctx.getBean(MyAsync.class);
System.out.println(service.getClass());
service.async1();
System.out.println("main thread finish...");
}
}
2.在對應的方法上新增@Async註解。
@Component
public class MyAsync {
@Async
public void asyncTest() {
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("asyncTest...");
}
}
執行程式碼,控制檯輸出:
main thread finish...
asyncTest...
證明asyncTest方法非同步執行了。
原理:
我們在主啟動類上貼了一個@EnableAsync註解,才能使用@Async生效。@EnableAsync的作用是透過@import匯入了AsyncConfigurationSelector。在AsyncConfigurationSelector的selectImports方法將ProxyAsyncConfiguration定義為Bean注入容器。在ProxyAsyncConfiguration中透過@Bean的方式注入AsyncAnnotationBeanPostProcessor類。
程式碼如下:
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {
}
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] { ProxyAsyncConfiguration.class.getName() };
//...
}
}
}
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
//建立postProcessor
AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
//...
}
}
AsyncAnnotationBeanPostProcessor往往期建立了一個增強器AsyncAnnotationAdvisor。在AsyncAnnotationAdvisor的buildAdvice方法中,建立了AnnotationAsyncExecutionInterceptor。
public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
@Override
public void setBeanFactory(BeanFactory beanFactory) {
super.setBeanFactory(beanFactory);
//建立一個增強器
AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
//...
advisor.setBeanFactory(beanFactory);
this.advisor = advisor;
}
}
public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
public AsyncAnnotationAdvisor(
@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
//增強方法
this.advice = buildAdvice(executor, exceptionHandler);
this.pointcut = buildPointcut(asyncAnnotationTypes);
}
// 委託給AnnotationAsyncExecutionInterceptor攔截器
protected Advice buildAdvice(
@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
//攔截器
AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
interceptor.configure(executor, exceptionHandler);
return interceptor;
}
}
AnnotationAsyncExecutionInterceptor繼承自AsyncExecutionInterceptor,間接實現了MethodInterceptor。該攔截器的實現的invoke方法把原來方法的呼叫提交到新的執行緒池執行,從而實現了方法的非同步。
public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport implements MethodInterceptor, Ordered {
public Object invoke(final MethodInvocation invocation) throws Throwable {
//...
//構建放到AsyncTaskExecutor執行Callable Task
Callable<Object> task = () -> {
//...
};
//提交到新的執行緒池執行
return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
}
由上面分析可以看到,@Async註解其實是透過代理的方式來實現非同步呼叫的。
那使用@Async有什麼要注意的呢?
1.使用@Aysnc的時候最好配置一個執行緒池Executor以讓執行緒複用節省資源,或者為SimpleAsyncTaskExecutor設定基於執行緒池實現的ThreadFactory,在否則會預設使用SimpleAsyncTaskExecutor,該executor會在每次呼叫時新建一個執行緒。
2.呼叫本類的非同步方法是不會起作用的。這種方式繞過了代理而直接呼叫了方法,@Async註解會失效。
最後給大家分享一個Github倉庫,上面有大彬整理的300多本經典的計算機書籍PDF,包括C語言、C++、Java、Python、前端、資料庫、作業系統、計算機網路、資料結構和演算法、機器學習、程式設計人生等,可以star一下,下次找書直接在上面搜尋,倉庫持續更新中~