从实践中理解Java的反射

2024-04-17 15:41:33 浏览数 (2)

问题背景

话接上回,到新公司也有一个月了,慢慢开始接手和熟悉项目,也开始分配给我一些二次开发的内容。这也是大多数入职到新公司后,从熟悉到接手项目的常规流程。这个时候,你就能发现,这个职位是不是招你来填坑的,以及当前项目的前辈们的代码质量咋样,你有没有在这里发现一点亮眼的代码。

这次的问题背景大概是这样的,公司新开发了一个票据业务相关的项目,这个业务分为4个子项目,4个子项目呢,其实是围绕着票据的一生,从开始到结束。理论上对于票据这个主体的内容是相同,只不过是票据的不同生命周期有不同的功能(业务)。

我接手的时候,这块内容的1.0版本已经开发完了,而且是之前两个人开发的,果不其然也是,4个子项目,4个Service,4个Mappper,其实4 个model/entity。而给我的新任务,是在票据业务中加入审批流,也就是4个子项目分别有申请,审批等等,然后再开发一个任务中心(类似OA审批)的功能。

对于审批来说,Java有现成的审批流应用框架,包括activiti和flowable等,这块功能有其他同事来做,我只需要负责调用方法就可以。

解决思路

我的任务中心这个功能,负责查看4个子业务下申请、待办和已办事项。按照常规的方式,新建一个任务中心service,引入4个mapper,然后分不同的接口,复用现有的mapper中的方法。这其实就是“正射”,我在查询之前知道我使用那个mapper去查那张表,也是常规的问题解决思路。

但是呢,这会产生一个问题,大同小异的代码需要写4次,而且在后期调试的时候,某一个地方改动,比如说,额外加一个查询参数等等。那么如果把4个大同小异的内容,整成一个呢,这里就是用反射。

毕竟现在已经有了4个子业务的mapper,里面都写好了完善的CURD功能。那我就可以把不同的功能作为不同的参数,按照不同的参数使用不同的mapper来执行查询(list和count)。

考虑细节

这里在具体写的时候,还需要注意几个地方:

1.需要自己管理SqlSession,按照“正射”的方式,通常就是service的impl实现类中,使用@Autowired 注入mapper对象,这其实用spring来管理。自行管理SqlSession,要注意使用完成后及时释放Session链接。

2.Dao层的Mapper类通常都是接口,在反射的时候,需要使用代理类,这地方是面试经常问的考点,接口的反射需要使用JDK动态代理。

实现代码

代码语言:javascript复制
/**
  TaskConfig对象,存查询的mapper类名,对象的列表查询方法,数量查询方法。
 */
TaskConfig config = configMap.get(reqDto.getFuncCode());

// 拿到SqlSessionFactory,创建SqlSession,SpringUtils是封装的工具。
// 通常使用springboot开发的话,多数使用配置Bean获取
SqlSessionFactory sqlSessionFactory = SpringUtils.getBean("sqlSessionFactory");

// 接收返回结果的对象
PageResultDto<DataWrapperDto> result = null;
// 待反射生成的类
Class interfaceImpl = null;
//查询列表方法
Method listMethod = null;
//查询数量方法
Method numMethod = null;
SqlSession sqlSession = null;
try {
            //打开一个sqlSession ,数据库会话
            sqlSession = sqlSessionFactory.openSession();
            // 获取mapper类,ClassName需要写全名,就是从com.xx.xxxx.mapper.ss
            interfaceImpl = Class.forName(config.getClassName());//这里要写全类名,从包写起,不然找不到classname
            // 获取代理对象
            Object instance = Proxy.newProxyInstance(interfaceImpl.getClassLoader(), new Class[]{interfaceImpl}, new MyInvocationHandler(sqlSession.getMapper(interfaceImpl)));
             // 查询参数的vo对象
            Class searchVoClass = Class.forName(config.getSearchVo());
            Object searchVoObject = searchVoClass.newInstance();

            listMethod = instance.getClass().getMethod(config.getListMethod(), searchVoClass, Pageable.class);
            numMethod = instance.getClass().getMethod(config.getNumMethod(), searchVoClass);

            BeanUtils.copyProperties(reqDto, searchVoObject);
            //需要处理查询参数的key不一样的问题
            setQueryParam(reqDto, searchVoObject);
            // 这是执行查询方法
            List<Object> list = (List<Object>) listMethod.invoke(instance, searchVoObject, pageable);
            Object num = numMethod.invoke(instance, searchVoObject);
            // 分页
            pageable.setTotalCount((Integer) num);
            // .....后续对象操作。

}
catch (InstantiationException e) {
            throw new RuntimeException(e);
        } finally {
        // 一定释放链接,不然数据库链接满了,就卡死了。
            if (sqlSession != null) {
                sqlSession.close();
            }
}

接口代理类:

代码语言:javascript复制
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * @ClassName: MyInvocationHandler

 * @Description: TODO MyInvocationHandler 为mapper提供代理对象
 */
public class MyInvocationHandler implements InvocationHandler {
    private Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //  这里可以实现方法的增强
          //  方法前,比如织入日志功能 打印查询参数
         Object  object   method.invoke(target, args);
          //  方法后,比如打印查询结果
         return object;

    }
}

注意以上内容是核心伪代码,并不能直接run,仅仅是表达下解决思路。具体还会有很多的问题,比如不同的人封装的查询对象不一样,同一个参数的名字也不一样。同一个参数,有的用String,有的用Integer,类型不同、名称不同,内容一样还好,难受的是,有的用“A1,A2”,有的用1、2、3,这就整起来很难受。

0 人点赞