记一次重构

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Mr_Megamind/article/details/82732828

前提

在刚进实验室没多久的时候,就被要求每天写日报,日报写完后要发给一个当周值周的同学。经过观察我发现:值周的同学每天晚上都要比其他人晚走半个小时以上,因为有的人拖着很晚才交,值周的同学还要把每个人的日报汇总成PDF文件,然后发一封邮件给所有学生,还要抄送给老师。

那么,需求就很明显了:

①自动收取日报;
②自动汇总成PDF文件;
③自动编辑最终的邮件,添加②的产出为附件;
④定时发送邮件,不可拖延,过时不候。

设计方面,需求中的②③④很明显用Java Web + Java Mail就可以实现,但是①就需要一个客户端。但是的初步想法有两个:①学习我不熟悉的前端知识,写个网页;②用熟悉的安卓写个客户端。

后来者两种想法都被自己否定了:①时间成本太大,且使用域名需要备案,很麻烦;②让大家在手机上额外装一个App是不能接受的,且很多人不用安卓手机。

之后想到了一个绝妙的办法——借用别人写好的客户端——让大家把日报写成邮件发到一个固定的邮箱。各种邮箱的网页都是各大公司写好的,直接让大家用自己喜欢的邮箱发邮件不就结了!

那么,设计方面就不包含客户端的问题了,纯后端。然后花了大约5天,系统就在自己的云服务器上上线了。 其后,除了日常的一些Bug修复,在学习了设计模式之后重构了一次,主要是调整代码结构。


重构的动机

本来这个系统仅仅用于我们小组的六七个人中,之后被其他小组的同学知道了就加上了他们小组的人,然后用户的数量就变成了十五六个。后来又被其他实验室的老师知道了,然后又加上了他们实验室的学生,用户数量就变成了二十多个人。

通过一段时间的运行和这两次加组,新的问题也是新的需求就出现了:

①这三个组并不能独立得启动/停止,如果某组休假了,需要暂时去掉,就需要修改代码;
②加组的时候也需要修改代码;
③如果某个同学请假了,还需要去修改数据库中的标志位。

看过《大话设计模式》的同学可能会记得一句话:“程序已经开始有‘坏’的味道了”。

因此在学习了Spring之后,就有了这一次重构,目的是把所有的配置信息——数据从程序中分离出去。


从Debug中学习

技术方面的东西全部都在之前的“XXX学习笔记”中了,这次主要是通过没有预期到的多个Bug中学习新知。

Bug 1

情况: 我的程序中有一个预先定义好的Runnable线程,这个线程会在预定的时间读取配置文件,然后动态定义几个执行日报任务的Runnable,可能是因为嵌套定义的问题,部署在服务器上的时候,发生异常并不会报出来,这对我调试程序造成了很大的困扰(因为找报错就找了很久)。

临时解决方案: 放在NetBeans IDE上调试,奇怪的是,在IDE上调试会有两个不同的日志控制台,因此找到了Bug的输出。

长远的考虑:之前为了提高学习的速度,故意把log4j的部分知识略过了,现在想来,这个应该是必要的(写完这篇博文我马上滚去学)。

Bug 2

情况: 因为需要程序一启动就执行一些初始化任务,因此定义了一个Servlet,用init函数执行这些任务,但即使用了@Service注解,且这个Servlet被放在可以被扫描到的service包中,其中的@Autowired等注解仍无效。

分析: 其实这个问题在开始重构前就一直在我的疑虑中。因为熟悉Spring IoC的人应该知道,它只会管理它自己生成的bean,程序员自己生成的bean它是不管的,因此这些bean中被冠以@Autowired注解的依赖也不会被正确注入。说了一堆,关键就是这个Servlet是Tomcat服务器生成的,Spring IoC不管它。

解决方案: 这个问题的关键字不好查,不过最后还是查到了,在init函数最前面加上以下三行代码就可以让Spring IoC管理这个Servlet了:

        WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        AutowireCapableBeanFactory factory = wac.getAutowireCapableBeanFactory();
        factory.autowireBean(this);

解释:
①获取Web应用上下文;
②获取AutowireCapableBeanFactory——自动装配可以装配的Bean的工厂;
③手动让工厂把当前bean(即这个Servlet)也管上。

举一反三:以后再有手动生成的bean想交给Spring IoC容器来管,也可以在bean的构造函数里加上这三行代码。

Bug 3

情况: 调用DAO中的函数并不会执行,且其后的任何操作都不会被执行。

分析:很明显,这一定是DAO里面出Bug了(但是因为Bug 1,很久没有找到异常输出)。

进一步的追溯: 通过缩小范围,发现DAO的Bean没问题,但是调用里面的任何成员函数都会报错:org.hibernate.HibernateException: Could not obtain transaction-synchronized Session for current thread

搜索: 有了异常信息就好办了,查询了一下。因为DAO中用了Hibernate 的sessionFactory.getCurrentSession(),而DAO类未被切入事务切面中。

解决方案: 之前的切面切中了service包中的类的切点:

