手写@MapperScan

  • 定义一个EnableMapperScan注解
    @Import(MapperProxyHandlerRegister.class) 标注将MapperProxyHandlerRegister导入到容器中。
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(MapperProxyHandlerRegister.class)
public @interface EnableMapperScan {

    String baskPackages();

}
  • MapperProxyHandlerRegister

ImportBeanDefinitionRegistrar 是一个特殊组件,实现了这个接口的组件registry会动态给容器中批量注册组件,这里的关键就是获取到baskPackages 指定的路径,然后批量扫描classpath下的所有的接口,然后定义BeanDefinition,注册到容器中。
注意:AnnotationMetadata metadata封装的是标注了@Import注解的那个类。这样就可以拿到最终因哪个类扫描的这个配置类。

/**
 * Mapper组件注册器
 */
public class MapperProxyHandlerRegister implements ImportBeanDefinitionRegistrar {

    /**
     * @param metadata  注意:metadata是Spring解析@Import注解标注的那个类的元信息,而不是导入的配置类的元信息
     * @param registry  BeanDefinition注册器
     * @param generator BeanName生成器
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                        BeanDefinitionRegistry registry,
                                        BeanNameGenerator generator) {
        //这里的这个metadata封装的是标注了@Import注解的那个类,如果标注@Import注解因其他类似@Enablexxx注解进一步解析到的
        //那这里的metadata封装的就是解析@Enablexxx那个类的元数据信息,
        //总之,metadata封装的就是Spring原始解析类上关联的@Import注解的类
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(EnableMapperScan.class.getName());
        String baskPackages = annotationAttributes.get("baskPackages").toString();
        //获取此包下所有的类
        try {
            Set<Class<?>> scan = scan(baskPackages);
            if (CollUtil.isNotEmpty(scan)){
                //遍历接口
                for (Class<?> mapperClass : scan) {
                    //注册到容器
                    RootBeanDefinition definition = new RootBeanDefinition();
                    definition.setBeanClass(MapperFactoryBean.class);
                    //设置构造方法参数值
                    ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
                    constructorArgumentValues.addIndexedArgumentValue(0,mapperClass);
                    definition.setConstructorArgumentValues(constructorArgumentValues);
                    //definition.setFactoryMethodName("getObject");
                    //注册Bean
                    registry.registerBeanDefinition(mapperClass.getName(),definition);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /*
        1. 创建A   --> 依赖B --> 创建B(依赖A) -> 为A创建代理对象 ,并从缓存中拿,后注入A的是一个代理对象。


    * */
    public static Set<Class<?>> scan(String basePackage) throws IOException, ClassNotFoundException {
        // 创建一个扫描器
        //ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
        // 可以添加过滤器来筛选特定类型的类,这里先不添加任何过滤器,获取所有类
        // 例如,如果只想获取带有特定注解的类,可以添加如下代码
        // scanner.addIncludeFilter(new AnnotationTypeFilter(YourAnnotation.class));
        // 如果只想获取特定接口的实现类,可以添加如下代码
        // scanner.addIncludeFilter(new AssignableTypeFilter(YourInterface.class));
        // 构建要扫描的资源路径模式
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + "/**/*.class";
        Resource[] resources = resolver.getResources(packageSearchPath);
        MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
        Set<Class<?>> classes = new HashSet<>();
        for (Resource resource : resources) {
            MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
            String sourceClassName = metadataReader.getClassMetadata().getClassName();
            if (metadataReader.getClassMetadata().isInterface()){
                //只扫描mapper接口
                classes.add(Class.forName(sourceClassName));
            }
            //System.out.println("sourceClassName = " + sourceClassName);
            // 排除接口和抽象类,只获取具体类
            //if (!metadataReader.getClassMetadata().isInterface() &&!metadataReader.getClassMetadata().isAbstract()) {
            //    String className = metadataReader.getClassMetadata().getClassName();
            //    classes.add(Class.forName(className));
            //}
        }
        return classes;
    }

    private static String resolveBasePackage(String basePackage) {
        return basePackage.replace('.', '/');
    }

  • MapperProxy

这个类封装了代理对象的核心方法,主要就是获取mapper接口上标注了@Select @Update @Delete注解的方法,解析sql参数,获取sqlsessionfactory,然后执行sql。这里简单模拟输出获取到的sql。

/**
    mapper组件动态代理方法拦截实现
 */
public class MapperProxy implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass() == Object.class){
            return method.invoke(this,args);
        }
        String name = method.getDeclaringClass().getName();
        String methodName = method.getName();
        System.out.println("mapperProxy 执行方法,被代理对象拦截 ===》 " + name + "." +methodName + "()");
        Select select = method.getAnnotation(Select.class);
        if (select != null){
            String[] value = select.value();
            //拦截方法
            System.out.println("拦截到的SQL:===> " + Arrays.toString(value));
        }

        Class<?> returnType = method.getReturnType();
        if (Number.class.isAssignableFrom(returnType) || returnType.isPrimitive()){
            return 1;
        }
        return null;
    }
}
  • MapperFactoryBean

这个类实现了FactoryBean 接口,实现了这个接口的类Spring会进一步调用getObject方法创建Bean对象,这个Bean对象的类型是getObjectType返回的数据类型,由此,这个类型就是真正的Mapper接口类型,可以实现基于Mapper接口的自动装配。

public class MapperFactoryBean implements FactoryBean {

    private Class mapperInterFaceClazz;

    public MapperFactoryBean(Class mapperInterFaceClazz){
        this.mapperInterFaceClazz = mapperInterFaceClazz;
    }

    @Override
    public Object getObject() throws Exception {
        /**
            在创建代理对象时,传入需要实现的接口mapperInterFaceClazz
            如果这个接口还继承了其他接口,在基于实现类调方法时,父接口的方法也会被代理对象拦截。
            原理很简单,面向对象三大特征之一: 继承,子类继承父类,就把父类所有的内容全都继承
            JVM在判断时,如果满足继承结构以及安全检查
         */
        return Proxy.newProxyInstance(mapperInterFaceClazz.getClassLoader(),new Class[]{mapperInterFaceClazz},new MapperProxy());
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterFaceClazz;
    }

}
  • MainApp
@SpringBootApplication
@EnableMapperScan(baskPackages = "com.example.ssm3.mapper")
<think>我们讨论的是在Spring框架中,如何使用XML文件手写SQL语句,并结合@Repository注解。在Spring Data JPA中,我们通常使用注解如@Query来写SQL,但有时复杂的SQL我们会选择写在XML文件中。以下是一个完整的解决方案。 ### 步骤1:配置Spring使用XML映射SQL 首先,需要在Spring配置中启用XML映射文件。假设我们使用MyBatis,但实际上Spring Data JPA本身并不直接支持XML映射,因此这里我们使用MyBatis作为示例,因为MyBatis广泛使用XML写SQL。如果你坚持使用JPA,可能需要通过其他方式,但这里给出MyBatis的示例。 #### 配置MyBatis和Spring Boot整合 1. **添加依赖**(Maven): ```xml <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> ``` 2. **配置数据源和MyBatis**(application.yml): ```yaml mybatis: mapper-locations: classpath:mappers/*.xml type-aliases-package: com.example.domain ``` ### 步骤2:创建实体类 ```java package com.example.domain; public class User { private Long id; private String name; private String email; // getters and setters } ``` ### 步骤3:创建Mapper接口 使用@Mapper注解或@Repository注解(需要额外配置),这里使用@Mapper ```java package com.example.mapper; import com.example.domain.User; import org.apache.ibatis.annotations.Mapper; @Mapper // 或者使用@Repository,但需要在配置类上使用@MapperScan public interface UserMapper { User findById(Long id); } ``` ### 步骤4:创建XML映射文件 在`src/main/resources/mappers/UserMapper.xml`中: ```xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.UserMapper"> <select id="findById" resultType="User"> SELECT * FROM users WHERE id = #{id} </select> </mapper> ``` ### 步骤5:在Service中使用 ```java package com.example.service; import com.example.domain.User; import com.example.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final UserMapper userMapper; @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } public User getUserById(Long id) { return userMapper.findById(id); } } ``` ### 步骤6:使用@Repository注解 如果你希望使用@Repository注解,可以这样: 1. 修改Mapper接口: ```java package com.example.mapper; import com.example.domain.User; import org.springframework.stereotype.Repository; @Repository public interface UserMapper { User findById(Long id); } ``` 2. 在配置类上添加@MapperScan,这样MyBatis会扫描这些接口并创建实现: ```java package com.example.config; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Configuration; @Configuration @MapperScan(basePackages = "com.example.mapper") public class MyBatisConfig { } ``` ### 解释 - **@Repository**:这是Spring的注解,用于标记数据访问层(DAO)的类。在MyBatis中,我们将其用于Mapper接口,这样Spring会将其识别为Repository,并处理其中的异常(比如将MyBatis异常转换为Spring的DataAccessException)。 - **XML映射文件**:每个Mapper接口的方法可以通过XML文件中的SQL语句来实现。XML文件中的`namespace`属性必须对应Mapper接口的全限定名,`<select>`等元素的`id`必须对应方法名。 ### 注意事项 1. 确保XML文件在类路径下,并且配置的`mapper-locations`正确。 2. 如果使用@Repository,不要忘记在配置类上使用@MapperScan来扫描Mapper接口。 ### 纯JPA使用XML配置SQL? 如果你使用的是JPA(Hibernate),并且想用XML写SQL,那么Hibernate也支持通过XML配置映射和查询。但是,在Spring Data JPA中,通常不这样使用。如果确实需要,可以考虑使用Hibernate的hbm.xml文件或orm.xml,但这通常用于映射元数据,而不是动态查询。 对于复杂的查询,Spring Data JPA支持通过方法名或@Query注解,或者使用JPA Criteria API。如果查询非常复杂,也可以使用原生SQL查询(通过@Query注解的nativeQuery=true)。 ### 相关问题 §§相关问题§§: 1. 在Spring Boot中,如何配置MyBatis使用XML写SQL? 2. MyBatis中@Mapper和@Repository注解有什么区别? 3. 如何在MyBatis的XML映射文件中编写动态SQL? 4. 除了MyBatis,Spring Data JPA还有哪些方式可以编写复杂的SQL查询?</think>### 使用 XML 手写 SQL 与 `@Repository` 的完整指南 在 Spring 框架中,`@Repository` 注解通常与基于接口的 JPA 方法一起使用,但也可以通过 XML 文件手动编写 SQL 语句。以下是完整的实现方案: --- #### 1. 基础配置:启用 XML SQL 映射 **步骤 1:添加必要的依赖** ```xml <!-- pom.xml --> <dependencies> <!-- Spring Data JPA --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- 数据库驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- XML 处理 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> </dependency> </dependencies> ``` **步骤 2:配置 XML SQL 映射文件** ```xml <!-- src/main/resources/sql-repository.xml --> <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 定义 SQL 查询 --> <bean id="productQueries" class="java.util.HashMap"> <constructor-arg> <map> <!-- 键值对形式存储 SQL --> <entry key="findMaxProductId"> <value> <![CDATA[ SELECT COALESCE(MAX(CAST(p.productid AS UNSIGNED)), 0) FROM product_information p WHERE p.servicetype = :serviceType AND p.productid REGEXP '^[0-9]+$' ]]> </value> </entry> <entry key="findProductsByType"> <value> SELECT * FROM product_information WHERE servicetype = :type ORDER BY created_date DESC </value> </entry> </map> </constructor-arg> </bean> </beans> ``` --- #### 2. 实现 XML SQL 仓库类 ```java // ProductRepository.java @Repository("productSqlRepository") public class ProductSqlRepository { private final Map<String, String> sqlQueries; private final JdbcTemplate jdbcTemplate; // 通过构造函数注入 SQL 映射 @Autowired public ProductSqlRepository( @Qualifier("productQueries") Map<String, String> sqlQueries, JdbcTemplate jdbcTemplate ) { this.sqlQueries = sqlQueries; this.jdbcTemplate = jdbcTemplate; } // 执行 SQL 查询 public Long findMaxProductId(String serviceType) { String sql = sqlQueries.get("findMaxProductId"); return jdbcTemplate.queryForObject( sql, new MapSqlParameterSource("serviceType", serviceType), Long.class ); } // 通用查询方法 public <T> List<T> executeQuery(String queryKey, Map<String, ?> params, RowMapper<T> rowMapper) { String sql = sqlQueries.get(queryKey); return jdbcTemplate.query( sql, new MapSqlParameterSource(params), rowMapper ); } } ``` --- #### 3. 在 Service 层使用 XML SQL ```java // ProductService.java @Service public class ProductService { private final ProductSqlRepository productSqlRepository; @Autowired public ProductService(ProductSqlRepository productSqlRepository) { this.productSqlRepository = productSqlRepository; } public Long getMaxProductId(String serviceType) { return productSqlRepository.findMaxProductId(serviceType); } public List<Product> getProductsByType(String type) { return productSqlRepository.executeQuery( "findProductsByType", Collections.singletonMap("type", type), new ProductRowMapper() ); } // 自定义结果映射器 private static class ProductRowMapper implements RowMapper<Product> { @Override public Product mapRow(ResultSet rs, int rowNum) throws SQLException { Product product = new Product(); product.setId(rs.getLong("id")); product.setName(rs.getString("name")); product.setServiceType(rs.getString("service_type")); return product; } } } ``` --- #### 4. 高级功能:动态 SQL 生成 ```xml <!-- 在 sql-repository.xml 中添加 --> <entry key="dynamicProductSearch"> <value> <![CDATA[ SELECT * FROM product_information WHERE 1=1 <% if (params.containsKey('name')) { %> AND name LIKE CONCAT('%', :name, '%') <% } %> <% if (params.containsKey('minPrice')) { %> AND price >= :minPrice <% } %> ORDER BY ${orderBy} ${direction} LIMIT :limit OFFSET :offset ]]> </value> </entry> ``` ```java // 在 ProductSqlRepository 中添加 public List<Product> searchProducts(Map<String, Object> params) { String template = sqlQueries.get("dynamicProductSearch"); // 使用 SimpleTemplateEngine 渲染 SQL SimpleTemplateEngine engine = new SimpleTemplateEngine(); String renderedSql = engine.createTemplate(template).make(params).toString(); return jdbcTemplate.query( renderedSql, new MapSqlParameterSource(params), new ProductRowMapper() ); } ``` --- #### 5. XML SQL 最佳实践 1. **安全处理** ```xml <!-- 使用 CDATA 避免 XML 转义问题 --> <value><![CDATA[SELECT * FROM table WHERE id = :id]]></value> ``` 2. **参数验证** ```java public Long findMaxProductId(String serviceType) { if (serviceType == null || serviceType.trim().isEmpty()) { throw new IllegalArgumentException("Service type cannot be null"); } // ...执行查询 } ``` 3. **SQL 注入防护** ```java // 使用 Spring 的 NamedParameterJdbcTemplate jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM users WHERE username = :username", new MapSqlParameterSource("username", userInput), // 自动处理转义 Integer.class ); ``` --- ### 完整工作流程 1. **定义 SQL**:在 XML 文件中组织 SQL 语句 2. **注入仓库**:通过 Spring 将 SQL 映射注入 Repository 3. **执行查询**:使用 `JdbcTemplate` 执行 SQL 4. **结果映射**:使用 `RowMapper` 转换结果集 5. **服务调用**:在 Service 层调用 Repository 方法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值