代碼質量管理工具:SonarQube常見的問題及正確解決方案


SonarQube 簡介

Sonar 是一個用於代碼質量管理的開放平台。通過插件機制,Sonar 可以集成不同的測試工具,代碼分析工具,以及持續集成工具。

與持續集成工具(例如 Hudson/Jenkins 等)不同,Sonar 並不是簡單地把不同的代碼檢查工具結果(例如 FindBugs,PMD 等)直接顯示在 Web 頁面上,而是通過不同的插件對這些結果進行再加工處理,通過量化的方式度量代碼質量的變化,從而可以方便地對不同規模和種類的工程進行代碼質量管理。

在對其他工具的支持方面,Sonar 不僅提供了對 IDE 的支持,可以在 Eclipse 和 IntelliJ IDEA 這些工具里聯機查看結果;同時 Sonar 還對大量的持續集成工具提供了接口支持,可以很方便地在持續集成中使用 Sonar。

此外,Sonar 的插件還可以對 Java 以外的其他編程語言提供支持,對國際化以及報告文檔化也有良好的支持

1."@RequestMapping" 方法應為“ public”

將調用具有@Controller注釋的類的@RequestMapping注釋部分的方法(直接或間接通過元注釋-Spring Boot的@RestController是一個很好的示例)來處理匹配的Web請求。即使該方法是私有的,也會發生這種情況,因為Spring會通過反射調用此類方法,而不檢查可見性。

因此,將敏感方法標記為私有似乎是控制如何調用此類代碼的好方法。不幸的是,並非所有的Spring框架都以這種方式忽略可見性。例如,如果您試圖通過將其標記為@Secured來控制對敏感,私有@RequestMapping方法的Web訪問,則無論用戶是否被授權訪問它,它仍將被調用。這是因為AOP代理不適用於非公開方法。

除了@RequestMapping之外,此規則還考慮了Spring Framework 4.3中引入的注釋:@ GetMapping,@ PostMapping,@ PutMapping,@ DeleteMapping,@ PatchMapping。

2.默認軟件包中不應使用“ @SpringBootApplication”和“ @ComponentScan”

@ComponentScan用於確定哪些Spring Bean在應用程序上下文中可用。可以使用basePackageClasses或basePackages(或其別名值)參數來配置要掃描的軟件包。如果未配置任何參數,則@ComponentScan將僅考慮帶有注釋的類的程序包。在屬於默認包的類上使用@ComponentScan時,將掃描整個類路徑。

這將減慢應用程序的啟動速度,並且該應用程序可能無法啟動BeanDefinitionStoreException,因為您最終掃描了Spring Framework軟件包本身。

在以下情況下,此規則會引起問題:

@ ComponentScan,@ SpringBootApplication和@ServletComponentScan用於默認包的類

@ComponentScan已使用默認程序包顯式配置

不兼容代碼示例

import org.springframework.boot.SpringApplication; @SpringBootApplication //不合規;RootBootApp在默認包中聲明 public class RootBootApp { ... } @ComponentScan("") public class Application { ... } 

兼容解決方案

package hello; import org.springframework.boot.SpringApplication; @SpringBootApplication //合規 RootBootApp屬於“ hello”包 public class RootBootApp { ... } 

3.不應使用雙重檢查鎖定

雙重檢查鎖定是在輸入同步塊之前和之后檢查延遲初始化對象的狀態,以確定是否初始化該對象。

如果不對float或int以外的任何可變實例進行額外同步,則無法以獨立於平台的方式可靠地工作。使用延遲初始化的雙重檢查鎖定任何其他類型的原始或可變對象風險第二個線程使用未初始化或部分初始化成員第一個線程仍然是創建它時,程序崩潰。

有多種解決方法。最簡單的方法是根本不使用雙重檢查鎖定,而是同步整個方法。對於早期版本的JVM,出於性能原因,通常建議不要同步整個方法。但是,在新的JVM中,同步性能已大大提高,因此,現在這是首選的解決方案。如果您希望完全避免使用同步,則可以使用內部靜態類來保存引用。內部靜態類保證延遲加載。

不兼容代碼示例

@NotThreadSafe //線程不安全 public class DoubleCheckedLocking { private static Resource resource; public static Resource getInstance() { if (resource == null) {//2 synchronized (DoubleCheckedLocking.class) { if (resource == null)//1 resource = new Resource(); //第一個線程還沒創建完,只是分配了內存,指了引用,線程執行上面,會判斷resource != null,最終導致程序崩潰 } } return resource; } static class Resource { } } 

兼容解決方案

