Bug、漏洞、异味、安全热点
Bug、Vulnerability、Code Smell、Security Hotspot
阻断、严重、主要、次要、提示
Blocker、Critical、Major、Minor、Info
Code Smell Major
一行不应太长
如果需要水平滚动才能浏览一段代码的话,会影响浏览和理解的效率。
Code Smell Major
不应直接使用标准输出进行日志记录
使用日志记录信息时,必须满足几条重要要求:
用户必须能够轻易取得日志;
日志记录格式必须统一,以提高可读性;
日志数据必须真的被记录下来;
敏感数据必须被安全地记录;
如果一个程序直接写入标准输出,则绝无可能满足上述要求,这就是为何推荐定义并使用一个专门的logger。
不合规代码
System.out.println("My Message"); // 不合规
合规代码
logger.log("My Message");
Code Smell Major
方法不应有过多参数
太长的参数列表说明要么应该创建一个新的结构对大量参数进行包装,要么该方法的功能太多了。
不合规代码
默认最大参数数量阈值为4
public void doSomething(int param1, int param2, int param3, String param4, long param5) {
...
}
合规代码
public void doSomething(int param1, int param2, int param3, String param4) {
...
}
例外
有下列注解的方法可以有很多参数,因为仍有可能进行封装
Spring的@RequestMapping(以及相关注解如@GetRequest);
JAX-RS API注解(如@javax.ws.rs.GET);
用@Autowired注入的Bean构造器;
用@javax.inject.Inject注入的CDI构造器;
@com.fasterxml.jackson.annotation.JsonCreator;
Code Smell Major
不应使用魔法值
魔法值指突然冒出来的数字,并直接用在语句中,这很常见,比如限制循环次数,检查属性值等。当你在写代码时,魔法值看起来显然而直接,但是在调试阶段它们看起来就没有那么显然和直观了。
这就是为何魔法值必须去神秘化,在使用前首先应当将其赋值给一个命名清晰的变量。
-1、0和1不被视作魔法值。
不合规代码
public static void doSomething() {
for(int i = 0; i < 4; i++){ // 不合规,4是魔法值
...
}
}
合规代码
public static final int NUMBER_OF_CYCLES = 4;
public static void doSomething() {
for(int i = 0; i < NUMBER_OF_CYCLES ; i++){
...
}
}
Code Smell Major
代码不应被注释掉
程序员不应注释掉代码,因为此举会使项目臃肿,降低可读性。
不再使用的代码应被删去,如有需要可以从源代码管理历史中找回。
Code Smell Critical
“if … else if”构造应以else语句结尾
最后一个else if应跟一个else语句,该要求属于防御式编程。
else语句要么执行恰当的动作,要么包含合适的注释说明为什么没有执行任何动作。这与switch语句要有最后的default的要求是一致的。
不合规代码
if (x == 0) {
doSomething();
} else if (x == 1) {
doSomethingElse();
}
合规代码
if (x == 0) {
doSomething();
} else if (x == 1) {
doSomethingElse();
} else {
throw new IllegalStateException();
}
Code Smell Critical
控制流语句“if”“for”“while”“switch”和“try”不应嵌套太深
“if”“for”“while”“switch”和“try”语句的嵌套是生产“意面代码”的关键原料。这种代码可读性差,难以重构和维护。
不合规代码
默认嵌套层数阈值是3
if (condition1) { // 合规 - 深度为1
/* ... */
if (condition2) { // 合规 - 深度为2
/* ... */
for(int i = 0; i < 10; i++) { // 合规 - 深度为3不要超过上限
/* ... */
if (condition4) { // 不合规 - 深度为4
if (condition5) { // 深度为5,超过上限,但只在深度为4处报告问题
/* ... */
}
return;
}
}
}
}
Code Smell = Major
局部变量不应遮蔽类字段
重写或遮蔽外部域中声明的变量会极大地影响可读性以及可维护性,甚至会导致维护者以为他在使用一个变量实际上却在使用另一个变量。
不合规代码
class Foo {
public int myField;
public void doSomething() {
int myField = 0;
...
}
}
Code Smell Major
工具类不应有public构造器
工具类是静态成员的集合,不应该被实例化,抽象工具类也不应有public构造器
如果一个类没有显式地定义至少一个构造器,Java就会隐式地添加一个public构造器,因此至少应该定义一个非public的构造器。
不合规代码
class StringUtils { // 不合规
public static String concatenate(String s1, String s2) {
return s1 + s2;
}
}
合规代码
class StringUtils { // 合规
private StringUtils() {
throw new IllegalStateException("Utility class");
}
public static String concatenate(String s1, String s2) {
return s1 + s2;
}
}
例外
当类中包含main方法时,该类不会被视作工具类,会被此规则忽略。
Code Smell Major
方法不能有太多返回语句
一个方法中有太多返回语句增加了方法的本质复杂度(essential complexity,通过入口点、entry point、终止点termination point和不可扣除节点nondeductible node衡量),因为每次遇到一条返回语句,执行流就会被打断,这会降低代码可读性,造成代码逻辑难以理解。
不合规代码
默认返回语句数量阈值为3
public boolean myMethod() { // 不合规;有4条返回语句
if (condition1) {
return true;
} else {
if (condition2) {
return false;
} else {
return true;
}
}
return false;
}
Code Smell Minor
应使用Collection.isEmpty()进行empty检查
使用Collection.size()进行empty检查是可行的,但是使用Collection.isEmpty()可读性更强,性能更高。isEmpty()方法实现的时间复杂度是O(1),而一些size()方法实现的时间复杂度可能是O(n)。
不合规代码
if (myCollection.size() == 0) { // 不合规
/* ... */
}
合规代码
if (myCollection.isEmpty()) {
/* ... */
}
Code Smell Major
异常处理应保留原本的异常信息
当处理捕获的异常时,原本的异常信息和栈轨迹(stack trace)应当记录到日志或继续传递。
不合规代码
try {
/* ... */
} catch (Exception e) { // 不合规 - 异常丢失
LOGGER.info("context");
}
try {
/* ... */
} catch (Exception e) { // 不合规 - 异常丢失(只保留了信息)
LOGGER.info(e.getMessage());
}
try {
/* ... */
} catch (Exception e) { // 不合规 - 原本的异常丢失
throw new RuntimeException("context");
}
合规代码
try {
/* ... */
} catch (Exception e) {
LOGGER.info(e); // 异常被计入日志
}
try {
/* ... */
} catch (Exception e) {
throw new RuntimeException(e); // 异常的栈轨迹被继续传递
}
try {
/* ... */
} catch (RuntimeException e) {
doSomething();
throw e; // 原异常被继续传递
} catch (Exception e) {
throw new RuntimeException(e); // 也允许转换为非检查型异常unchecked exception
}
例外
InterruptedException、NumberFormatException、DateTimeParseException、 ParseException和MalformedURLException按理说是用于指示非异常输出。类似地,使用Java反射API时也要经常处理 NoSuchMethodException。这些异常是Java的一部分,开发者只能处理它们,别无他选。但这条规则并不能证明那些异常得到了正确的处理。
int myInteger;
try {
myInteger = Integer.parseInt(myString);
} catch (NumberFormatException e) {
// 不处理这个异常是完全可以接受的
myInteger = 0;
}
允许异常信息和一些额外信息被一同记录到日志中。
try {
/* ... */
} catch (Exception e) {
String message = "Exception raised while authenticating user: " + e.getMessage();
LOGGER.warn(message); // 合规 - 异常信息和一些上下文信息被一同记录到日志中
}
Code Smell Critical
方法不应为空
有几个理由:
如果这是不小心遗漏的,应该被修复,意面生产中出现意外。
如果该方法还未被支持或不会被支持,则应该抛出UnsupportedOperationException。
如果方法是有意重写为空方法,则应留下注释,解释理由。
不合规代码
public void doSomething() {
}
public void doSomethingElse() {
}
合规代码
@Override
public void doSomething() {
// Do nothing because of X and Y.
}
@Override
public void doSomethingElse() {
throw new UnsupportedOperationException();
}
例外
默认(无参)构造器会被此规则忽略,抽象类里的空方法也会被次规则忽略。
public abstract class Animal {
void speak() {
}
}
Code Smell Critical
字符串字面量不应重复
重复的字符串字面量会使得重构工作更容易出现错误,因为你必须要保证更新其出现的所有位置。另一方面,常量可以引用到很多地方,但只需要在一个地方进行更新。
不合规代码
默认重复次数的阈值是3
public void run() {
prepare("action1"); // 不合规 - "action1"重复3次
execute("action1");
release("action1");
}
@SuppressWarning("all") // 合规 - 注解被排除
private void method1() { /* ... */ }
@SuppressWarning("all")
private void method2() { /* ... */ }
public String method3(String a) {
System.out.println("'" + a + "'"); // 合规 - 短于5个字符的字面量被排除
return "";
}
合规代码
private static final String ACTION_1 = "action1"; // 合规
public void run() {
prepare(ACTION_1); // 合规
execute(ACTION_1);
release(ACTION_1);
}
Code Smell Minor
类或接口的成员声明应按规则排序
按照Oracle定义的Java语言编码规范(Java Code Conventions),类或接口中的成员应按照下列顺序出现在源代码文件中:
类变量
实例变量
构造器
方法
不合规代码
public class Foo{
private int field = 0;
public boolean isTrue() {...}
public Foo() {...} // 不合规,构造器定义在方法后面
public static final int OPEN = 4; // 不合规,变量定义在构造器和方法后面
}
合规代码
public class Foo{
public static final int OPEN = 4;
private int field = 0;
public Foo() {...}
public boolean isTrue() {...}
}
Code Smell Critical
泛型通配符类型不应用于返回类型
强烈建议不要使用通配符类型作为返回类型。因为类型推理规则相当复杂,该API的用户不太可能知道如何正确使用它。
举一个例子,方法返回一个“List”,这个列表中是否可以添加Dog、Cat…我们根本不知道。编译器也不知道,这就是为什么它不允许这样直接使用。通配符类型的使用应该限制在方法参数上。
不合规代码
List<? extends Animal> getAnimals(){...}
合规代码
List<Animal> getAnimals(){...}
或
List<Dog> getAnimals(){...}
Code Smell Critical
方法不应过于复杂
方法的圈复杂度不应超过给定阈值。
复杂的代码的性能可能较差,并且难以理解和维护。
例外
equals和hashCode方法的高复杂度会被忽略。
Code Smell Major
不应使用三元运算符
尽管三元运算符非常紧凑,但会导致代码可读性下降,因此应当避免使用,改为更啰嗦的if/else结构。
不合规代码
System.out.println(i>10?"yes":"no");
合规代码
if (i > 10) {
System.out.println(("yes");
} else {
System.out.println("no");
}
尽管如此,代码紧凑和可读性并非总是冲突,像下面这行代码,使用三元运算符只要简单的1行,改为if/else结构,需要6行代码,就像上文Sonar自己说的更啰嗦,不见得可读性更高。
int result = grade >= threshold ? 1 : 0;
Bug Critical
“Random”对象应当复用
每次需要的时候创建一个新的Random对象是不高效的,有些JDK在这种情况下可能生成的数字并不随机。为了高效和更好的随机性,创建一个Random,储存并重用。
Random()构造器每次都会设置一个不同的数字作为种子。但是种子不保证随机,甚至不保证均匀分布,有些JDK会使用当前时间作为种子,从而使得生成的数字完全不随机。
该规则查找的是方法每次被调用时都创建一个新的Random的情况。
值得注意的是,SecureRandom在生成随机数的质量上确实要比Random好,但是Sonar给出合规代码中使用的SecureRandom.getInstanceStrong()在Linux系统中(尤其是云服务器)会产生阻塞问题,应慎用。直接使用new SecureRandom().nextInt()方法不会产生阻塞。
不合规代码
public void doSomethingCommon() {
Random rand = new Random(); // 不合规;每次调用都创建新的实例
int rValue = rand.nextInt();
//…
合规代码
private Random rand = SecureRandom.getInstanceStrong(); // SecureRandom要好于Random
public void doSomethingCommon() {
int rValue = this.rand.nextInt();
//...
例外
在构造器或main方法里使用Random的情况会被该规则忽略
Code Smell Minor
不访问实例数据的private和final的方法应该是静态的
不访问实例数据的不可重写的(private或final)方法可以是静态的,以免对方法的规约产生误解。
不合规代码
class Utilities {
private static String magicWord = "magic";
private String getMagicWord() { // 不合规
return magicWord;
}
private void setMagicWord(String value) { // 不合规
magicWord = value;
}
}
合规代码
class Utilities {
private static String magicWord = "magic";
private static String getMagicWord() {
return magicWord;
}
private static void setMagicWord(String value) {
magicWord = value;
}
}
例外
若实现了java.io.Serializable接口,则下列方法被该规则排除:
private void writeObject(java.io.ObjectOutputStream out) throws IOException;
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
private void readObjectNoData() throws ObjectStreamException;
Code Smell Major
当键和值都用到时,应使用“entrySet()”进行迭代
当循环中只用到map中的键时,迭代keySet是讲得通的,但是当键和值都用到时,迭代entrySet是更高效的做法,因为这样可以同时拿到键和值,而不是拿到键,再用键去检索值。
不合规代码
public void doSomethingWithMap(Map<String,Object> map) {
for (String key : map.keySet()) { // 不合规,因为每个值都通过键进行检索才能获取
Object value = map.get(key);
// ...
}
}
合规代码
public void doSomethingWithMap(Map<String,Object> map) {
for (Map.Entry<String,Object> entry : map.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
// ...
}
}
Code Smell Major
内部类不应有太多行代码
内部类应该短小精炼,以控制文件的整体复杂度。一个超过了特定阈值的内部类或许应该被放置到独立的文件中称为外部类。
父类“静态”成员不应通过其子类访问
Code Smell Critical
为使代码清晰易读,基类的静态成员不应通过其衍生类访问。这么做会导致误解:存在两个不同的静态成员。
不合规代码
class Parent {
public static int counter;
}
class Child extends Parent {
public Child() {
Child.counter++; // 不合规
}
}
合规代码
class Parent {
public static int counter;
}
class Child extends Parent {
public Child() {
Parent.counter++;
}
}
Security Hotspot Minor
在生产环境中加入开启了调试功能的代码是安全敏感的(security-sensitive)
在生产环境中加入开启了调试功能的代码是安全敏感的,会导致一些安全隐患。一个应用的调试功能可以方便开发者寻找bug,同时也给攻击者带来便利。通常会让攻击者拿到运行着应用的系统和用户的细节信息。
请自问是否有如下行为:
开启应用调试功能的代码或配置部署到了生产服务器或分发给了终端用户;
应用在运行时默认开启了调试功能。
如果你对上述任一问题的回答是肯定的,那么就存在安全风险。
推荐的安全编码实践是不要在生产服务器或分发给终端用户的应用上开启调试功能。
敏感代码示例
Throwable.printStackTrace(…) 打印了Throwable 及其栈踪迹到System.Err(默认),这么做既不易解析,也可能暴露敏感信息:
try {
/* ... */
} catch(Exception e) {
e.printStackTrace(); // 敏感
}
合规代码
应该使用Logger而不是 用printStackTrace去打印Throwable:
try {
/* ... */
} catch(Exception e) {
LOGGER.log("context", e);
}
Spring的@EnableWebSecurity注解的debug参数设置为true,启用了调试支持:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity(debug = true) // 敏感
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
合规代码
Spring的@EnableWebSecurity注解的debug参数设置为false,关闭调试支持:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@Configuration
@EnableWebSecurity(debug = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
Android的WebView.setWebContentsDebuggingEnabled(true)启用了调试支持:
import android.webkit.WebView;
WebView.setWebContentsDebuggingEnabled(true); // 敏感
WebView.getFactory().getStatics().setWebContentsDebuggingEnabled(true); // 敏感
合规代码
Android的WebView.setWebContentsDebuggingEnabled(false)关闭调试支持:
import android.webkit.WebView;
WebView.setWebContentsDebuggingEnabled(false);
WebView.getFactory().getStatics().setWebContentsDebuggingEnabled(false);
Code Smell Major
使用Lambda表达式可以方便而紧凑地注入行为,而无需创建一个专门的类或方法,但是只有当注入的行为可以用几行代码定义时才应该使用lambda表达式,否则源码很快就会变得无法阅读。
[强制]抽象类名要以Abstract或Base开头。异常类名要以Exception结尾。测试用例名要以被测试的类名开头,以Test结尾。
[强制]每个类都应包含作者和日期信息
[推荐]废弃的代码或配置应被坚决地从项目中移除。
备注:
及时移除代码或配置以避免代码冗余
对于临时移除且有可能再次使用的代码,使用///来添加合理注释
正例:
public static void hello() {
/// Business is stopped temporarily by the owner.
// Business business = new Business();
// business.active();
System.out.println("it's finished");
}
必须设置@Transaction注解的rollbackFor属性