深入理解Spring源码应用之扩展功能,扩展Spring小功能,扩展Feign接口,改为调用所有机器的服务。自己集成mybaits到Spring,实现Mapper接口注入Spring IOC容器。

前言

学习了Spring源码之后如何自己扩展spring的功能呢?今天就举几个实用的小例子

运用一、扩展feign功能,由负载均衡调用单服务改为调用全部服务。

需求与目标

我们知道通过Feign调用服务时,会根据配置的负载均衡策略调用微服务中的一台机器,这也正是微服务的作用之一。
但是此时我们有一个需求,通过Feign调用时需要调用所有服务下的机器接口,用于刷新服务器上的静态缓存。

实现方案

通过Spring提供的BeanPostProcessor接口在初始化完成后对bean进行改造。具体代码如下:

1.新增实现类,实现BeanPostProcessor接口

//这个类本身也要注册为一个Spring的bean,这样才会被Spring调用
@Component
public class FeignPostProcessor implements BeanPostProcessor {

    private Logger logger = LoggerFactory.getLogger(FeignPostProcessor.class.getSimpleName());

	/**
	* 微服务基于Eureka作为注册中心,由于要调用微服务的所有机器,
	* 需要通过该服务获取所有的微服务,用于多机	器调用
	*/
    @Resource
    private DiscoveryClient discoveryClient;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        logger.info("自定义后置处理器接口,beanName:{},beanClass:{}",beanName,bean.getClass());
        FeignClient annotation = ExpressFeign.class.getAnnotation(FeignClient.class);
        Class<?> fallback = annotation.fallback();

		//这里定义好自己需要处理的feign,我这里是ExpressFeign
        if(bean instanceof ExpressFeign ){
            if(fallback != null && bean.getClass().isAssignableFrom(fallback) ){
                logger.info("bean:{},是熔断限流的实现",bean.getClass());
                return bean;
            }
            //这里真正的实现多调用时采用JDK动态代理重新生成bean,重点看InvokeAllFeignInvocationHandler的invoke方法
            return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{ExpressFeign.class},
                    new InvokeAllFeignInvocationHandler(discoveryClient));
        }
        return bean;
    }
}

2. InvokeAllFeignInvocationHandler实现Feign接口多机器调用

public class InvokeAllFeignInvocationHandler implements InvocationHandler {

    private static final String EXPRESS_SERVICE_NAME = "express-service";

    private DiscoveryClient discoveryClient;

    public InvokeAllFeignInvocationHandler(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        List<ServiceInstance> instances = discoveryClient.getInstances(EXPRESS_SERVICE_NAME);
        if(CollUtil.isEmpty(instances)){
            return null;
        }
        //这里暂定返回值为String,数组,实际上多接口调用时不应该有返回值,我们只关心是否调用成功即可。
        List<String> strings = new ArrayList<>();
        for (ServiceInstance instance : instances) {
        	//这里假设Feign接口的请求都是使用的@RequestMapping注解,其他的注解如:@GetMapping都是一样的。
            RequestMapping annotation = method.getAnnotation(RequestMapping.class);
            String path = annotation.value()[0];
            String forObject = new RestTemplate().getForObject(instance.getUri() + path, String.class,args);
            strings.add(forObject);
        }
        return Arrays.toString(strings.toArray());
    }
}

运用二、自己集成mybaits到Spring,实现Mapper接口注入Spring IOC容器。

需求与目标

我们大多数人都在Spring环境中用过Mybatis的功能,通常的使用方式是直接注入Mapper接口即可实现数据库的操作,例如:

public interface UserMapper{
   List<User> selectAll();

}

public class TestService{
	
	@Resource
	private UserMapper userMapper;
	
	public void test(){
		userMapper.selectAll();
	}
}

上面代码中的UserMapper是一个接口,我们并没有为这个接口提供实现类,那么为什么能使用@Resrouce注解注入到Spring中实现数据库的操作呢?这就是我们本次学习需要了解的内容:如何将Mybatis的Mapper接口注入到Spring Ioc容器中。

代码实现

一:公共部分

POM文件

除了常规的Spring相关的依赖,Mysql驱动外,我们只导入Mybatis包。

<!-- 仅仅导入mybatis包,spring官方的mybatis-spring依赖我们不导入,自己实现-->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.3</version>
</dependency>
在resources目录下创建Mybatis配置文件【mybatis-config.xml】
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!--相同的目录下创建properties文件,设置数据库相关的配置信息 -->
    <properties resource="config.properties"/>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${driver}"/>
                <property name="url" value="${url}"/>
                <property name="username" value="${username}"/>
                <property name="password" value="${password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 采用扫描包的方式,让Mybatis自己生成一个Mapper接口的实例-->
        <package name="org.chenglj.mybatis.mapper"/>
        <!--<mapper resource="mapper/UserMapper.xml"/> 取消单个Mybatis的Mapper文件的注册 -->
    </mappers>
</configuration>
创建SqlSessionFactory (org.apache.ibatis.session.SqlSessionFactory)
@Configuration
public class DataSourceConfig {