@ThreadSafe public class SafeLazyInitialization { private static Resource resource; public static synchronized Resource getInstance() { if (resource == null) resource = new Resource(); return resource; } static class Resource { } } 
@ThreadSafe public class ResourceFactory { private static class ResourceHolder { public static Resource resource = new Resource(); // This will be lazily initialised } public static Resource getResource() { return ResourceFactory.ResourceHolder.resource; } static class Resource { } } 
class ResourceFactory { private volatile Resource resource; public Resource getResource() { Resource localResource = resource; if (localResource == null) { synchronized (this) { localResource = resource; if (localResource == null) { resource = localResource = new Resource(); } } } return localResource; } static class Resource { } } 

4.資源應該關閉

在使用后,需要關閉實現Closeable接口或其超級接口AutoCloseable的連接,流,文件和其他類。此外,必須在finally塊中進行關閉調用,否則異常可能使調用無法進行。最好在類實現AutoCloseable時,應使用“ try-with-resources”模式創建資源並將其自動關閉。

無法正確關閉資源將導致資源泄漏,這可能首先導致應用程序崩潰,然后可能使應用程序崩潰。

不兼容代碼示例

private void readTheFile() throws IOException { Path path = Paths.get(this.fileName); BufferedReader reader = Files.newBufferedReader(path, this.charset); // ... reader.close(); // 不合規 // ... Files.lines("input.txt").forEach(System.out::println); // 不合規:需要關閉流 } private void doSomething() { OutputStream stream = null; try { for (String property : propertyList) { stream = new FileOutputStream("myfile.txt"); // 不合規 // ... } } catch (Exception e) { // ... } finally { stream.close(); //打開了多個流。僅最后一個關閉。 } } 

兼容解決方案

private void readTheFile(String fileName) throws IOException { Path path = Paths.get(fileName); try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { reader.readLine(); // ... } // .. try (Stream<String> input = Files.lines("input.txt")) { input.forEach(System.out::println); } } private void doSomething() { OutputStream stream = null; try { stream = new FileOutputStream("myfile.txt"); for (String property : propertyList) { // ... } } catch (Exception e) { // ... } finally { stream.close(); } } 

Java 7引入了try-with-resources語句,該語句隱式關閉Closeables。在try-with-resources語句中打開的所有資源都被該規則忽略。

4.“Random”對象應重復使用

每次需要一個隨機值時,創建一個新的Random對象都是效率低下的,並且可能生成取決於JDK的非隨機數。為了獲得更好的效率和隨機性,請創建一個隨機數,然后存儲並重新使用它。

Random()構造函數每次嘗試為種子設置一個不同的值。但是,不能保證種子將是隨機的,甚至是均勻分布的。一些JDK將當前時間用作種子,這使得生成的數字根本不是隨機的。

該規則查找每次調用方法並將其分配給局部隨機變量時都會創建新的Random的情況。

不兼容代碼示例

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(); //... 

5.不再使用時應清除“ ThreadLocal”變量

一旦保持線程不再存在,就應該對ThreadLocal變量進行垃圾回收。當重新使用保持線程時,可能會發生內存泄漏,在使用線程池的應用程序服務器上就是這種情況。

為避免此類問題,建議始終使用remove()方法清除ThreadLocal變量,以刪除ThreadLocal變量的當前線程值。

另外,調用set(null)刪除值可能會在映射中保留對該指針的引用,這在某些情況下可能導致內存泄漏。使用remove可以更安全地避免此問題。

不兼容代碼示例

public class ThreadLocalUserSession implements UserSession { private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>(); public UserSession get() { UserSession session = DELEGATE.get(); if (session != null) { return session; } throw new UnauthorizedException("User is not authenticated"); } public void set(UserSession session) { DELEGATE.set(session); } public void incorrectCleanup() { DELEGATE.set(null); // //不合規 } // some other methods without a call to DELEGATE.remove() } 

兼容解決方案

public class ThreadLocalUserSession implements UserSession { private static final ThreadLocal<UserSession> DELEGATE = new ThreadLocal<>(); public UserSession get() { UserSession session = DELEGATE.get(); if (session != null) { return session; } throw new UnauthorizedException("User is not authenticated"); } public void set(UserSession session) { DELEGATE.set(session); } public void unload() { DELEGATE.remove(); // 合規 } // ... } 

6.字符串和包裝類型應使用“equals()"進行比較

使用引用相等==或!=比較java.lang.String或包裝類型(如java.lang.Integer)的兩個實例幾乎總是一個錯誤,因為它不是在比較實際值,而是在內存中的位置。

不兼容代碼示例

String firstName = getFirstName(); // String overrides equals String lastName = getLastName(); if (firstName == lastName) { ... }; //不合規;即使字符串具有相同的值,也為false 

兼容解決方案

String firstName = getFirstName();
String lastName = getLastName();
if (firstName != null && firstName.equals(lastName)) { ... }; 

在Java 中包裝類型與基本數據類型存儲位置不同。

Java 基本數據類型存放位置

  • 方法參數、局部變量存放在棧內存中的棧楨中的局部變量表

  • 常量存放在常量池中

包裝類型如Integer存放位置

  • 常量池

  • 堆內存

Integer 存儲在常量池中時可以使用==對比,但當在堆內存中時,使用==對比,實際對比的是兩個內存地址而非值。

根據Integer源碼,

可以看出數值在-128-127時,會使用cache中的數據,其實也就是常量池。超過范圍后新創建Integer,此時數據就無法使用==。

本項規則,主要就是為了避免對比內存地址而引發的錯誤判斷。

7.“ compareTo”不應重載

在實現Comparable.compareTo方法時,參數的類型必須與Comparable聲明中使用的類型匹配。當使用其他類型時,這將創建一個重載而不是一個重寫,這不太可能成為意圖。

當實現Comparable的類的compareTo方法的參數與Comparable聲明中使用的參數不同時,此規則會引起問題。

不兼容代碼示例

public class Foo { static class Bar implements Comparable<Bar> { public int compareTo(Bar rhs) { return -1; } } static class FooBar extends Bar { public int compareTo(FooBar rhs) { //不合規參數類型必須為Bar return 0; } } } 

兼容解決方案

public class Foo { static class Bar implements Comparable<Bar> { public int compareTo(Bar rhs) { return -1; } } static class FooBar extends Bar { public int compareTo(Bar rhs) { return 0; } } } 

8.周年("YYYY")不應用於日期格式

當使用SimpleDateFormat格式化和解析日期時,很少有開發人員會意識到“周年”的Y和“年”的y之間的區別。這很可能是因為對於大多數日期而言,“周年”和“年”是相同的,因此在除該年的第一周或最后一周之外的任何時間進行測試,都會得到y和Y相同的值。但是在12月的最后一周和 一月的第一周,您可能會得到意想不到的結果。

不兼容代碼示例

Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); String result = new SimpleDateFormat("YYYY/MM/dd").format(date); //Noncompliant; yields '2016/12/31' 

兼容解決方案

Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); String result = new SimpleDateFormat("yyyy/MM/dd").format(date); //Yields '2015/12/31' as expected 

異常

Date date = new SimpleDateFormat("yyyy/MM/dd").parse("2015/12/31"); String result = new SimpleDateFormat("YYYY-ww").format(date); //compliant, 'Week year' is used along with 'W 

9.裝箱和拆箱不應連續操作

裝箱是將原始值放入類似對象的過程,例如創建一個Integer來保存一個int值。拆箱是從此類對象中檢索原始值的過程。

由於在裝箱和拆箱期間原始值保持不變,因此在不需要時進行任何操作都是沒有意義的。這也適用於自動裝箱和自動拆箱(當Java為您隱式處理原始/對象轉換時)。

不兼容代碼示例

public void examineInt(int a) { //... } public void examineInteger(Integer a) { // ... } public void func() { int i = 0; Integer iger1 = Integer.valueOf(0); double d = 1.0; int dIntValue = new Double(d).intValue(); // Noncompliant examineInt(new Integer(i).intValue()); // Noncompliant; explicit box/unbox examineInt(Integer.valueOf(i)); // Noncompliant; boxed int will be auto-unboxed examineInteger(i); // Compliant; value is boxed but not then unboxed examineInteger(iger1.intValue()); // Noncompliant; unboxed int will be autoboxed Integer iger2 = new Integer(iger1); // Noncompliant; unnecessary unboxing, value can be reused } 

兼容解決方案

public void examineInt(int a) { //... } public void examineInteger(Integer a) { // ... } public void func() { int i = 0; Integer iger1 = Integer.valueOf(0); double d = 1.0; int dIntValue = (int) d; examineInt(i); examineInteger(i); examineInteger(iger1); } 

10.Boxed "Boolean" should be avoided in boolean expressions

在布爾表達式中應避免使用裝箱的“布爾”

如果將裝箱的類型java.lang.Boolean用作表達式,則如Java語言規范§5.1.8取消裝箱轉換中所定義的,如果該值為null,則它將拋出NullPointerException。

完全避免這種轉換並顯式處理null值是更安全的。

不兼容代碼示例

Boolean b = getBoolean();
if (b) { // Noncompliant, 當b為null,回拋NPE foo(); } else { bar(); } 

兼容解決方案

Boolean b = getBoolean();
if (Boolean.TRUE.equals(b)) { //注意這塊寫法 foo(); } else { bar(); // will be invoked for both b == false and b == null } 

微信公眾號

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM