话接上回,到新公司也有一个月了,慢慢开始接手和熟悉项目,也开始分配给我一些二次开发的内容。这也是大多数入职到新公司后,从熟悉到接手项目的常规流程。这个时候,你就能发现,这个职位是不是招你来填坑的,以及当前项目的前辈们的代码质量咋样,你有没有在这里发现一点亮眼的代码。
这次的问题背景大概是这样的,公司新开发了一个票据业务相关的项目,这个业务分为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,这就整起来很难受。