    /**
     * 配置Mybatis的SqlSessionFactory
     * @return
     * @throws IOException
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        return sqlSessionFactory;
    }
}
采用注解的方式启动spring,创建Spring注解文件
import org.springframework.context.annotation.ComponentScan;

@ComponentScan("org.chenglj.mybatis")
public class MybatisSpringDemo {

}

二、整合版本V1.0

2.1 创建Mybatis接口

public interface UserMapper2 {

	//这里先使用Mybatis提供的注解方式实现sql语句,后面的版本调整为我们最常用的xml文件
    @Select("select * from user limit 10;")
    List<User> selectAll();
}

2.2 通过FactoryBean 注册bean对象。
//这里的@Component注解一定要加上,表示这个类本身也会注册到springIOC容器中,这样Spring才会回调getObject方法注册目标bean对象。
@Component
public class UserMapperFactoryBean implements FactoryBean<UserMapper2> {

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    public UserMapperFactoryBean(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    @Override
    public UserMapper2 getObject() throws Exception {
        return sqlSessionFactory.openSession().getMapper(UserMapper2.class);
    }

    @Override
    public Class<?> getObjectType() {
        return UserMapper2.class;
    }
}
2.3 测试

public class MybatisSpringDemoTest {

    private static AnnotationConfigApplicationContext context ;
    static {
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringDemo.class);
    }
    
	@Test
    public void testV1(){
        context.refresh();
        UserMapper2 bean = context.getBean(UserMapper2.class);
        System.out.println(bean.selectAll());
    }
}    

三、整合版本V2.0

对V1.0中我们注册UserMapper2.java的方式采用Spring的FactoryBean<UserMapper2> 有一个弊端,当我们项目中有多张表时就需要创建多个Factorybean的实现吗?显然这种方式不符合实际需求?(但却能帮我们很好理解Spring)。

3.1.创建一个通用的FactoryBean,用来注册所有的Mybatis的Mapper接口。
public class MapperFactoryBean implements FactoryBean {

	//重点是这里,我们不再写死一个UserMapper2.java文件
    private Class<?> mapperClass;

    public MapperFactoryBean(Class<?> mapperClass) {
        this.mapperClass = mapperClass;
    }

    @Resource
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public Object getObject() throws Exception {
        return sqlSessionFactory.openSession().getMapper(mapperClass);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperClass;
    }
}
3.2 引入一个hutool工具包

有什么用我们等下说到

<dependency>
    <groupId>cn.hutool</groupId>
     <artifactId>hutool-all</artifactId>
     <version>5.7.13</version>
 </dependency>
3.3 测试(注册beanDefinition对象)
public class MybatisSpringDemoTest {

    private static AnnotationConfigApplicationContext context ;
    static {
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringDemo.class);
    }
    
	@Test
    public void testV2(){
    	//1.定义扫描的Mybaits的Mapper接口路径
        String mapperPackage = "org.chenglj.mybatis.mapper";
        //2.使用hutool工具包、扫描包下所有的class文件。
        Set<Class<?>> mapperClasses = ClassUtil.scanPackage(mapperPackage);
        
	        for (Class<?> mapperClazz : mapperClasses) {
	        	//3. 循环创建BeanDefinition对象
	            AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
	                    .genericBeanDefinition(MapperFactoryBean.class)
	                    .getBeanDefinition();
	//这里通过MapperFactoryBean的有参构造器方法将class文件传入
		            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(mapperClazz);
	            
	//4.为Spring上下文循环注册BeanDefition对象
	            context.registerBeanDefinition(mapperClazz.getSimpleName(),beanDefinition);
	        }
	
	        context.refresh();

		//此时我们在UserMapper2的同级包下创建多个Mapper接口均可被注册到Spring中。
		UserMapper2 userMapper2Bean = context.getBean(UserMapper2.class);
        System.out.println(userMapper2Bean.selectAll());

        StudentMapper studentMapper = context.getBean(StudentMapper.class);
        System.out.println(studentMapper.getNameByPrimaryKey(2));
	    }

四、整合版本V3.0

上面的V2.0版本中有一个问题,我们扫描包的路径(代码:String mapperPackage = "org.chenglj.mybatis.mapper";)是在代码中写死的,这样作为公共组件显示是不符合需求的。

4.1实现spring的ImportBeanDefinitionRegistrar接口来注册beanDefition
public class MybatisMapperImportBeanDefinitionRegistrar2 implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {

        Map<String, Object> annotationAttributes = annotationMetadata
                .getAnnotationAttributes(MybatisMapperScan.class.getName());
        String mapperPackage = (String)annotationAttributes.get("value");

        Assert.notNull(mapperPackage,"@MybatisMapperScan value can not be null");

		//这里的代码就是将版本V2.0中的test代码移动到这里
        Set<Class<?>> mapperClasses = ClassUtil.scanPackage(mapperPackage);

        for (Class<?> mapperClazz : mapperClasses) {
            AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder
                    .genericBeanDefinition(MapperFactoryBean.class)
                    .getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(mapperClazz);
            beanDefinitionRegistry.registerBeanDefinition(mapperClazz.getSimpleName(),beanDefinition);
        }

    }
}
4.2 自定义注解,通过@Import注解注入Spring容器
//这里这里的Class是我们刚刚写的
@Import(MybatisMapperImportBeanDefinitionRegistrar2.class)

@Documented
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MybatisMapperScan {
    String value();
}
4.3 在注解类上使用
@ComponentScan("org.chenglj.mybatis")
@MybatisMapperScan("org.chenglj.mybatis.mapper")
public class MybatisSpringImportDemo2 {
}

4.4 测试
public class MybatisSpringDemoTest {

    private static AnnotationConfigApplicationContext context ;
    static {
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringDemo.class);
    }
    
	@Test
    public void testV3(){
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringImportDemo2.class);
        context.refresh();
        
		UserMapper2 userMapper2Bean = context.getBean(UserMapper2.class);
        System.out.println(userMapper2Bean.selectAll());

        StudentMapper studentMapper = context.getBean(StudentMapper.class);
        System.out.println(studentMapper.getNameByPrimaryKey(2));
    }
}  

五、代码整合V4.0

在V2.0和V3.0的整合中我们使用了hutool的工具包、通过扫描指定包下的所有class文件实现了Spring beanDefinition的注册,其实Spring本身有比较成熟的包扫描实现了

5.1继承ClassPathBeanDefinitionScanner实现包扫描

public class MybatisMapperScanner extends ClassPathBeanDefinitionScanner {

    private static Logger logger = LoggerFactory.getLogger(MybatisMapperScanner.class);

    public MybatisMapperScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }



    /*@Override
    public int scan(String... basePackages) {
        int scan = super.scan(basePackages);
        logger.info("scan packages {} nums is {}",basePackages,scan);
        return scan;
    }*/
/*
    方案一、在scan结束后循环修改BeanDefinition中的集合内容
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
        for (BeanDefinitionHolder beanDefinitionHolder : beanDefinitionHolders) {
            BeanDefinition beanDefinition = beanDefinitionHolder.getBeanDefinition();
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
            beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
        }
        return beanDefinitionHolders;
    }
*/
    /**
     * 重写2个isCandidateComponent方法  是否候选的组件,我们只需要扫描接口,默认接口不会被扫描的
     * @param metadataReader
     * @return
     * @throws IOException
     */
    @Override
    protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        return metadataReader.getClassMetadata().isInterface();
    }

    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
        boolean anInterface = beanDefinition.getMetadata().isInterface();
        return anInterface;
    }

    /**
     * 方案二:重新注册beanDefinition方法,此处修改下beanDefinition的ClassName为我们自定义的MapperFactoryBean即可
     * @param definitionHolder
     * @param registry
     */
    /*@Override
    protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = definitionHolder.getBeanDefinition();
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
        beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
        super.registerBeanDefinition(definitionHolder, registry);
    }*/

    /**
     * 重新方案三 推荐
     * @param beanDefinition
     * @param beanName
     */
    @Override
    protected void postProcessBeanDefinition(AbstractBeanDefinition beanDefinition, String beanName) {
        beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(beanDefinition.getBeanClassName());
        //BeanClass和BeanClassName二则设置其一即可,Spring会优先使用beanClassName,注意BeanClassName一定要是Class的全称
        beanDefinition.setBeanClass(MapperFactoryBean.class);
        beanDefinition.setBeanClassName(MapperFactoryBean.class.getName());
        super.postProcessBeanDefinition(beanDefinition, beanName);
    }
}

