深入理解Spring(六)、JDBC源码分析

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

深入理解Spring源码分为7小节,本小节为Spring源码第6小节,各个小节目录如下。

  1. 扫描过程
  2. bean创建过程
  3. 容器扩展
  4. AOP源码分析
  5. 事务源码分析

6.  Spring JDBC源码分析

  1. Spring常用设计模式

JDBC是Java的一套规范接口,用来和各种数据库交互,Java提供接口,各个厂商实现,但是里面并没有很厉害的东西可以让我们学习,最厉害的还是各个厂商的实现,但是他们的实现都很复杂,所以没有多少人去解析他们的源码,拿Mysql驱动来说,他需要创建Socket和目标服务器进行通信,我们每执行的sql语句,都会发送到远程服务端执行,执行后返回数据并转换成ResultSet,这其实是很复杂的事情,复杂的原因是要定义一套信息交互格式,虽然我们可以不用MySql驱动照样可以和Mysql服务端通信,但是这是很难的,如果你非常了解mysql交互协议的话,也并不是不可能。

其他厂商也是。

这里的驱动是指Driver类,任何厂商都会有实现,他是用来创建Connection的,但这里就有一个问题,Java是怎么知道使用哪个驱动的?

其实Java也不知道,在初学JDBC的时候,首先会使用Class.forName("com.mysql.cj.jdbc.Driver"),这条语句其实是可以不需要的,因为有SPI机制,具体mysql在那个版本增加了对SPI的支持,我就不知道了,不想找资料了,但是8.0以后肯定是有的,在8.0后的驱动包中有META-INF/services/java.sql.Driver这个文件,这是关键,这个文件的内容是com.mysql.cj.jdbc.Driver。接下来调用ServiceLoader.load(Driver.class)会自动通过Class.forName加载这个类,Class.forName()一个类后,这个类的static代码块就会被执行。

那么驱动类被加载后往往都会在static代码块中执行DriverManager.registerDriver,注册的对象是本身,内部会有一个集合保存所有注册的Driver,接下来在调用getConnection()的时候,依次遍历已经注册后的Driver,看哪个Driver的connect()方法不返回null,表示这个Driver对传递的参数感兴趣,可以对他建立一个连接,所以每个厂商的url都不一样。

剩下的就是各个厂商对Statement的实现了。

下面还是分析一下JdbcTemplate。

JdbcTemplate的作用是省去了一些过程,直接扔给他一个DataSource就可以工作了,最重要的还为我们封装了对ResultSet的转换,但是可气的是,没有一个方法可以直接把结果转换成对应的实体,倒是提供了一大堆接口,通过接口,我们可以间接实现,这方面大不如Mybatis方便。

实现对List的转换

JdbcTemplate提供的queryForList()方法,没有一个可以把多行结果转换成对应的List,只可以处理1列数据。

System.out.println(jdbcTemplate.queryForList("select id from test where id =1 ", Integer.class));

虽然他可以获取全部,但是返回的格式是List<Map<String, Object>>

下面我们做一下扩展。

public class EnhanceJdbcTemplate extends JdbcTemplate {
   public EnhanceJdbcTemplate(DataSource dataSource) {
      super(dataSource);
   }
   @Override
   public <T> List<T> queryForList(String sql, Class<T> tClass) {
      return this.query(sql, (rs, rowNum) ->  mapping(tClass, rs));
   }
   private <T> mapping(Class<T> cls, ResultSet resultSet) {
      Field[] classFields = cls.getDeclaredFields();
      T classInstance = BeanUtils.instantiateClass(cls);
      for (Field classField : classFields) {
         classField.setAccessible(true);
         String name = classField.getName();
         try {
            Object dataBaseValue = resultSet.getObject(name, classField.getType());
            ReflectionUtils.setField(classField, classInstance, dataBaseValue);
         } catch (SQLException e) {
            e.printStackTrace();
         }
      }
      return classInstance;
   }
}
EnhanceJdbcTemplate jdbcTemplate = new EnhanceJdbcTemplate(dataSource());
System.out.println(jdbcTemplate.queryForList("select * from test",Test.class));

源码分析

JdbcTemplate中有下面这几个接口很重要

RowMapper:提供给我们可以自行实现ResultSet到实体类的转换,这里的ResultSet是单条记录,虽然可以调用next,但是后续会被影响,JdbcTemplate内部会遍历ResultSet依次传递给他。

ResultSetExtractor:提供给我们可以自行实现ResultSet到实体的转换,这里的ResultSet是数据整体,通常转换成List。

RowCallbackHandler:提供给我们用于处理当前行,但是不返回数据,JdbcTemplate在内部还是遍历ResultSet,传递当前行给他。

query()

下面我们来分析一下query()源码,根据以前看Spring核心的经验,JdbcTemplate的作者和Spring 核心的作者绝逼不是一波人,风格大不相同,方法中都出现类定义了,虽然这是允许的,但很少见。

在query方法中,

public <T> query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
   class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
      @Override
      @Nullable
      public T doInStatement(Statement stmt) throws SQLException {
         ResultSet rs = null;
         try {
            rs = stmt.executeQuery(sql);
            return rse.extractData(rs);
         }
         finally {
            JdbcUtils.closeResultSet(rs);
         }
      }
      @Override
      public String getSql() {
         return sql;
      }
   }
   return execute(new QueryStatementCallback(), true);
}

在进入到这里,这里会调用上面方法的doInStatement,执行executeQuery(),这个方法就到头了,剩下的是解析。

private <T> execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
   Connection con = DataSourceUtils.getConnection(obtainDataSource());
   Statement stmt = null;
   try {
      stmt = con.createStatement();
      applyStatementSettings(stmt);
      T result = action.doInStatement(stmt);
      handleWarnings(stmt);
      return result;
   }
   catch (SQLException ex) {
   }
  
}

解析数据在这里,同样是要遍历ResultSet,然后通过rowMapper.mapRow把当前行转换成对应的实体了,这个通常我们自行实现。

@Override
public List<T> extractData(ResultSet rs) throws SQLException {
   List<T> results = (this.rowsExpected > 0 ? new ArrayList<>(this.rowsExpected) : new ArrayList<>());
   int rowNum = 0;
   while (rs.next()) {
      results.add(this.rowMapper.mapRow(rs, rowNum++));
   }
   return results;
}

不得不说,JdbcTemplate这个封装的真不错。

其他updata()最终还是要调用到execute,只不过对应的StatementCallback实现类不同,但是看完他的源码,发现在方法中声明类是个很不错的选择,因为这个类只会在当前方法中使用,其他地方用不到。

实战

假定现在有如下接口。

public interface UserMapping {
   List<Test> queryUser(Integer id);
}

我们想这样作,该如何实现?

@Configuration
public class UserTest implements InitializingBean {
   @Autowired
   UserMapping userMapping;
   @Override
   public void afterPropertiesSet() throws Exception {
      System.out.println(userMapping.queryUser(1));
   }
}

先实现一个ImportBeanDefinitionRegistrar,用于把某个包下所有 标有@Mapping的接口注册到容器。

public class EasyMappingBeanDefinitionRegistry implements ImportBeanDefinitionRegistrar {
   static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
   @Override
   public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      Map<String, Object> mappingScan = importingClassMetadata.getAnnotationAttributes(EnabledEasyDB.class.getName());
      PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
      String basePackage = ClassUtils.convertClassNameToResourcePath(((String) mappingScan.get("value")));
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + basePackage + '/' + DEFAULT_RESOURCE_PATTERN;
      try {
         Resource[] resources = pathMatchingResourcePatternResolver.getResources(packageSearchPath);
         CachingMetadataReaderFactory cachingMetadataReaderFactory = new CachingMetadataReaderFactory();
         for (Resource resource : resources) {
            MetadataReader metadataReader = cachingMetadataReaderFactory.getMetadataReader(resource);
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            boolean anInterface = classMetadata.isInterface() &&
                  !classMetadata.isAnnotation() &&
                  metadataReader.getAnnotationMetadata().hasAnnotation(Mapping.class.getName());
            if (anInterface) {
               String className = metadataReader.getClassMetadata().getClassName();
               AbstractBeanDefinition beanDefinition;
               beanDefinition = BeanDefinitionBuilder
                     .genericBeanDefinition(MappingFactorBean.class)
                     .addConstructorArgValue(className)
                     .getBeanDefinition()
;
               String beanName = className.substring(className.lastIndexOf(".") + 1);
               registry.registerBeanDefinition(beanName, beanDefinition);
            }
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }
}

@Import

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EasyMappingBeanDefinitionRegistry.class)
public @interface EnabledEasyDB 
{
   String value() default "";
}

扩展JdbcTemplate源码,主要重写queryForList的两个重载,这样的返回更符合我们的习惯。

