深入理解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