Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"ruleKey": "S8947",
"hasTruePositives": false,
"falseNegatives": 4,
"falsePositives": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package checks;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;

@Entity
final class JpaEntityFinalCheckJakartaFinalEntity { // Noncompliant {{Remove this "final" modifier from this JPA entity class.}}
//^[sc=1;ec=5]
@Id
private Long id;

public Long getId() {
return id;
}
}

@MappedSuperclass
final class JpaEntityFinalCheckJakartaFinalMappedSuperclass { // Noncompliant {{Remove this "final" modifier from this JPA entity class.}}
//^[sc=1;ec=5]
@Id
private Long id;
}

@Entity
class JpaEntityFinalCheckJakartaCompliantEntity { // Compliant
@Id
private Long id;

public Long getId() {
return id;
}
}

@MappedSuperclass
class JpaEntityFinalCheckJakartaCompliantMappedSuperclass { // Compliant
@Id
private Long id;
}

class JpaEntityFinalCheckJakartaNotAnEntity { // Compliant - not a JPA entity
}

final class JpaEntityFinalCheckJakartaFinalNotAnEntity { // Compliant - not a JPA entity
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package checks;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@Entity
final class JpaEntityFinalCheckJavaxFinalEntity { // Noncompliant {{Remove this "final" modifier from this JPA entity class.}}
//^[sc=1;ec=5]
@Id
private Long id;

public Long getId() {
return id;
}
}

@MappedSuperclass
final class JpaEntityFinalCheckJavaxFinalMappedSuperclass { // Noncompliant {{Remove this "final" modifier from this JPA entity class.}}
//^[sc=1;ec=5]
@Id
private Long id;
}

@Entity
class JpaEntityFinalCheckJavaxCompliantEntity { // Compliant
@Id
private Long id;

public Long getId() {
return id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks;

import java.util.List;
import org.sonar.check.Rule;
import org.sonar.java.model.ModifiersUtils;
import org.sonar.plugins.java.api.IssuableSubscriptionVisitor;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.Modifier;
import org.sonar.plugins.java.api.tree.ModifierKeywordTree;
import org.sonar.plugins.java.api.tree.Tree;

@Rule(key = "S8947")
public class JpaEntityFinalCheck extends IssuableSubscriptionVisitor {

private static final List<String> ENTITY_ANNOTATIONS = List.of(
"javax.persistence.Entity",
"jakarta.persistence.Entity",
"javax.persistence.MappedSuperclass",
"jakarta.persistence.MappedSuperclass"
);

@Override
public List<Tree.Kind> nodesToVisit() {
return List.of(Tree.Kind.CLASS);
}

@Override
public void visitNode(Tree tree) {
ClassTree classTree = (ClassTree) tree;
SymbolMetadata metadata = classTree.symbol().metadata();
if (ENTITY_ANNOTATIONS.stream().noneMatch(metadata::isAnnotatedWith)) {
return;
}

ModifierKeywordTree finalClassModifier = ModifiersUtils.getModifier(classTree.modifiers(), Modifier.FINAL);
if (finalClassModifier != null) {
reportIssue(finalClassModifier, "Remove this \"final\" modifier from this JPA entity class.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SonarQube Java
* Copyright (C) SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* You can redistribute and/or modify this program under the terms of
* the Sonar Source-Available License Version 1, as published by SonarSource Sàrl.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the Sonar Source-Available License for more details.
*
* You should have received a copy of the Sonar Source-Available License
* along with this program; if not, see https://sonarsource.com/license/ssal/
*/
package org.sonar.java.checks;

import org.junit.jupiter.api.Test;
import org.sonar.java.checks.verifier.CheckVerifier;

import static org.sonar.java.checks.verifier.TestUtils.mainCodeSourcesPath;

class JpaEntityFinalCheckTest {

@Test
void testWithJakarta() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/JpaEntityFinalCheckJakartaSample.java"))
.withCheck(new JpaEntityFinalCheck())
.verifyIssues();
}

@Test
void testWithJavax() {
CheckVerifier.newVerifier()
.onFile(mainCodeSourcesPath("checks/JpaEntityFinalCheckJavaxSample.java"))
.withCheck(new JpaEntityFinalCheck())
.verifyIssues();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<p>This issue arises when Java classes annotated with <code>@Entity</code> or <code>@MappedSuperclass</code> are declared <code>final</code>, or when
methods within these classes are declared <code>final</code>. JPA providers require the ability to create proxy subclasses for lazy loading and other
runtime optimizations, which is prevented by the <code>final</code> modifier.</p>
<h2>Why is this an issue?</h2>
<p>JPA (Java Persistence API) providers like Hibernate rely on runtime proxy generation to implement several key features:</p>
<ul>
<li><strong>Lazy loading</strong>: Loading associated entities only when they are actually accessed, rather than eagerly fetching everything
upfront</li>
<li><strong>Dirty checking</strong>: Tracking which fields have changed to optimize database updates</li>
<li><strong>Performance optimizations</strong>: Creating lightweight proxies that defer expensive operations</li>
</ul>
<p>To create these proxies, the JPA provider needs to generate a subclass of your entity class at runtime. This subclass overrides methods to add the
lazy loading and tracking behavior.</p>
<p>When you declare a class or method as <code>final</code>, you prevent inheritance and method overriding. This breaks the proxy mechanism:</p>
<ul>
<li>A <code>final</code> class cannot be subclassed, so no proxy can be created</li>
<li>A <code>final</code> method cannot be overridden, so the JPA provider cannot intercept calls to implement lazy loading</li>
</ul>
<p>Without working proxies, lazy loading fails. Instead of loading data on demand, the JPA provider may fall back to eager loading, which can cause
significant performance problems. In some cases, it may even cause runtime exceptions.</p>
<h3>What is the potential impact?</h3>
<p>When JPA entities or their methods are marked as <code>final</code>, the application can experience:</p>
<ul>
<li><strong>Performance degradation</strong>: Lazy loading fails, forcing the application to eagerly load entire object graphs. This can result in
loading hundreds or thousands of unnecessary records from the database, significantly slowing down queries.</li>
<li><strong>Increased memory consumption</strong>: Eagerly loading large object graphs consumes excessive memory, potentially leading to
OutOfMemoryErrors in production.</li>
<li><strong>Runtime exceptions</strong>: Some JPA providers throw exceptions when they cannot create required proxies, causing application
failures.</li>
<li><strong>Unpredictable behavior</strong>: The application may work correctly in development or testing (with small datasets) but fail or perform
poorly in production (with realistic data volumes).</li>
</ul>
<h2>How to fix it</h2>
<p>Remove the <code>final</code> modifier from the entity class declaration. This allows the JPA provider to create proxy subclasses for lazy loading
and other optimizations.</p>
<h3>Code examples</h3>
<h4>Noncompliant code example</h4>
<pre data-diff-id="1" data-diff-type="noncompliant">
@Entity
public final class User { // Noncompliant
@Id
private Long id;

private String username;

@OneToMany(fetch = FetchType.LAZY)
private List&lt;Order&gt; orders;

// getters and setters
}
</pre>
<h4>Compliant solution</h4>
<pre data-diff-id="1" data-diff-type="compliant">
@Entity
public class User {
@Id
private Long id;

private String username;

@OneToMany(fetch = FetchType.LAZY)
private List&lt;Order&gt; orders;

// getters and setters
}
</pre>
<h2>Resources</h2>
<h3>Documentation</h3>
<ul>
<li>JPA Specification - Entity Classes - <a href="https://jakarta.ee/specifications/persistence/3.0/jakarta-persistence-spec-3.0.html#a121">Official
JPA specification section on entity class requirements</a></li>
<li>Hibernate User Guide - Proxies - <a
href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#BytecodeEnhancement">Hibernate documentation
explaining proxy generation and bytecode enhancement</a></li>
</ul>

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"title": "JPA entity classes should not be final",
"type": "BUG",
"status": "ready",
"remediation": {
"func": "Constant\/Issue",
"constantCost": "5min"
},
"tags": [
"jpa",
"hibernate",
"pitfall"
],
"defaultSeverity": "Critical",
"ruleSpecification": "RSPEC-8947",
"sqKey": "S8947",
"scope": "Main",
"quickfix": "unknown",
"code": {
"impacts": {
"RELIABILITY": "HIGH",
"MAINTAINABILITY": "MEDIUM"
},
"attribute": "LOGICAL"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,7 @@
"S8745",
"S8786",
"S8911",
"S8924"
"S8924",
"S8947"
]
}
Loading