public class EnhanceJdbcTemplate extends JdbcTemplate {
   public EnhanceJdbcTemplate(DataSource dataSource) {
      super(dataSource);
   }
   @Override
   public <T> List<T> queryForList(String sql, Class<T> elementType) {
      return this.query(sql, (rs, rowNum) -> mapping(elementType, rs));
   }
   @Override
   public <T> List<T> queryForList(String sql, Class<T> elementType, Object... args) throws DataAccessException {
      return this.query(sql, (rs, rowNum) -> mapping(elementType, rs), args);
   }
   private <T> mapping(Class<T> elementType, ResultSet resultSet) {
      Field[] classFields = elementType.getDeclaredFields();
      T classInstance = BeanUtils.instantiateClass(elementType);
      for (Field classField : classFields) {
         classField.setAccessible(true);
         String name = classField.getName();
         try {
            Object dataBaseValue = resultSet.getObject(name, classField.getType());
            ReflectionUtils.setField(classField, classInstance, dataBaseValue);
         } catch (SQLException e) {
            e.printStackTrace();
         }
      }
      return classInstance;
   }
}

@Mapping用于标在接口类上,使这个接口可以有代理。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mapping {
}

为接口代理并放在容器,必须实现FactoryBean接口。

public class MappingFactorBean<Timplements FactoryBean<T>, BeanFactoryAware {
   private BeanFactory beanFactory;
   private Class<T> targetClass;
   
   @Override
   public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
      this.beanFactory = beanFactory;
   }
   public MappingFactorBean(String className) {
      try {
         targetClass = (Class<T>) Class.forName(className);
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
   }
   @Override
   public T getObject() throws Exception {
      return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{targetClass},
            new MappingInvocationHandler(beanFactory.getBean(EnhanceJdbcTemplate.class)));
   }
   @Override
   public Class<?> getObjectType() {
      return targetClass;
   }
}

下面两个注解用于表在接口方法上。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Select {
   String value() default "";
}

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Update {
   String value() default "";
}

最后UserMapping就可以这样做。

@Mapping
public interface UserMapping {
   @Select("select * from test where id =?")
   List<Test> queryUser(Integer id);
}

核心实现如下。

public class MappingInvocationHandler implements InvocationHandler {
   private JdbcTemplate jdbcTemplate;
   public MappingInvocationHandler(JdbcTemplate jdbcTemplate) {
      this.jdbcTemplate = jdbcTemplate;
   }
   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if (method.getDeclaringClass().equals(Object.class)) {
         return method.invoke(this, args);
      }
      Class<?> methodReturnType = method.getReturnType();
      if (methodReturnType.equals(List.class)) {
         return resultForList(method, args);
      }
      return null;
   }
   public List<?> resultForList(Method method, Object[] args) {
      String execSql = getSqlValueForMethodAnnotation(method);
      ResolvableType resolvableType = ResolvableType.forMethodReturnType(method);
      Class<?> rawClass = resolvableType.getGeneric(0).getRawClass();
      if (args == null || args.length == 0) {
         return jdbcTemplate.queryForList(execSql, rawClass);
      }
      return jdbcTemplate.queryForList(execSql, rawClass, args);
   }
   private String getSqlValueForMethodAnnotation(Method method) {
      if (hasAnnotation(method, Select.class)) {
         return AnnotationUtils.getAnnotation(method, Select.class).value();
      }
      if (hasAnnotation(method, Update.class)) {
         return AnnotationUtils.getAnnotation(method, Update.class).value();
      }
      throw new IllegalArgumentException("找不到sql");
   }
   private boolean hasAnnotation(Method method, Class<? extends Annotation> annotation) {
      return AnnotationUtils.getAnnotation(method, annotation) != null;
   }
}

最后别忘了加数据源。

@Configuration
public class UserTest implements InitializingBean {
   @Autowired
   UserMapping userMapping;
   @Bean
   public DataSource dataSource() {
      DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource("jdbc:mysql://localhost:3306/test");
      driverManagerDataSource.setUsername("root");
      driverManagerDataSource.setPassword("hxl..");
      driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
      return driverManagerDataSource;
   }
   @Bean
   public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) {
      DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dataSource);
      return dataSourceTransactionManager;
   }
   @Bean
   public EnhanceJdbcTemplate jdbcTemplate() {
      return new EnhanceJdbcTemplate(dataSource());
   }
   
   @Override
   public void afterPropertiesSet() throws Exception {
      System.out.println(userMapping.queryUser(1));
   }
}

- END -


原文始发于微信公众号(十四个字节):深入理解Spring(六)、JDBC源码分析