天天看點

注入dao報空指針_MyBatis中如何通過繼承SqlSessionDaoSupport來編寫DAO

注入dao報空指針_MyBatis中如何通過繼承SqlSessionDaoSupport來編寫DAO

在MyBatis中,當我們編寫好通路資料庫的映射器接口後,MapperScannerConfigurer就能自動成批地幫助我們根據這些接口生成DAO對象(請參考本系列前面的博文:MyBatis MapperScannerConfigurer配置),然後我們再使用Spring把這些DAO對象注入到業務邏輯層的對象(Service類的對象)。是以,在這種情況下的DAO層,我們幾乎不用編寫代碼,而且也沒有地方編寫,因為隻有接口。這固然友善,不過如果我們需要在DAO層寫一些代碼的話,這種方式就無能為力了。此時,MyBatis-Spring提供給我們的SqlSessionDaoSupport類就派上了用場。今天,就以通路學生表為例,通過繼承SqlSessionDaoSupport,來寫一個StudentDao。

首先來認識一個SqlSessionDaoSupport類。類org.mybatis.spring.support.SqlSessionDaoSupport繼承了類org.springframework.dao.support.DaoSupport,它是一個抽象類,本身就是作為DAO的基類來使用的。它需要一個SqlSessionTemplate或一個SqlSessionFactory,若兩者都設定了,則SqlSessionFactory會被忽略(實際上它接收了SqlSessionFactory後也會利用SqlSessionFactory建立一個SqlSessionTemplate)。這樣,我們在子類中就能通過調用SqlSessionDaoSupport類的getSqlSession()方法來擷取這個SqlSessionTemplate對象。而SqlSessionTemplate類實作了SqlSession接口,是以,有了SqlSessionTemplate對象,通路資料庫就不在話下了。是以,我們需要Spring給SqlSessionDaoSupport類的子類的對象(多個DAO對象)注入一個SqlSessionFactory或一個SqlSessionTemplate。好消息是,SqlSessionTemplate是線程安全的,是以,給多個DAO對象注入同一個SqlSessionTemplate是沒有問題的,本例也将注入SqlSessionFactory。

但壞消息是,自mybatis-spring-1.2.0以來,SqlSessionDaoSupport的setSqlSessionTemplate和setSqlSessionFactory兩個方法上的@Autowired注解被删除,這就意味着繼承于SqlSessionDaoSupport的DAO類,它們的對象不能被自動注入SqlSessionFactory或SqlSessionTemplate對象。如果在Spring的配置檔案中一個一個地配置的話,顯然太麻煩。比較好的解決辦法是在我們的DAO類中覆寫這兩個方法之一,并加上@Autowired注解。那麼如果在每個DAO類中都這麼做的話,顯然很低效。更優雅的做法是,寫一個繼承于SqlSessionDaoSupport的BaseDao,在BaseDao中完成這個工作,然後其他的DAO類再都從BaseDao繼承。BaseDao代碼如下:

package com.abc.dao.base;import org.mybatis.spring.SqlSessionTemplate;import org.mybatis.spring.support.SqlSessionDaoSupport;import org.springframework.beans.factory.annotation.Autowired;public class BaseDao extends SqlSessionDaoSupport {  @Autowired public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { super.setSqlSessionTemplate(sqlSessionTemplate); } }
           

接着的問題就是StudentDao該怎麼寫。擷取了SqlSessionTemplate之後,有兩種方式通路資料庫,一種是通過SqlSessionTemplate的相關方法執行SQL映射檔案中的SQL語句,一種是先通過SqlSessionTemplate擷取映射器對象(在這裡就是StudentMapper接口類型的對象),然後再調用這個映射器對象的資料庫通路方法。鑒于第二種方法更友善(第一種方法需要寫冗長的SQL語句全名,全是字元串,無代碼提示,第二種方法可以利用IDE的代碼提示功能)、及具有更好的類型安全性,本示例就采用後者。

先看StudentMapper接口,代碼如下:

package com.abc.mapper;import com.abc.domain.Student;public interface StudentMapper {  //根據id查找學生 public Student getById(int id);  //添加一名學生 public int add(Student student);  //修改學生 public int update(Student student);  //删除學生 public int delete(int id); }
           

至于StudentDao,應該先定義一個私有的StudentMapper類型的變量studentMapper,并在合适的時機初始化studentMapper,然後就簡單了,在各方法中調用studentMapper相應的方法即可。不過,難就難在這個合适的時機不好找。如果定義後直接初始化,如下所示:

private StudentMapper studentMapper = this.getSqlSession().getMapper(StudentMapper.class);
           

或者在構造方法中使用類似的代碼進行初始化,你都會得到空指針異常。原因是這些代碼在運作時,SqlSessionTemplate對象還沒有被注入(如果你選擇注入SqlSessionFactory,結果是一樣的)。

似乎進入了死胡同。

能不能寫一個初始化方法,在這個方法中對studentMapper進行初始化,然後在SqlSessionTemplate被注入後,這個方法被自動調用呢?

首先想到的是afterPropertiesSet()方法。接口org.springframework.beans.factory.InitializingBean隻聲明了一個afterPropertiesSet()方法,顧名思義,此方法在Spring為bean注入完所有的依賴關系後會被Spring自動調用,如果StudentDao實作了此接口,就可以在afterPropertiesSet()方法中對studentMapper進行初始化。

很完美是嗎?

不幸的是,org.springframework.dao.support.DaoSupport類(SqlSessionDaoSupport類的父類)已經實作了此接口,為afterPropertiesSet()方法提供了實作,并且把此方法定義成了final類型的。也就是說,我們在子類中不能覆寫此方法。是以,afterPropertiesSet()方法不能為我們所用。

還有一種方法,就是先在StudentDao中寫好初始化方法,然後在Spring中使用xml配置bean的時候,指定init-method屬性的值為此方法。則此方法也會在Spring注入完所有的依賴關系後,被Spring調用。不過這種方法需要使用xml配置每一個DAO bean,不如使用注解友善,故這種方法也被排除。

還有沒有其他辦法呢?

有的,我們還可以使用Spring的bean後處理器。同樣顧名思義,bean後處理器中的方法是在Spring注入完依賴關系之後被調用的,是以很适合我們目前的要求。現在,我們就用bean後處理器來解決我們的問題。先上StudentDao的代碼如下(注意對studentMapper進行初始化的方法是init方法):

package com.abc.dao;import org.springframework.stereotype.Repository;import com.abc.dao.base.BaseDao;import com.abc.domain.Student;import com.abc.mapper.StudentMapper;@Repositorypublic class StudentDao extends BaseDao{  private StudentMapper studentMapper;  public Student getById(int id) { return this.studentMapper.getById(id); }  public void deleteById(int id) { int count = this.studentMapper.delete(id); System.out.println("删除了" + count + "行資料。"); }  public void update(Student student) { int count = this.studentMapper.update(student); System.out.println("修改了" + count + "行資料。"); }  public void add(Student student) { // TODO Auto-generated method stub int count = this.studentMapper.add(student); System.out.println("添加了" + count + "行資料。"); } //對studentMapper進行初始化的方法 public void init() { System.out.println("初始化studentMapper..."); this.studentMapper  = this.getSqlSession().getMapper(StudentMapper.class); } }
           

在StudentDao中的各資料庫通路方法中,我們就可以根據自己的需要來編寫相應的代碼了。

然後是編寫bean後處理器,這要通過實作接口org.springframework.beans.factory.config.BeanPostProcessor實作,這個接口聲明了如下兩個方法:

Object postProcessBeforeInitialization(Object bean, String beanName)

Object postProcessAfterInitialization(Object bean, String beanName)

兩個方法的第一個參數都是待處理的bean,第二個參數都是待處理的bean的名字,Spring會調用這兩個方法對容器中的每個bean進行處理,具體的運作順序是:

1、注入依賴關系;

2、調用postProcessBeforeInitialization方法;

3、調用afterPropertiesSet方法;

4、調用init-method方法(如前所述,在使用xml配置bean的時候,可使用init-method屬性指定bean的初始化方法);

5、調用postProcessAfterInitialization方法。

由于第一步就已經完成了依賴關系注入,是以我們在postProcessBeforeInitialization或postProcessAfterInitialization這兩個方法中調用DAO(這裡隻有StudentDao,但實際上應該有很多DAO類,每個DAO類都應該有自己的init方法)的init方法都可以,這裡我們在postProcessBeforeInitialization方法中調用。不過這裡還有一個問題需要解決,就是在postProcessBeforeInitialization方法中,參數bean的類型是Object類型,我們最多隻能把它強制轉換為BaseDao類型(因為具體的DAO類型有很多),但即使如此,也不能通過參數bean調用DAO的init方法,因為init方法不是在BaseDao中聲明的。而如果每種DAO類型都分别判斷一遍再做相應的強制類型轉換,則顯然很低效,而且每增加一種DAO類型,就得添加相應的類型判斷、強制類型轉換的代碼。對于這個問題,有兩個解決方法,一是用反射,二是用多态,而用多态顯然更優雅。具體做法是改造BaseDao,在BaseDao中聲明一個抽象的init方法,顯然此時BaseDao類也應該聲明為抽象類,然後在各子類中實作此init方法。這樣,我們隻需要把參數bean強制轉換為BaseDao類型,就可以通過參數bean調用init方法了。而根據多态的原理,實際調用的是具體的子類(如StudentDao類)中實作的init方法。這樣,我們的問題就完美解決了。經多态改造後的BaseDao代碼如下:

package com.abc.dao.base;import org.mybatis.spring.SqlSessionTemplate;import org.mybatis.spring.support.SqlSessionDaoSupport;import org.springframework.beans.factory.annotation.Autowired;public abstract class BaseDao extends SqlSessionDaoSupport {  @Autowired public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { super.setSqlSessionTemplate(sqlSessionTemplate); }  //抽象方法 public abstract void init();  }
           

StudentDao的代碼不用改動,而bean後處理器的代碼如下:

package com.abc.post;import org.springframework.beans.BeansException;import org.springframework.beans.factory.config.BeanPostProcessor;import com.abc.dao.base.BaseDao;public class DaoPostProcessor implements BeanPostProcessor {  @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { // TODO Auto-generated method stub //隻處理BaseDao的子類的對象 if(bean.getClass().getSuperclass()==BaseDao.class) { BaseDao dao = (BaseDao)bean; dao.init(); } //傳回原bean執行個體 return bean; }  @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { // TODO Auto-generated method stub //直接傳回原bean執行個體,不做任何處理 return bean; }}
           

最後一步就是要在Spring的配置檔案中配置這個bean後處理器,跟配置普通的bean一樣。而且如果你不需要擷取這個bean後處理器的話,你甚至可以不給它指定id屬性的值。配置代碼如下:

如果Spring容器是BeanFactory,則還需手動注冊此後處理器;而如果Spring容器是ApplicationContext,則無需手動注冊,我們這裡采用後者。執行類的代碼如下(StudentDao類的對象被注入到了StudentService對象,這裡是請求了StudentService對象并調用了相關的方法,具體請參考StudentService、StudentDao的代碼,以及Spring中context:component-scan的相關配置。本示例完整源代碼與資料庫腳本下載下傳位址:http://down.51cto.com/data/1970833):

package com.demo;import org.springframework.context.ApplicationContext;import com.abc.service.StudentService;import com.abc.domain.Student;import org.springframework.context.support.ClassPathXmlApplicationContext;public class TestDaoSupport {  private static ApplicationContext ctx; static { // 在類路徑下尋找spring主配置檔案,啟動spring容器 ctx = new ClassPathXmlApplicationContext( "classpath:/applicationContext.xml"); }  public static void main(String[] args) { // 從Spring容器中請求服務元件 StudentService studentService =  (StudentService)ctx.getBean("studentService");  Student student = studentService.getById(13); System.out.println(student.getName());  }}
           

運作結果如下:

注入dao報空指針_MyBatis中如何通過繼承SqlSessionDaoSupport來編寫DAO

感謝你耐心看到這裡,如果我現在告訴你,其實用不着費這麼大勁寫後處理器,有更簡便的方法,你會不會很生氣?o(∩_∩)o

其實,在StudentDao的init方法上,加上@PostConstruct注解(需要引入javax.annotation.PostConstruct)就可以了。用@PostConstruct标注init方法後,init方法就會成為初始化方法,而在Spring完成依賴注入後被Spring調用。也就是說,此注解與前面提到的init-method屬性的用途類似,讀者可自行嘗試一下。

不過,我還是希望,前面介紹的利用bean後處理器解決問題的方法,能對大家有參考價值,感謝你的關注。

o(∩_∩)o

關注我,私信關鍵字【資料】即可擷取Java并發程式設計/Spring源碼分

析/redis/mongodb/dubbo/zookeeperfka /Spring-cloud和高并發、高可用、分布式、

高性能架構設計精講,還有面試專題的資料