How to Prevent Duplicate Submissions in Java: Simple Backend Solutions
This article explores practical methods to prevent duplicate submissions in Java applications, starting with a simple front‑end button disabling technique and progressing through several backend strategies—including HashMap, fixed‑size array, double‑checked locking, and Apache Commons LRUMap—complete with code samples and performance considerations.
Problem Statement
A friend once asked: What is the simplest way to prevent duplicate submissions in Java? The key points are preventing duplicate submissions and keeping the solution simple.
Simulating the User Scenario
The scenario is illustrated below:
Sample Spring Boot controller:
<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
/**
* Method that may be called repeatedly
*/
@RequestMapping("/add")
public String addUser(String id) {
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Front‑End Interception
Disabling the submit button after a click can block normal duplicate clicks.
Implementation code:
<code><html>
<script>
function subCli() {
// disable button
document.getElementById("btn_sub").disabled = "disabled";
document.getElementById("dv1").innerText = "按钮被点击了~";
}
</script>
<body style="margin-top: 100px;margin-left: 100px;">
<input id="btn_sub" type="button" value=" 提 交 " onclick="subCli()">
<div id="dv1" style="margin-top: 80px;"></div>
</body>
</html>
</code>However, a malicious user can bypass the front end by sending repeated HTTP requests directly.
Back‑End Interception
The idea is to check whether a request has already been processed before executing the business logic.
Using a
HashMapto store processed IDs (basic version):
<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/** Ordinary Map version */
@RequestMapping("/user")
@RestController
public class UserController3 {
// cache ID set
private Map<String, Integer> reqCache = new HashMap<>();
@RequestMapping("/add")
public String addUser(String id) {
// non‑null check omitted
synchronized (this.getClass()) {
// duplicate request check
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
// store request ID
reqCache.put(id, 1);
}
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Problem: the
HashMapgrows indefinitely, consuming memory and slowing look‑ups.
Optimized Version – Fixed‑Size Array
Uses a circular array with an index counter to limit size:
<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RequestMapping("/user")
@RestController
public class UserController {
private static String[] reqCache = new String[100]; // request ID storage
private static Integer reqCacheCounter = 0; // points to next slot
@RequestMapping("/add")
public String addUser(String id) {
// non‑null check omitted
synchronized (this.getClass()) {
// duplicate request check
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
// reset counter if needed
if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0;
reqCache[reqCacheCounter] = id;
reqCacheCounter++;
}
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Version 3 – Double‑Checked Locking (DCL)
Improves performance by separating duplicate check and insertion:
<code>import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
@RequestMapping("/user")
@RestController
public class UserController {
private static String[] reqCache = new String[100];
private static Integer reqCacheCounter = 0;
@RequestMapping("/add")
public String addUser(String id) {
// non‑null check omitted
// duplicate request check
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
synchronized (this.getClass()) {
// double‑checked locking
if (Arrays.asList(reqCache).contains(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0;
reqCache[reqCacheCounter] = id;
reqCacheCounter++;
}
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Note: DCL is suitable for high‑frequency duplicate‑submission scenarios; otherwise it may not be appropriate.
Version 4 – LRUMap
Apache Commons Collections provides
LRUMap, which automatically evicts the least‑recently‑used entries:
<code>import org.apache.commons.collections4.map.LRUMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
// max 100 entries, LRU eviction
private LRUMap<String, Integer> reqCache = new LRUMap<>(100);
@RequestMapping("/add")
public String addUser(String id) {
// non‑null check omitted
synchronized (this.getClass()) {
// duplicate request check
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return "执行失败";
}
// store request ID
reqCache.put(id, 1);
}
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Version 5 – Encapsulated Utility
A reusable utility class centralises the idempotency check:
<code>import org.apache.commons.collections4.map.LRUMap;
/** Idempotency utility */
public class IdempotentUtils {
// LRU map with capacity 100
private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);
/**
* Idempotency judge
*/
public static boolean judge(String id, Object lockClass) {
synchronized (lockClass) {
if (reqCache.containsKey(id)) {
System.out.println("请勿重复提交!!!" + id);
return false;
}
reqCache.put(id, 1);
}
return true;
}
}
</code>Usage in a controller:
<code>import com.example.idempote.util.IdempotentUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController4 {
@RequestMapping("/add")
public String addUser(String id) {
// non‑null check omitted
// ---- Idempotency check start ----
if (!IdempotentUtils.judge(id, this.getClass())) {
return "执行失败";
}
// ---- Idempotency check end ----
// business logic ...
System.out.println("添加用户ID:" + id);
return "执行成功!";
}
}
</code>Tip: For even cleaner code you can create a custom annotation that applies the idempotency check automatically.
Extended Knowledge – LRUMap Implementation Details
The
LRUMapis built on a circular doubly‑linked list with a header node.
<code>AbstractLinkedMap.LinkEntry entry;
</code>When a key is accessed, the entry is moved to the most‑recently‑used (MRU) position:
<code>public V get(Object key, boolean updateToMRU) {
LinkEntry<K, V> entry = this.getEntry(key);
if (entry == null) {
return null;
} else {
if (updateToMRU) {
this.moveToMRU(entry);
}
return entry.getValue();
}
}
protected void moveToMRU(LinkEntry<K, V> entry) {
if (entry.after != this.header) {
++this.modCount;
if (entry.before == null) {
throw new IllegalStateException("Entry.before is null. This should not occur if your keys are immutable, and you have used synchronization properly.");
}
entry.before.after = entry.after;
entry.after.before = entry.before;
entry.after = this.header;
entry.before = this.header.before;
this.header.before.after = entry;
this.header.before = entry;
} else if (entry == this.header) {
throw new IllegalStateException("Can't move header to MRU This should not occur if your keys are immutable, and you have used synchronization properly.");
}
}
</code>When adding a new entry and the map is full, the least‑recently‑used entry (the one after the header) is removed:
<code>protected void addMapping(int hashIndex, int hashCode, K key, V value) {
// check if container is full
if (this.isFull()) {
LinkEntry<K, V> reuse = this.header.after;
boolean removeLRUEntry = false;
if (!this.scanUntilRemovable) {
removeLRUEntry = this.removeLRU(reuse);
} else {
while (reuse != this.header && reuse != null) {
if (this.removeLRU(reuse)) {
removeLRUEntry = true;
break;
}
reuse = reuse.after;
}
if (reuse == null) {
throw new IllegalStateException("Entry.after=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
}
}
if (removeLRUEntry) {
if (reuse == null) {
throw new IllegalStateException("reuse=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
}
this.reuseMapping(reuse, hashIndex, hashCode, key, value);
} else {
super.addMapping(hashIndex, hashCode, key, value);
}
} else {
super.addMapping(hashIndex, hashCode, key, value);
}
}
</code>The
isFull()method simply checks whether the current size has reached the maximum capacity:
<code>public boolean isFull() {
return size >= maxSize;
}
</code>In summary, the article presents six approaches to prevent duplicate submissions: a front‑end button‑disable method and five back‑end implementations (HashMap, fixed‑size array, DCL array, LRUMap, and a packaged LRUMap utility). The LRUMap version offers automatic eviction of stale entries, making it the most robust solution for high‑traffic idempotent operations.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.