关联Sonar分析学习笔记一
(1) 避免java包间的循环依赖
首先看看sonar对这条规则的解释:当很多包形成了一个环(包A > 包B > 包C > 包A,">"意味着依赖),这意味着那些包高度耦合,难以在不引用其他所有包的情况下,重用/提取这些包。这样满足不了飞速提升维护应用的需求,也会限制业务变更。这个规则记录了每个拥有外向依赖源代码文件的违规。
下面来看个新增代码的实例:
product包中where4gAtomProduct依赖于继承sqlAtomFactory的两个子类,这两个子类是在factory包中的,在被where4gAtomProduc调用的方法又会依赖于继承DateAtomProduct的两个子类,而这两个子类又是在product包中,这样就有一个product>factory >product包循环依赖。需要注意的是,如果用C#时的VisualStudio开发工具检测到循环依赖会报错,但是我们用的myeclipse是通过的。
那解决循环依赖出路在哪?
方法1:新创建一个A包和B包都依赖的C包。(同样是A和B是循环依赖,我们把B中被A依赖的类放到C中,A和B都依赖于C,这样也可以解决环状依赖。)即新建包放继承DateAtomProduct的两个子类。
方法2: 使用依赖倒置原则(例如:如果A和B是循环依赖,则应该把B中被A依赖的类进行抽象,然后将抽象放入A中,这样A中的类依赖于本包中的抽象而不依赖于B中的实现。)这种方法可能不适用于当前的情况,因为factory包的类是分别调用DateAtomProduct子类的。
老实说,我们在项目中包设计这块工作做的是不够的,新建包和包里包括的类放置都是比较随意的,那么怎么样才能设计较好的包呢?
首先,神马是包,包是神马?包是包容一组类的容器,我们将类进行合理划分后,把类分配到包中。包的设计原则又有哪些呢?(我自己认为学习设计之类的知识,重在掌握原则,而不是具体的方法,原则掌握好了,运用之妙存乎一心,正所谓“任他燎原火,自有倒海水”)
现在,我们已经设计好了一些类,并且它们之间的相互关系也基本明确。由此可见,类的设计先于包的设计。如何将类合理的划分到相应的包中?这里有三个原则:
1)REP(Reuse-ReleaseEquivalence Principles)
重用—发布等价原则,我们对源代码的重用必须是基于包的,可重用的包必须包含可重用的类。
2)CRP(Common-ReusePrinciple)
共同—重用原则,共同重用的类应该同属于一个包,也就是说一组功能相关的类或一组强耦合的类如果它们具有重用性,则它们应该存在于通一个包中。例:容器类以及与它关联的迭代器类。这些类彼此之间紧密耦合在一起,因此必须共同重用,所以它们应该在同一个包中。
3)CCP(Common-ClosurePrinciple)
共同—封闭原则,一个包(包中的所有类)的变化由同一类原因引起的,且应该是共同封闭的。例:界面类的状态变化都是由用户操作引起的,所以界面类应该属于同一个包。从可维护性的角度,一个应用程序中经常需要改动的代码,应该把更改都集中在一个包中。例如我们的业务逻辑类,它们应该放在同一个业务包中进行管理。也就是说:如果类之间有非常紧密的绑定关系,不管是物理上的还是概念上的,它们总是一同变化,这样它们应该属于同一个包。
我们现在进行了基本的包划分,包的数量和功能基本已经确立,此时需要进一步确认就是检查这些包之间的相互关系,规划得是否合理。这里也有三个原则:
1) ADP(Acyclic-Dependencies Principle)
无环—依赖原则,前文已述,这里不再重复了。
2) SDP (Stable-Dependencies Principle)
稳定—依赖原则,包的依赖关系总是朝着稳定的方向进行依赖。也就是说:要使一个软件包是稳定的或要判别出一个软件包是稳定的,那么肯定可行的方法是让许多其他的软件包依赖于它。例如:一个软件中的公共函数包一般是稳定的,且业务包、界面包、操作包等都依赖于公共函数包。我们在设计的包的时候通常要实时考虑到这一点,减少互相依赖,尽量让包都依赖于同一个包,从而满足SDP原则。
3) SAP (Stable-Abstractions Principle)
稳定—抽象原则,大家都知道抽象和接口一般都是很稳定的,所以包的抽象程度应该和其稳定程度一致。也就是说:一个相对稳定的包应该是抽象程度很高的包,这样它具有很好的扩展性同时它也是最稳定的。一个相对不稳定的包应该是具体的即包中的类都是具体的实现,这样它的具体代码经常需要修改同时它也是最不稳定的。
对这些原则的体会和个人的编码经验和思考深度有关,我自己也只是一知半解,写在这里,只是扩大大家知识面的,要写灵活运用这些原则还需进一步编码锤炼。
(2) if(null != groupByTimeValue &&(groupByTimeValue instanceof Integer){…}
之所以直接把语句作为小标题出现,是因为违反的规则很简单:简化条件。但是简单的背后往往是不简单,复杂的背后往往是简单的演绎。为啥子这个幺蛾子说是可以简化条件呢?规则是这样描述的:No need to check for null before an instanceof; the instanceofkeyword returns false when given a null argument.首先找这条语句判断条件的核心了—instanceof,
当instanceof的第一个操作数为null的话,那么第二个操作数无论是什么类型,都应该返回false。也就是说null != groupByTimeValue && groupByTimeValue instanceofInteger这个判断条件可以抽象成a∧(a∧b)而这个条件是和a∧b等价的,所以说判断条件是可以简化的。
那么instanceof这个关键字又是怎样去判断第一个操作为null的情况的呢,首先可以看jvm规范:http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.instanceof
那jvm怎么去实现的呢?首先看一下javac编译类文件成字节码中instanceof的相关过程。查看openJDK源码, instanceof是javac能识别的一个关键字,会对应到Token.INSTANCEOF的token类型。在做词法分析的时候扫描到"instanceof"关键字就映射到了一个Token.INSTANCEOF token。代码路径:openjdk-6-src-b27-26_oct_2012\langtools\src\share\classes\com\sun\tools\javac\parser\token.java
接着做语法分析,解析到instanceof运算符就会生成JCTree.JCInstanceof类型的节点,用于表示instanceof运算。源码路径同上,文件parser.java
最后生成字节码的时候为JCTree.JCInstanceof节点生成instanceof字节码指令,路径及路径如下图所示
字节码生成之后,instanceof在VM Runtime里实现怎么去运行实现的呢?
一个简单的实现是:
jint instanceof(Hjava_lang_Class*c, Hjava_lang_Class* oc)
{
/* Handle the simplest case first -they are the same */
if (c == oc) {
return (1);
}
/* Else if an array check that */
else if (CLASS_IS_ARRAY(c)) {
return (instanceof_array(c,oc));
}
/* Else if an interface check that */
else if (CLASS_IS_INTERFACE(c)) {
return (instanceof_interface(c,oc));
}
/* Else is must to a class */
else {
return (instanceof_class(c,oc));
}
}
基本步骤:
1) obj如果为null,则返回false;否则设S为obj的类型对象,剩下的问题就是检查S是否为T的子类型
2)如果S == T,则返回true;
3)接下来分为3种情况,S是数组类型、接口类型或者类类型。之所以要分情况是因为instanceof要做的是“子类型检查”,而Java语言的类型系统里数组类型、接口类型与普通类类型三者的子类型规定都不一样,必须分开来讨论。对接口类型的instanceof就直接遍历S里记录的它所实现的接口,看有没有跟T一致的;而对类类型的instanceof则是遍历S的super链(继承链)一直到Object,看有没有跟T一致的。遍历类的super链意味着这个算法的性能会受类的继承深度的影响。
值得一提的是,通常使用的HotSpotVM特别针对instanceof做了许多的优化工作。简单来说,优化的主要思路就是把Java语言的特点考虑进来:由于Java的类所继承的超类与所实现的接口都不会在运行时改变,整个继承结构是稳定的,某个类型C在继承结构里的“深度”是固定不变的。也就是说从某个类出发遍历它的super链,总是会遍历到不变的内容。
这样我们就可以把原本要循环遍历super链才可以找到的信息缓存在数组里,并且以特定的下标从这个数组找到我们要的信息。同时,Java的类继承深度通常不会很深,所以为这个缓存数组选定一个固定的长度就足以优化大部分需要做子类型判断的情况。
HotSpot VM具体使用了长度为8的缓存数组,记录某个类从继承深度0到7的超类。HotSpot把类继承深度在7以内的超类叫做“主要超类型”(primary super),把所有其它超类型(接口、数组相关以及超过深度7的超类)叫做“次要超类型”(secondary super)。
对“主要超类型”的子类型判断直接就能判断子类型关系是否成立。这样,类的继承深度对HotSpot VM做子类型判断的性能影响就变得很小了。
对“次要超类型”,则是让每个类型把自己的“次要超类型”混在一起记录在一个数组里,要检查的时候就线性遍历这个数组。留意到这里把接口类型、数组类型之类的子类型关系都直接记录在同一个数组里了,只要在最初初始化数组时就分情况填好了。
举例来说,如果有下述类继承关系:Apple <: Fruit <: Plant <: Object,并且以Object为继承深度0,那么对于Apple类来说,它的主要超类型就有:
0: Object
1: Plant
2: Fruit
3: Apple
这个信息就直接记录在Apple类的primary_supers数组里了。Fruit、Plant等类同理。
如果我们有这样的代码:
Object f = new Apple();
boolean result = f instanceof Plant;
也就是变量f实际指向一个Apple实例,而我们要问这个对象是否是Plant的实例。
可以知道f的实际类型是Apple;要测试的Plant类的继承深度是1,拿Apple类里继承深度为1的主要超类型来看是Plant,马上就能得出结论是true。这样就不需要顺着Apple的继承链遍历过去一个个去看是否跟Plant相等了。
HotSpot VM的两个编译器,ClientCompiler与 Server Compiler各自对子类型判断的实现有更进一步的优化。这里我就没深究了,如果有兴趣可以自己研究。
总结一下,由此可见对instanceof关键字来说,第一个操作数为null的情况已经做了处理,所以没必要再加一个相同的条件判断,所以Sonar在这里报了可以简化条件的违规。
(3) 不要使用printStackTrace
首先看一下规则的描述:AvoidprintStackTrace(); use a logger call instead。意思是避免用printStackTrace要用Log去记录异常。其实仅仅用Log去处理还是不够的,当然相对printStackTrace来说还是好的。
下面请看我们项目里的经典代码:
try
{
…
}
catch(Exception e)
{
e.printStackTrace();//发现新增代码里还有一处用System.out.println(e)的,这都是需要注意的;
}
上面的代码似乎没有什么问题,捕获异常后将异常打印,然后继续执行。事实上在catch块中对发生的异常情况并没有作任何处理(打印异常不能是算是处理异常,因为在程序交付运行后调试信息就没有什么用处了)。这样程序虽然能够继续执行,但是由于这里的操作已经发生异常,将会导致以后的操作并不能按照预期的情况发展下去,可能导致两个结果:
一是由于这里的异常导致在程序中别的地方抛出一个异常,这种情况会使程序员在调试时感到迷惑,因为新的异常抛出的地方并不是程序真正发生问题的地方,也不是发生问题的真正原因。
二是程序继续运行,并得出一个错误的输出结果,这种问题更加难以捕捉,因为很可能把它当成一个正确的输出。
那么应该如何处理呢?这里有四个方法:
(1) 处理异常,进行修复以让程序继续执行。
(2)重新抛出异常,在对异常进行分析后发现这里不能处理它,那么重新抛出异常,让调用者处理。
(3)将异常转换为用户可以理解的自定义异常再抛出,这时应该注意不要丢失原始异常信息。
(4)不要捕获异常。当捕获一个unchecked Exception的时候,必须对异常进行处理;如果认为不必要在这里作处理,就不要捕获该异常,在方法体中声明方法抛出异常,由上层调用者来处理该异常。
当然这些违规需要根据实际情况具体分析具体解决,合理使用JAVA异常机制可以使程序健壮而清晰,但是用好还真不容易,举一反三一下,现在在这里例举几个常见的使用异常需要注意的情况。
1) 不要一次捕获所有的异常
请看下面的代码:
try
{
method1(); //method1抛出ExceptionA
method2(); //method1抛出ExceptionB
method3(); //method1抛出ExceptionC
}
catch(Exceptione)
{
……
}
这是一个很诱人的方案,代码中使用一个catch子句捕获了所有异常,看上去完美而且简洁,事实上很多代码也是这样写的。但这里有两个潜在的缺陷,一是针对try块中抛出的每种Exception,很可能需要不同的处理和恢复措施,而由于这里只有一个catch块,分别处理就不能实现。二是try块中还可能抛出RuntimeException,代码中捕获了所有可能抛出的RuntimeException而没有作任何处理,掩盖了编程的错误,会导致程序难以调试。下面是改正后的正确代码:
try
{
method1(); //method1抛出ExceptionA
method2(); //method1抛出ExceptionB
method3(); //method1抛出ExceptionC
}
catch(ExceptionAe)
{
……
}
catch(ExceptionBe)
{
……
}
catch(ExceptionCe)
{
……
}
2) 使用finally块释放资源
finally关键字保证无论程序使用任何方式离开try块,finally中的语句都会被执行。在以下三种情况下会进入finally块:
(1) try块中的代码正常执行完毕。
(2) 在try块中抛出异常。
(3) 在try块中执行return、break、continue。
因此,当你需要一个地方来执行在任何情况下都必须执行的代码时,就可以将这些
代码放入finally块中。当你的程序中使用了外界资源,如数据库连接,文件等,必须将释放这些资源的代码写入finally块中。
必须注意的是,在finally块中不能抛出异常。JAVA异常处理机制保证无论在任何情况下必须先执行finally块然后在离开try块,因此在try块中发生异常的时候,JAVA虚拟机先转到finally块执行finally块中的代码,finally块执行完毕后,再向外抛出异常。如果在finally块中抛出异常,try块捕捉的异常就不能抛出,外部捕捉到的异常就是finally块中的异常信息,而try块中发生的真正的异常堆栈信息则丢失了。
请看下面的代码:
Connection con = null;
try
{
con = dataSource.getConnection();
……
}
catch(SQLExceptione)
{
……
throw e;//进行一些处理后再将数据库异常抛出给调用者处理
}
finally
{
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
……
}
}
运行程序后,调用者得到的信息如下
java.lang.NullPointerException
at myPackage.MyClass.method1(methodl.java:266)
而不是我们期望得到的数据库异常。这是因为这里的con是null的关系,在finally语句中抛出了NullPointerException,在finally块中增加对con是否为null的判断可以避免产生这种情况。
3) 异常不能影响对象的状态
异常产生后不能影响对象的状态,这是异常处理中的一条重要规则。 在一个函数中发生异常后,对象的状态应该和调用这个函数之前保持一致,以确保对象处于正确的状态中。
如果对象是不可变对象(不可变对象指调用构造函数创建后就不能改变的对象,即 创建后没有任何方法可以改变对象的状态),那么异常发生后对象状态肯定不会改变。如果是可变对象,必须在编程中注意保证异常不会影响对象状态。有三个方法可以达到这个目的:
(1) 将可能产生异常的代码和改变对象状态的代码分开,先执行可能产生异常的代码,如果产生异常,就不执行改变对象状态的代码。
(2) 对不容易分离产生异常代码和改变对象状态代码的方法,定义一个recover方法,在异常产生后调用recover方法修复被改变的类变量,恢复方法调用前的类状态。
(3) 在方法中使用对象的拷贝,这样当异常发生后,被影响的只是拷贝,对象本身不会受到影响。
4) 不要使用同时使用异常机制和返回值来进行异常处理
下面是我们项目中的一段代码
try
{
doSomething();
}
catch(MyExceptione)
{
if(e.getErrcode== -1)
{
……
}
if(e.getErrcode== -2)
{
……
}
……
}
假如在过一段时间后来看这段代码,你能弄明白是什么意思吗?混合使用JAVA异常处理机制和返回值使程序的异常处理部分变得“丑陋不堪”,并难以理解。如果有多种不同的异常情况,就定义多种不同的异常,而不要像上面代码那样综合使用Exception和返回值。
修改后的正确代码如下:
try
{
doSomething(); //抛出MyExceptionA和MyExceptionB
}
catch(MyExceptionAe)
{
……
}
catch(MyExceptionBe)
{
……
}
这个其实也是怎样将我们代码中大量if-else怎么用多态处理的方法,相当于在异常中的应用。
5) 不要让try块过于庞大
出于省事的目的,很多人习惯于用一个庞大的try块包含所有可能产生异常的代码,这样有两个坏处:
一是,阅读代码的时候,在try块冗长的代码中,不容易知道到底是哪些代码会抛出哪些异常,不利于代码维护。
二是,使用try捕获异常是以程序执行效率为代价的,将不需要捕获异常的代码包含在try块中,影响了代码执行的效率。