5.2 重写ImportBeanDefinitionRegistrar
public class MybatisMapperImportBeanDefinitionRegistrarV4 implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {

        Map<String, Object> annotationAttributes = annotationMetadata
                .getAnnotationAttributes(MybatisMapperScanV4.class.getName());
        String mapperPackage = (String)annotationAttributes.get("value");

        Assert.notNull(mapperPackage,"@MybatisMapperScan value can not be null");

        ClassPathBeanDefinitionScanner mapperScan = new MybatisMapperScanner(beanDefinitionRegistry);
        // 【重点】我们只需要调用scan方法即可。
        mapperScan.scan(mapperPackage);
    }
}
5.3注解类上使用
@Import(MybatisMapperImportBeanDefinitionRegistrarV4.class)
@Documented
@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MybatisMapperScanV4 {
    String value();
}
5.4 测试
public class MybatisSpringDemoTest {

    private static AnnotationConfigApplicationContext context ;
    static {
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringDemo.class);
    }
    
	@Test
    public void testV4(){
        context = new AnnotationConfigApplicationContext();
        context.register(MybatisSpringImportDemoV4.class);
        context.refresh();
        
		UserMapper2 userMapper2Bean = context.getBean(UserMapper2.class);
        System.out.println(userMapper2Bean.selectAll());

        StudentMapper studentMapper = context.getBean(StudentMapper.class);
        System.out.println(studentMapper.getNameByPrimaryKey(2));
    }
}  

github源码地址

上述的整合mybatis的完整源码均在github:https://github.com/jurnea/my-batis

  • V1.0分支:mybatis-spring
  • V2.0分支:mybatis-spring-v2
  • V3.0分支:mybatis-spring-v3
  • V4.0分支:mybatis-spring-v4
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值