zoukankan      html  css  js  c++  java
  • Spring Relational Database

    为了避免持久化的逻辑分散到应用的各个组件中,将数据访问功能放到一个或多个专注于此项任务的组件中,这样的组件通常称为数据访问对象(DAO)或Repository。

    为了避免应用与特定的数据访问策略耦合在一起,编写良好的Repository应该以借口的方式暴漏功能。通过接口来访问Repository可以为这些数据访问接口创建mock实现,提升单元测试的效率。

    SQLException表示在尝试访问数据库时出现了问题,但这个异常却没有告诉你哪里出错了以及如何进行处理。

    可能导致抛出SQLException的常见问题包括:应用程序无法连接数据库;要执行查询存在语法错误;查询中所使用的表/列不存在和试图插入或更新的数据违反了数据库约束。事实上,大多数抛出SQLException的情况是致命的错误。即使对某些SQLException有处理方案,还是要捕获SQLException并查看其属性才能获知问题根源的更多信息。这是因为SQLException被视为处理数据访问所有问题的通用一场。

    Spring JDBC提供的数据访问异常体系提供了多个数据访问异常,分别描述了他们抛出时所对应的问题。而且它并没有与特定的持久化方式相关联,这意味着可以使用Spring抛出一致的异常而不用关心所选择的持久化方案。Spring JDBC异常都继承自DataAccessException。DataAccessException是一个非检查型异常,因此不用捕获Srping所抛出的数据访问异常。

    模板方法将过程中与特定实现相关的部分委托给接口,而这个接口的不同实现定义了过程中的具体行为。

    Spring将数据访问过程中固定的和可变的部分明确划分为两个不同的类:模板(template)和回调(callback)。模板管理过程中固定的部分(书屋控制,管理资源以及处理异常),而回调处理自定义的数据访问代码(应用程序相关的数据访问——语句、绑定参数以及整理结果集)。

    Spring提供的数据访问模板,分别适用于不同的持久化机制:

    jca.cci.core.CciTemplate  JCA CCI连接

    jdbc.core.JdbcTemplate  JDBC连接

    jdbc.core.namedparam.NamedParameterJdbcTemplate  支持命名参数的JDBC连接

    jdbc.core.simple.SimpleJdbcTemplate  通过Java5简化后的JDBC连接

    orm.hibernate3.HibernateTemplate  Hibernate 3.x以上的Session

    orm.ibatis.SqlMapClientTemplate  iBatis SqlMap客户端

    orm.jdo.JdoTemplate  Java数据对象实现

    orm.jpa.JpaTempalte  Java持久化API的实体管理器

    Spring提供了在Spring上下文中配置数据源bean的多种方式,包括:通过JDBC驱动程序定义的数据源;通过JNDI查找的数据源和连接池的数据源。也可以使用Apache Commons DBCP、c3p0和BoneCP这些框架来配置Spring的数据源

    使用DBCP配置数据源

      基于java的配置

    @Bean
    public BasicDataSource dataSource(){
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:tcp://localhost/...");
        dataSource.setUsernname("username");
        dataSource.setPassword("root");
        dataSource.setInitialSize(5);
        dataSource.setMaxActive(10);
        return dataSource
    }

      基于xml的配置

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        p:driverClassName="org.h2.Driver"
        p:url="jdbc:h2:tcp://localhost/"
        p:username="username"
        p:password="root"
        p:initialSize="5"
        p:maxActive="10"     />

    DBCP BasicDataSource一些常用的池配置属性

    initialSize  池启动时创建的连接数量

    maxActive  同以时间可以从池中分配的最多连接数,若为0,则表示无限制

    madIde  池里不会被释放的最多空闲连接数,若为0,则表示无限制

    maxOpenPreparedStatements  在同一时间能够从语句池中分配的预处理语句的最大数量,若为0,则表示无限制

    maxWait  在抛出异常之前,池等待连接回收的最大时间,若设置为-1,表示无限等待

    minEvictabelIdeTimeMills  连接在池中保持空闲而不被回收的最大时间

    minIde  在不创建新连接的情况下,池中保持空闲的最小连接数

    poolPreparedStatements  是否对预处理语句进行池管理

    在Spring中,通过JDBC驱动定义数据源是最简单的配置方式。Spring提供了三个这样的数据源类供选择:

      DriverManagerDataSource:在每个连接请求时都会返回一个新建的连接。与DBCP的BasicDataSource不同,由DriverManagerDataSource提供的连接并没有进行池化管理

      SimpleDriverDataSource:与DriberManagerDataSource的工作方式类似,但是它直接使用JDBC驱动,来解决在特定环境下的类的加载问题,如OSGi容器

      SingleConnectionDataSource:在每个连接强求时都会返回同一个的连接。尽管SingleConnectionDataSource不是严格意义上的连接池数资源,但可以视其为自由一个连接的池

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" 
        p:driverClassName="driverClassName"
        p:url="jdbc:..."
        p:username="username"
        p:passord=""  />
    @Bean
    public DataSource dataSource(){
        DriverManagerDataSource dataSource =new DriverManagerDataSource();
        dataSource.setDriverClassName("driverClassName");
        dataSource.setUrl("url");
        dataSource.setUsername("username");
        dataSource.setPassword("password");
        return dataSource;
    }

    嵌入式数据库(embedded database)作为应用的一部分运行,而不是应用连接的独立数据库服务器。Spring的jdbc命名空间能够简化嵌入式数据库的配置

    <?xml version="1.0" encoding="UTF-8" ?> 
    <beans xmlns="http://www.springframework.org/schema/jdbc"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns:jdbc="http://www.springframework.org/schema/jdbc"
                 xsi:schemaLocation="http:///www.springframework.org/schema/jdbc
                                     http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
    http://www.springframework.org/schema/beans                    http://www.springframework.org/schema/beans/spring-beans.xsd "
    >   <jdbc:embedded-datatbase id="dataSource" type="H2">    <jdbc:script location="" />    <jdbc:script location="" />   </jdbc:embedded-database> </beans>
    @Bean
    public DataSource dataSource(){
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:*.sql")
            .build();
    }

    可以将不同的profile配置在不同的数据源中

    package cherry.config;
    
    import javax.sql.DataSource;
    
    import org.apache.commons.dbcp.BasicDataSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Profile;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
    import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
    import org.springframework.jndi.JndiObjectFactoryBean;
    
    @Configuration
    public class DataSourceConfiguration {
    
        @Profile("development")
        @Bean
        public DataSource embeddedDataSource(){
            return new EmbeddedDatabaseBuilder()
                    .setType(EmbeddedDatabaseType.H2)
                    .addScript("classpath:schema.sql")
                    .addScript("classpath:test-data.sql")
                    .build();
        }
        
        @Profile("qa")
        @Bean
        public DataSource dataSourceForQA(){
            BasicDataSource dataSource = new BasicDataSource();
            dataSource.setDriverClassName("driverName");
            dataSource.setUrl("url");
            dataSource.setUsername("username");
            dataSource.setPassword("password");
            dataSource.setInitialSize(5);
            dataSource.setMaxActive(10);
            return dataSource;
        }
        
        @Profile("production")
        @Bean
        public DataSource dataSource(){
            JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
            jndiObjectFactoryBean.setJndiName("jdbc/databaseName");
            jndiObjectFactoryBean.setResourceRef(true);
            jndiObjectFactoryBean.setProxyInterface(javax.sql.DataSource.class);
            return (DataSource)jndiObjectFactoryBean.getObject();
        }
    }

    Spring JDBC将数据访问的样板代码抽象到模板类之中。Spring为JDBC提供了三个模板类供选择:

      JdbcTemplate: 最基本的Spring JDBC模板,这个模板支持简单的JDBC数据访问功能以及基于索引参数的查询

      NamedParameterJdncTemplate:使用该模板类执行查询时可以将值以命名参数的形式绑定到SQL中,而不是使用简单的索引参数

    使用JdbcTemplate只需要为其设置DataSource即可

      @Bean

      public JdbcTemplate jdbcTempalte(DataSource dataSource){

        return new JdbcTemplate(dataSource);

      }

    将jdbcTemplate装配到Respository中.JdbcOperations是一个接口,定义了JdbcTemplate所实现的操作

      @Repository

      public class RepositoryImpl implements Respostory{

        private JdbcOperations jdbcOperations;

        @Inject

        public RepositoryImpl(JdbcOperations jdbcOperations){

          this.jdbcOperations = jdbcOperations;

        }

      }

      调用JdbcTemplate.update()方法时会获取连接、创建语句并执行插入SQL;findOne()方法使用了JdbcTemplate的回调;queryForObject()方法从数据库查询,它有三个参数:String(查询数据的sql),rowMapper(从ResultSet中提取数据并构建对象)和可变参数列表(要绑定到查询上的索引参数值)

      命名参数可以赋予SQL中每个参数一个明确的名字,在绑定只到查询语句的时候就通过该名字来引用参数。使用命名参数查询,绑定只的顺序就不重要了。如果查询语句发生了变化导致参数的顺序与之前不一致,也扔不需要修改绑定的代码。

    @Bean
    public NamedParameterJdbcTemplate jdbcTemplate(DataSource dataSource){
        return new NamedParameterJdbcTemplate(dataSource);
    }
    
    
    
    public void addSpitter(Spitter spitter){
        private static final String INSER_SPITTER = "insert into spitter(username, password, email) values(:username, :password, :email)";
        Map<String, Object> paramMap = new HashMap<String, Object>();
        paramMap.put("username", spitter.getUsername());
        paramMap.put("password", spittler.getPassword());
        paramMap.put("email", spitter.getEmail());
        jdbcOperations.update(INSER_SPITTER, paramMap);
    }

    延迟加载(Lazy loading):允许我们在需要的时候获取数据

    预先抓取(Eager fetching):借助预先抓取,可以使用一个查询获取完整的关联对象

    级联(Cascading):更改数据库中的某些表时会更改其他表

    Spring支持多个持久化框架,包括:Hibernate、iBatis、Java数据对象(java data object, JDO)以及Java持久化API(java persistence APT,JPA)。Spring对ORM框架的支持提供了一些附加的服务:支持集成Spring声明式事务;透明的异常处理;线程安全的、轻量级的模板类;DAO支持类和资源管理。

    在Spring中集成Hibernate

      声明Hibernate所需的主要接口是org.hibernate.Session。Session接口提供了基本的数据访问功能,如保存、更新、删除以及从数据库加载对象的功能。通过Hibernate的Session接口,应用程序的Repository能够满足所有的持久化需求。获取Hibernate Session对象的标准方式是借助于Hibernate SessionFactory接口的实现类。SessionFactory主要负责Hibernate Session的打开、关闭以及管理。

      Spring3.1+提供了三个SessionFactoryBean供选择:

        org.springframework.orm.hibernate3.LocalSesssionFactoryBean

        org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean

        org.springframework.orm.hibernate4.LocalSessionFactoryBean

      这些Session工厂bean都是Spring FactoryBean接口的实现,它会产生一个HibernateSessionFactory,它能装配进任何SessionFactory类型的属性中。这样就能在Spring应用上下文中,与其他的bean一起配置Hibernate Session工厂。选择SessionFactory取决于使用哪个版本的Hibernate以及使用XML还是使用注解来定义对象-数据库之间的映射关系。

      使用Hibernate3.2-4.0(不包含4.0)并使用XML定义映射,需要使用Spring的org.springframework.orm.hibernate3中的LocalSessionFactoryBean  

    @Bean
    public LocalSessionFactoryBean sessionFactory(DataSource dataSource){
        LocalSessionFactoryBean sfb = new LocalSessionFactoryBean();
        sfb.setDataSource(dataSource);
        sfb.setMappingResources(new String[]{"Spitter.hbm.xml"});
        Properties props = new Properties();
        props.setProperty("dialect", "org.hibernate.dialect.H2Dialect");
        sfb.setHibernateProperties(props);
        return sfb;
    }

      使用注解来定义持久化并木有使用Hibernate4,需要使用AnnotationSessionFactoryBean来代替LocalSessionFactoryBean

    @Bean
    public AnnotationSessionFactoryBean sessionFactory(DataSources dataSource){
        AnnotationSessionFactoryBean sfb = new AnnotationSessionFactoryBean();
        sfb.setDataSource(dataSource);
        sfb.setPackagesToScan(new String[]{"com.spitter.domain"});
        Properties props = new Properties();
        props.setProperty("dialect", "org.hibernate.dialect.H2Dialect");
        sfb.setHibernateProperties(props);
        return sfb;
    }

      使用Hibernate4应该使用org.springframework.orm.hiberante4中的LocalSessionFactoryBean来生成Session。packagesToScan属性告诉Spring扫描一个或多个包以查找域类,这些类通过注解的方式表明要使用Hibernate进行持久化,这些类可以使用注解包括JPA的@Entity或@MappedSuperclass以及Hibernate的@Entity

    @Bean
    public LocalSessionFactoryBean sessionFactory(DataSource dataSource){
        LocalSesssionFactory sfb = new LocalSessionFactory();
        sfb.setDataSource(dataSource);
        sfb.setPackagesToScan(new String[]{com.spittr.domain""});
        Properties props = new Properties();
        props.setProperty("dialect", "org.hibernate.dailect.H2Dialect");
        sfg.setHibernateProperties(props);
        return sfb;
    }

    为了不使用模板的Hibernate Repository添加异常转换功能,只需要在Spring应用上下文中添加一个PersistenceExceptionTranslationPostProcessor bean。PersistenceExceptionTranslationPostProcessors是一个bean后置处理器(bean post-processor),它会在所有拥有@Respository注解的类上添加一个通知器(advisor),这样会捕获任何平台相关的异常并以Spring非检查型数据访问异常的形式重新抛出。

    @Bean
    public BeanPostProcessor persistenceTranslation(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    Spring与JPA

      JPA是基于POJO的持久化机制。

      在Spring中使用JPA的第一不是要在Spring应用上下文中将实体管理工厂(entity manager factory)按照bean的形式来进行配置。

      基于JPA的应用程序需要使用EntityManagerFactory的实现类来获取EntityManager实例。JPA定义了两种类型的实体管理器:

        应用程序管理类型(Application-managed):当应用程序向实体管理器工厂直接请求实体管理器时,工厂会创建一个实体管理器。在这种模式下,程序要负责打开或关闭实体管理器并在事务中对其进行控制。这种方式的实体管理器适合于不运行在Java EE容器中的独立应用程序

        容器管理类型(Container-managed):实体管理器由Java EE创建和管理。应用程序根本不与实体管理器工厂打交道。相反,实体管理器直接通过注入或JNDI来获取。容器负责配置实体管理器工厂。这种类型的实体管理器最适用于Java EE容器,在这种情况下回希望在persistence.xml指定的JPA配置之外保持一些自己对JPA的控制。

      以上两种实体管理器实现了同一个EntityManager接口,区别在于EntityManager的创建和管理方式。应用程序管理类型的EntityManager是由EntityManagerFactory创建的,通过PersistenceProvider的createEntityManagerFactory()方法得到的。容器管理类型的EntityManagerFactory是通过PersistenceProvider的createContainerEntityManagerFactory()方法获得的。

      LocalEntityManagerFactoryBean生成应用程序管理类型的EntityManagerFactory

      LocalContainerEntityManagerFactoryBean生成容器管理类型的EntityManagerFactory

    配置应用程序管理类型的JPA

      1.对于应用程序管理类型的实体管理器工厂来说,它绝大部分配置信息来源于一个名为persistence.xml的配置文件,这个必须位于类路径下的META-INF目录下。persistence.xml的作用在于定义一个或多个持久化单元。持久化单元是同一个数据源下的一个或多个持久化类。

    <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
        <persistence-unit name="spitterPU">
            <class>com.domain.Spitter</class>
            <class>com.domain.Spittle</class>
            <properties>
                <property name="toplink.jdbc.driver" value="org.hsqldb.jdbcDriver" />
                <property name="toplink.jdbc.url" value="jdbc:hsqldb:hsql://localhost/spitter" />
                <property name="toplink.jdbc.user" value="username" />
                <property name="toplink.jdbc.password" value="password" />
            </properties>
        </persistence-unit>
    </presistence>    

      2.在Spring上下文中声明LocalEntityManagerFactoryBean

    @Bean
    public LocalEntityManagerFactoryBean entityManagerFactoryBean(){
        LocalEntityManagerFactoryBean emfb = new LocalEntityManagerFactoryBean();
        emfb.setPesistenceUnitName("spitterPU");
        return emfb;
    }

      创建应用程序管理类型的EntityManagerFactory都是在persistence.xml中进行的。在应用程序管理的场景下,完全由应用程序本身来负责获取EntityManagerFactory,这是通JPA实现的PersistenceProvider做到的。如果每次请求EntityManagerFactory时都需要定义持久化单元,那么代码会迅速膨胀。通过将其配置在persistence.xml中,JPA就能够在特定的位置查找持久化单元定义。

    使用容器管理类型的JPA

      当运行在容器中时,可以使用容器提供的信息来生成EntityManagerFactory。可以将数据源信息配置在Spring应用上下文中,而不是persistence.xml中。

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter jpaVendorAdapater){
        LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
        emfb.setDataSource(dataSource);
        emfb.setJpaVendorAdapter(jpaVendorAdapter);
    emfb.setPackagesToScan("com.domian");
    return emfb; }

      jpaVendorAdapter属性用于指明所使用的是哪个厂商的JPA。Spring提供了多个JPA厂商适配器:EclipseLinkJpaVendorAdapter;HibernateJpaVendorAdapter;OpenJpaVendorAdapter。

    @Bean
    public JpaVendorAdapter jpaVendorAdapter(){
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setDatabase("HSQL");
        adapter.setShowSql(true);
        adapter.setGeneralDdl(false);
        adapter.setDatabasePlatform("org.hibernate.dialect.HSQLDialect");
        return adapter;
    }

      从JNDI获取实体管理工厂

        若将Spring应用程序部署在应用服务器中,EntityManagerFactory可能已经创建好了并且位于JNDI中等待查询使用。在这种情况下,可以使用Spring jee命名空间下的<jee:jndi-lookup>元素来获取对EntityManagerFactory的引用:

      <jee:jndi-lookup id="emf" jndi-name="persistence/spitterPU" />

        通过Java配置来获取EntityManagerFactory

    @Bean
    public JndiObjectFactoryBean entityManagerFactory(){
        JndiObjectFactoryBean jndiObjectFB = new JndiObjectFactoryBean();
        jndiObjectFB.setJndiName("jdbc/SpitterDS");
        return jndiObjectFB;
    }

    基于JPA的Respository

    @Repository
    @Transactional
    public class JpaSpitterRepository implements SpitterRespository{
        @PersistenceUnit
        private EntityManager emf;
        
        public void addSpitter(Spittter spitter){
            emf.persist(spitter);
        }
    
        public Spitter getSpitterById(long id){
            return emf.find(Spitter.class, id);
        }
    
        public void saveSpitter(Spitter spitter){
            emf.merge(spitter);
        }
    
    }

      @PersistenceUnit和@PesistenceContext并不是Spring的注解,它们是有JPA规范提供的。我们需要配置Sprong的PersistenceAnnotationBeanPostProcessor。如果使用<context:annotation-config>或<context:component-scan>,则配置元素会自动注册PersistenceAnnotationBeanPostProcessor bean。否则需要在显式注册这个bean

    @Bean
    public PersistenceAnnotationBeanPostProcessor paPostProcessor(){
        return new PersistenceAnnotationBeanPostProcessor();
    }

    借助Spring Data实现自动化的JPA Repository 

      在xml中配置Spring Data JPA。<jpa:repositories>会扫描它的基础包来查找扩展自Spring Data JPA Respository接口的所有接口。如果发现扩展自Repository的接口,它会自动生成这个接口的实现。

    <?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:jpa="http://www.springframework.org/schema/data/jpa" 
        xsi:schemaLocation="http://www.springframework.org/schema/data/jpa
                       http://www.springframework.org/schema/data/jpa/sping-jpa-1.0.xsd " >
        <jpa:repositories base-package="com.">
    </beans>    

      使用Java配置则只需要在配置类上添加@EnableJpaRespositories注解

    @Configuration
    @EnableJpaRespositories(basePackage="")
    public class JpaConfiguration{
    }
    public interface SpitterRepository extends JpaRepository<Spitter, Long>{
      Spitter findByUsername(String username);
      List<Spitter> readByFirstnameOrLastNameAscFirstnameDest(String firstname, String lastname);
      List<Spitter> readByFirstnameIgnoringCaseOrLastnameIgnoresCase(String firstname, String lastname);
    }

      实际上,我们并不需要实现findByUsername()。方法签名已经告诉Spring Data JPA足够的信息来创建这个方法的实现了。当创建Repository实现的时候,Spring Data会检查Respository接口的所有方法,解析方法的名称,并基于被持久化的对象来视图推测方法的目的。Repository方法是有一个动词、一个可选的主题、关键词By以及一个断言所组成。在findByUsername()这个样例中,动词是find,断言是Username,主题没有指定,暗含的主题是Spitter。Spring Data允许在方法命中使用四种动词:get、read、find和count。get、read和find都会查询数据并返回对象,而count则会返回匹配对象的数量。对于大部分场景来说,主题会被省略。如果主题的名称以Distinct开头的话,那么在生成查询的时候会确保所返回的结果集中不包含重复记录。断言限制结果集的属性。在断言中,会有一个或多个限制结果的条件。每个条件必须引用一个属性,并且还可以指定一种比较操作。如果省略比较操作符的话,会暗指这是一种相等的比较操作。Spring Data提供了以下几种比较操作符:IsAfter、After、IsGreaterThan、GreaterThan、IsGreaterThanEqual、GreaterThanEqual、IsBefore、Before、IsLessThan、LessThan、IsLessThanEqual、LessThanEqual、IsBetween、Between、IsNull、Null、IsNotNull、NotNull、IsIn、In、IsNotIn、NotIn、IsStartingWith、StartingWith、StartsWith、IsEndingWith、EndingWith、EndsWith、IsContaining、Containing、Contains、IsLike、Like、IsNotLike、NotLike、IsTrue、True、IsFalse、False、Is、Equals、IsNot和Not。处理Stirng类型的属性时,条件中可能还会包含IgnoringCase,IgnoresCase,AllIgnoringCase或AllIgnoresCase。可以在方法名称的结尾处添加OrderBy,实现结果集排序。条件部分是通过And或Or进行分割。

    声明自定义查询

      使用@Query方法为Spring Data提供要执行的查询

    public interface SpitterRepository extends JpaRepository<Spitter, Long>{
      Spitter findByUsername(String username);
    
       @Query("select * from spitter where email like '%gmail.com%'")
       List<Spitter> findAllGmailSpitters(); 
    }
  • 相关阅读:
    Android深度探索与HAL驱动开发(卷1)-- 第六章随笔
    Android深度探索与HAL驱动开发(卷1)-- 第五章随笔
    Android深度探索与HAL驱动开发(卷1)-- 第四章随笔
    Android深度探索与HAL驱动开发(卷1)-- 第三章随笔
    Android深度探索与HAL驱动开发(卷1)-- 第二章随笔
    Android深度探索与HAL驱动开发(卷1)-- 第一章随笔
    读《Android深度探索(卷1)HAL与驱动开发》的一些思考10
    读《Android深度探索(卷1)HAL与驱动开发》的一些思考09
    读《Android深度探索(卷1)HAL与驱动开发》的一些思考08
    读《Android深度探索(卷1)HAL与驱动开发》的一些思考07
  • 原文地址:https://www.cnblogs.com/forerver-elf/p/6485627.html
Copyright © 2011-2022 走看看