<aop:pointcut id="serviceMethod"
                      expression=" execution(* com.implementist.xxx.service..*(..))" />

现在再加上dao包中的类的切点就可以了:

<aop:pointcut id="serviceMethod"
                      expression=" execution(* com.implementist.xxx.service..*(..)) 
or execution(* com.implementist.xxx.dao..*(..))" />

Bug 4

背景:动态定义Runnable线程,执行总是出错。 首先,我的工程主要是每天做定时任务,而且每天任务的数量和内容根据配置文件中的内容是不同的。因此需要通过工厂类动态构建Runnable。

情况:关键的构建Runnable的代码如下:

    @Autowired
    TaskFactory taskFactory;
    
    for (int i = 0; i < taskInfos.size(); i++){
        taskFactory.setTaskInfo(taskInfos.get(i));
        Runnable runnable = taskFactory.buildTask();
        ExecuteOnce(taskInfos.get(i).getStartTime(), runnable));
    }

当TaskFactory的作用域设置为单例模式时,所有的任务都会按照taskInfos中的最后一个任务信息来执行。把作用域改成原型模式后,除了最后一个任务,其他任务都报空指针异常。

分析: 关于Runnable线程的问题,因为第一次采用动态构建的方式,所以经过几天的摸索和思考,才想明白:线程,尤其是定时任务要用到的线程:
①任务执行的时间和定义的时间有一段距离;
②任务依赖于构建它时所在的上下文环境,这一条是关键所在。
这两条综合起来分析上述TaskFactory作用域的问题:
当TaskFactory作用域为单例模式时,所有由它构建的Runnable都最终依赖于最后一个setTaskInfo()传入的任务信息;
当TaskFactory作用域为原型模式时,只有最后一个Runnable所依赖的TaskFactory还活着,其他的TaskFactory已经被销毁了。

解决方案:依照Bug 2的解决方案,手动创建TaskFactory,不再注入(因为没有作用域符合要求)。让每一个Runnable都有属于自己的TaskFactory。

    for (int i = 0; i < taskInfos.size(); i++){
        TaskFactory taskFactory = new TaskFactory();
        taskFactory.setTaskInfo(taskInfos.get(i));
        taskFactory.buildTask();
        ExecuteOnce(taskInfos.get(i).getStartTime(), taskFactory.getRunnable()));
    }

Bug 5

情况: 每次在调试时关闭tomcat服务器,时间会比较长,且控制台会报类似下面的警告:

The web application [/XXX] appears to have started a thread named [pool-1-thread-1] but has failed to stop it. This is very likely to create a memory leak.

翻译一下意思就是:应用程序XXX启动了一个叫[pool-1-thread-1]的线程,但是没有关闭它,这可能会造成内存泄漏。

分析: 这确实是我自己的行为,在设置每一个定时任务的第一行代码,就是:

    ScheduledExecutorService periodicExecutor = Executors.newScheduledThreadPool(1);

意为申请一个具有一个线程的线程池。在调用完成之后就再也无法访问到这些线程池了,也因此无法关闭它们。

解决方案:
①定义全局的线程池:

    private ScheduledExecutorService executor;

    @Override
    public void init() {
        executor = Executors.newScheduledThreadPool(4);
    }

②覆盖Servlet的destroy方法,在销毁servlet的时候销毁线程池及里面的所有线程:

    @Override
    public void destroy() {
        executor.shutdownNow();
    }

Bug 6

情况: 这个Bug没有什么技术含量,但是如果不注意,会对程序的鲁棒性造成很大的影响。就是程序中对null的控制。比如下面这段程序:

    public calculate(int[] numbers){
        for (int number : numbers)
            ......
    }

一般情况下,这段程序不会出现问题,但是当函数传进来的numbers没有实例化,而是个null的时候,就会报NullPointorException了。

解决方案:
①一种是在上一级控制好,确保calculate的调用方不会传进来null;
②另一种就是在使用numbers之前在外面套一层判断;


后记

原本花了不到两天就完成了重构,信心满满的觉得就改了改老程序,添了点新东西,应该没问题。从没想过会出现这么多Bug,因此调试花了一周的时间。不过“实践是检验真理的唯一标准”,通过犯错来学习也是我很喜欢的学习方式。
——2018.09.17

其实前天自信了一波,以为当天一修改,程序就没问题了,结果仍然有。目前判断问题在对Runnable线程的理解方面。立一个Flag,这次重构的Bug改不好,就不理头。距离上次理头已经一个月过去了,但愿在变成 “长毛怪” 之前能调的好。
——2018.09.19

19号改完之后,经过连续两天的实际测试,没有再发现异常。其实这次最主要的Bug就是Bug 4,因为之前没有动态构建过Runnable线程,所以不知道它依赖于上下文这个因素。我原本以为只要定义好了Runnable,它所依赖的数据都写死在定义里面,与上下文就没关系了。这个Bug也耗费了大约三四天,终于解决了。我终于可以去理头啦,哈哈哈。
——2018.09.21

猜你喜欢

转载自blog.csdn.net/Mr_Megamind/article/details/82732828