@pilllesss/yorn 1.0.182 → 1.0.183
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @pilllesss/yorn might be problematic. Click here for more details.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
# Java 8 / Legacy Stack Code Review Guide
|
|
2
|
+
|
|
3
|
+
Review guidance for codebases still on **Java 8** (and nearby legacy stacks: Spring Boot 2.x, `javax.*`, Hibernate 5). Do **not** require Java 17/21 features (records, text blocks, virtual threads, `ProblemDetail`, etc.) on these PRs.
|
|
4
|
+
|
|
5
|
+
> For modern stacks (Java 17/21 + Spring Boot 3), use the [Java Guide](java.md).
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [When to Use This Guide](#when-to-use-this-guide)
|
|
10
|
+
- [Lambdas & Functional Interfaces](#lambdas--functional-interfaces)
|
|
11
|
+
- [Stream API](#stream-api)
|
|
12
|
+
- [Optional](#optional)
|
|
13
|
+
- [Date/Time API (`java.time`)](#datetime-api-javatime)
|
|
14
|
+
- [Concurrency: Thread Pools & CompletableFuture](#concurrency-thread-pools--completablefuture)
|
|
15
|
+
- [Spring Boot 2](#spring-boot-2)
|
|
16
|
+
- [JPA / Hibernate 5](#jpa--hibernate-5)
|
|
17
|
+
- [Exception Handling](#exception-handling)
|
|
18
|
+
- [Testing](#testing)
|
|
19
|
+
- [Review Checklist](#review-checklist)
|
|
20
|
+
- [References](#references)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## When to Use This Guide
|
|
25
|
+
|
|
26
|
+
| Scenario | Load |
|
|
27
|
+
|----------|------|
|
|
28
|
+
| Java 8 / 11 (no modern syntax), Spring Boot 2.x, `javax.persistence` | **This file** |
|
|
29
|
+
| Java 17/21, Spring Boot 3, `jakarta.*`, virtual threads | [java.md](java.md) |
|
|
30
|
+
| Migrating Boot 2 → Boot 3 | Use both: this guide for legacy pitfalls, `java.md` for the target |
|
|
31
|
+
|
|
32
|
+
Confirm `java.version`, Spring Boot version, and `javax` vs `jakarta` package names in `pom.xml` / `build.gradle` before choosing a guide.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Lambdas & Functional Interfaces
|
|
37
|
+
|
|
38
|
+
### Keep them short; prefer method references
|
|
39
|
+
|
|
40
|
+
```java
|
|
41
|
+
// ❌ Long lambdas are hard to read, test, and debug
|
|
42
|
+
users.stream().forEach(u -> {
|
|
43
|
+
// dozens of lines of business logic...
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ✅ Extract a method, or use a method reference
|
|
47
|
+
users.forEach(this::processUser);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Prefer JDK functional interfaces
|
|
51
|
+
|
|
52
|
+
```java
|
|
53
|
+
// ❌ Unnecessary custom functional interface
|
|
54
|
+
@FunctionalInterface
|
|
55
|
+
interface UserCallback {
|
|
56
|
+
void accept(User u);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ✅ Use Consumer / Function / Predicate / Supplier / BiFunction, etc.
|
|
60
|
+
void process(Consumer<User> callback) { ... }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Captured variables must be effectively final
|
|
64
|
+
|
|
65
|
+
```java
|
|
66
|
+
// ❌ Mutating a captured variable → compile error (or Atomic/array hacks)
|
|
67
|
+
int sum = 0;
|
|
68
|
+
list.forEach(n -> sum += n); // does not compile
|
|
69
|
+
|
|
70
|
+
// ✅ Reduce with a stream, or use an explicit mutable accumulator type
|
|
71
|
+
int sum = list.stream().mapToInt(Integer::intValue).sum();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Stream API
|
|
77
|
+
|
|
78
|
+
### Do not force Streams for simple loops
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
// ❌ Side effects only — Stream adds no value
|
|
82
|
+
items.stream().forEach(item -> process(item));
|
|
83
|
+
|
|
84
|
+
// ✅ Plain for-each is clearer
|
|
85
|
+
for (Item item : items) {
|
|
86
|
+
process(item);
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Collect with `Collectors` on Java 8
|
|
91
|
+
|
|
92
|
+
```java
|
|
93
|
+
// ❌ Stream.toList() is Java 16+ — not available on Java 8
|
|
94
|
+
list.stream().map(...).toList();
|
|
95
|
+
|
|
96
|
+
// ✅ Java 8
|
|
97
|
+
List<Dto> result = list.stream()
|
|
98
|
+
.map(...)
|
|
99
|
+
.collect(Collectors.toList());
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Note: `Collectors.toList()` is not guaranteed immutable. For an unmodifiable list, wrap with `Collections.unmodifiableList(...)` or use Guava / `List.copyOf` (the latter needs a newer JDK).
|
|
103
|
+
|
|
104
|
+
### Two classic `Collectors.toMap` pitfalls
|
|
105
|
+
|
|
106
|
+
```java
|
|
107
|
+
// ❌ Null values → NPE (internally uses Map.merge, which forbids null values)
|
|
108
|
+
Map<Long, String> map = users.stream()
|
|
109
|
+
.collect(Collectors.toMap(User::getId, User::getNickname)); // nickname may be null
|
|
110
|
+
|
|
111
|
+
// ✅ Filter first, or handle nulls explicitly
|
|
112
|
+
Map<Long, String> map = users.stream()
|
|
113
|
+
.filter(u -> u.getNickname() != null)
|
|
114
|
+
.collect(Collectors.toMap(User::getId, User::getNickname));
|
|
115
|
+
|
|
116
|
+
// ❌ Duplicate keys → IllegalStateException
|
|
117
|
+
.collect(Collectors.toMap(User::getName, Function.identity()));
|
|
118
|
+
|
|
119
|
+
// ✅ Provide a merge function
|
|
120
|
+
.collect(Collectors.toMap(User::getName, Function.identity(), (a, b) -> a));
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Be careful with `parallelStream()`
|
|
124
|
+
|
|
125
|
+
```java
|
|
126
|
+
// ❌ Small collections / I/O / shared mutable state — often slower or unsafe
|
|
127
|
+
list.parallelStream().forEach(sharedList::add); // race
|
|
128
|
+
|
|
129
|
+
// ❌ Side-effecting forEach into a non-concurrent collection
|
|
130
|
+
map.entrySet().parallelStream().forEach(e -> result.put(e.getKey(), e.getValue()));
|
|
131
|
+
|
|
132
|
+
// ✅ Consider only for CPU-bound work, no shared mutable state, and large enough data
|
|
133
|
+
// ✅ Collect with toMap / toConcurrentMap — do not forEach into an external Map
|
|
134
|
+
Map<K, V> result = list.parallelStream()
|
|
135
|
+
.collect(Collectors.toConcurrentMap(Item::getKey, Item::getValue, (a, b) -> a));
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Parallel streams use `ForkJoinPool.commonPool()` by default and compete with other parallel / CompletableFuture work in the same process.
|
|
139
|
+
|
|
140
|
+
### Do not mutate the stream source; avoid nested `forEach`
|
|
141
|
+
|
|
142
|
+
```java
|
|
143
|
+
// ❌ Mutating the source during the pipeline → ConcurrentModificationException
|
|
144
|
+
list.stream().peek(list::add).count();
|
|
145
|
+
|
|
146
|
+
// ❌ Nested forEach is hard to read and hard to short-circuit
|
|
147
|
+
a.forEach(x -> b.forEach(y -> ...));
|
|
148
|
+
|
|
149
|
+
// ✅ Prefer flatMap or ordinary loops for cartesian / join-style logic
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Prefer primitive streams to avoid boxing
|
|
153
|
+
|
|
154
|
+
```java
|
|
155
|
+
// ❌ Stream<Integer> boxing overhead
|
|
156
|
+
int sum = list.stream().map(Order::getAmount).reduce(0, Integer::sum);
|
|
157
|
+
|
|
158
|
+
// ✅ IntStream / LongStream / DoubleStream
|
|
159
|
+
int sum = list.stream().mapToInt(Order::getAmount).sum();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Optional
|
|
165
|
+
|
|
166
|
+
**Intent:** express possible absence as a **return type**, not as a general null replacement.
|
|
167
|
+
|
|
168
|
+
```java
|
|
169
|
+
// ❌ Optional as field / parameter / collection element (serialization, reflection, API noise)
|
|
170
|
+
class User {
|
|
171
|
+
private Optional<String> email;
|
|
172
|
+
}
|
|
173
|
+
void send(Optional<String> email) { ... }
|
|
174
|
+
Optional<List<Order>> findOrders(); // empty list already means "none"
|
|
175
|
+
|
|
176
|
+
// ✅ Return type only; return empty collections for "none"
|
|
177
|
+
public Optional<User> findById(Long id) { ... }
|
|
178
|
+
public List<Order> findOrders(Long userId) { ... } // emptyList when none
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Do not use `isPresent()` + `get()` as a null check
|
|
182
|
+
|
|
183
|
+
```java
|
|
184
|
+
// ❌ More verbose than a null check, and get() can still blow up
|
|
185
|
+
if (userOpt.isPresent()) {
|
|
186
|
+
return userOpt.get().getName();
|
|
187
|
+
}
|
|
188
|
+
return "Unknown";
|
|
189
|
+
|
|
190
|
+
// ✅ Functional chain (available on Java 8)
|
|
191
|
+
return userOpt.map(User::getName).orElse("Unknown");
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `orElse` vs `orElseGet`
|
|
195
|
+
|
|
196
|
+
```java
|
|
197
|
+
// ❌ orElse argument is always evaluated (even when the Optional is present)
|
|
198
|
+
return findUser(id).orElse(loadDefaultFromDb()); // always hits DB
|
|
199
|
+
|
|
200
|
+
// ✅ Expensive defaults → orElseGet
|
|
201
|
+
return findUser(id).orElseGet(this::loadDefaultFromDb);
|
|
202
|
+
|
|
203
|
+
// ✅ Required value (Java 8)
|
|
204
|
+
return findUser(id).orElseThrow(() -> new UserNotFoundException(id));
|
|
205
|
+
// Note: no-arg orElseThrow() is Java 10+; Java 8 requires a Supplier
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `of` vs `ofNullable`; nest with `flatMap`
|
|
209
|
+
|
|
210
|
+
```java
|
|
211
|
+
// ❌ of(null) → immediate NPE
|
|
212
|
+
Optional.of(possiblyNull);
|
|
213
|
+
|
|
214
|
+
// ✅
|
|
215
|
+
Optional.ofNullable(possiblyNull);
|
|
216
|
+
|
|
217
|
+
// ❌ map returning Optional → Optional<Optional<T>>
|
|
218
|
+
optional.map(this::findOther); // findOther returns Optional
|
|
219
|
+
|
|
220
|
+
// ✅
|
|
221
|
+
optional.flatMap(this::findOther);
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Java 8 has **no** `Optional.stream()` / `ifPresentOrElse` / `or` (those are Java 9+). Filtering a collection of Optionals:
|
|
225
|
+
|
|
226
|
+
```java
|
|
227
|
+
list.stream()
|
|
228
|
+
.map(this::findUser)
|
|
229
|
+
.filter(Optional::isPresent)
|
|
230
|
+
.map(Optional::get) // OK after filter; or extract a helper
|
|
231
|
+
.collect(Collectors.toList());
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Date/Time API (`java.time`)
|
|
237
|
+
|
|
238
|
+
One of the most common production footguns in legacy systems: keeping `Date` / `Calendar` / `SimpleDateFormat`.
|
|
239
|
+
|
|
240
|
+
```java
|
|
241
|
+
// ❌ SimpleDateFormat is not thread-safe; a shared static instance corrupts state
|
|
242
|
+
private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd");
|
|
243
|
+
|
|
244
|
+
// ✅ DateTimeFormatter is immutable and thread-safe
|
|
245
|
+
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Pick the right type
|
|
249
|
+
|
|
250
|
+
| Type | Use for |
|
|
251
|
+
|------|---------|
|
|
252
|
+
| `Instant` | Machine timestamps, audit, cross-service events (UTC timeline) |
|
|
253
|
+
| `LocalDate` | Date only (birthday, business day) |
|
|
254
|
+
| `LocalDateTime` | Date-time **without** zone; do not use for "when an event happened" |
|
|
255
|
+
| `ZonedDateTime` / `OffsetDateTime` | Human time that needs a zone or offset |
|
|
256
|
+
|
|
257
|
+
```java
|
|
258
|
+
// ❌ LocalDateTime for "order placed at" — ambiguous across zones / DST
|
|
259
|
+
private LocalDateTime createdAt = LocalDateTime.now();
|
|
260
|
+
|
|
261
|
+
// ✅ Instant for event time; convert to a zone only for display
|
|
262
|
+
private Instant createdAt = Instant.now();
|
|
263
|
+
|
|
264
|
+
// ❌ now() depends on the JVM default zone — CI / prod / laptop disagree
|
|
265
|
+
LocalDate.now();
|
|
266
|
+
|
|
267
|
+
// ✅ Explicit ZoneId, or inject Clock for tests
|
|
268
|
+
LocalDate.now(ZoneOffset.UTC);
|
|
269
|
+
LocalDate.now(clock);
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Formatting traps
|
|
273
|
+
|
|
274
|
+
```java
|
|
275
|
+
// ❌ YYYY is week-based year — wrong year near year boundaries
|
|
276
|
+
DateTimeFormatter.ofPattern("YYYY-MM-dd");
|
|
277
|
+
|
|
278
|
+
// ✅ Calendar year uses yyyy
|
|
279
|
+
DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Interop with legacy APIs: `date.toInstant()`, `Date.from(instant)`, `LocalDateTime.ofInstant(instant, zone)`.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Concurrency: Thread Pools & CompletableFuture
|
|
287
|
+
|
|
288
|
+
Java 8 has **no virtual threads**. I/O-heavy work needs a well-sized pool — not `newCachedThreadPool` or an unbounded queue.
|
|
289
|
+
|
|
290
|
+
```java
|
|
291
|
+
// ❌ Unbounded queue + default rejection — latency explodes or OOM under load
|
|
292
|
+
ExecutorService exec = Executors.newFixedThreadPool(8); // unbounded queue
|
|
293
|
+
|
|
294
|
+
// ❌ Never shut down → thread leak
|
|
295
|
+
Executors.newFixedThreadPool(8).submit(task);
|
|
296
|
+
|
|
297
|
+
// ✅ Bounded queue + explicit rejection policy + lifecycle management
|
|
298
|
+
ThreadPoolExecutor exec = new ThreadPoolExecutor(
|
|
299
|
+
8, 16, 60L, TimeUnit.SECONDS,
|
|
300
|
+
new ArrayBlockingQueue<>(500),
|
|
301
|
+
new ThreadPoolExecutor.CallerRunsPolicy());
|
|
302
|
+
// On shutdown: shutdown → awaitTermination → shutdownNow
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### CompletableFuture
|
|
306
|
+
|
|
307
|
+
```java
|
|
308
|
+
// ❌ I/O on the common pool (supplyAsync with no Executor)
|
|
309
|
+
CompletableFuture.supplyAsync(() -> restTemplate.getForObject(url, Dto.class));
|
|
310
|
+
|
|
311
|
+
// ✅ Explicit I/O executor
|
|
312
|
+
CompletableFuture.supplyAsync(() -> callRemote(), ioExecutor);
|
|
313
|
+
|
|
314
|
+
// ❌ thenApply returning a CF → nested CompletableFuture<CompletableFuture<T>>
|
|
315
|
+
.thenApply(id -> findAsync(id));
|
|
316
|
+
|
|
317
|
+
// ✅ Dependent async work → thenCompose
|
|
318
|
+
.thenCompose(id -> findAsync(id));
|
|
319
|
+
|
|
320
|
+
// ❌ get()/join() inside async callbacks — starves the pool or deadlocks
|
|
321
|
+
.thenApply(x -> other.join());
|
|
322
|
+
|
|
323
|
+
// ✅ Combine with allOf / thenCombine; join once at the boundary
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Timeouts:** Java 8 has no `orTimeout` / `completeOnTimeout` (Java 9+). Use `get(timeout, unit)`, or a scheduler + `applyToEither` that completes exceptionally.
|
|
327
|
+
|
|
328
|
+
**Exceptions:** Attach `exceptionally` / `handle` / `whenComplete` at the chain boundary so failures are not swallowed.
|
|
329
|
+
|
|
330
|
+
```java
|
|
331
|
+
// ❌ Shared mutable SimpleDateFormat / HashMap as a cache
|
|
332
|
+
private static final SimpleDateFormat SDF = ...;
|
|
333
|
+
private final Map<String, String> cache = new HashMap<>(); // concurrent puts
|
|
334
|
+
|
|
335
|
+
// ✅ ConcurrentHashMap; dates via java.time
|
|
336
|
+
private final ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Spring Boot 2
|
|
342
|
+
|
|
343
|
+
Packages are **`javax.*`**, not `jakarta.*`. Do not require a Jakarta migration unless the PR's goal is upgrading to Boot 3.
|
|
344
|
+
|
|
345
|
+
### Dependency injection
|
|
346
|
+
|
|
347
|
+
```java
|
|
348
|
+
// ❌ Field @Autowired: hard to test, opaque dependencies
|
|
349
|
+
@Autowired
|
|
350
|
+
private UserRepository userRepo;
|
|
351
|
+
|
|
352
|
+
// ✅ Constructor injection (Boot 2: single constructor can omit @Autowired)
|
|
353
|
+
private final UserRepository userRepo;
|
|
354
|
+
|
|
355
|
+
public UserService(UserRepository userRepo) {
|
|
356
|
+
this.userRepo = userRepo;
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### Configuration
|
|
361
|
+
|
|
362
|
+
```java
|
|
363
|
+
// ❌ Hard-coded secrets; @Value scattered everywhere
|
|
364
|
+
@Value("${app.payment.api-key}")
|
|
365
|
+
private String apiKey;
|
|
366
|
+
|
|
367
|
+
// ✅ @ConfigurationProperties (Java 8 uses a class, not a record)
|
|
368
|
+
@ConfigurationProperties(prefix = "app.payment")
|
|
369
|
+
public class PaymentProperties {
|
|
370
|
+
private String apiKey;
|
|
371
|
+
private int timeoutMs;
|
|
372
|
+
// getters / setters
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Register with `@EnableConfigurationProperties`. On Boot **2.2+**, `@ConfigurationPropertiesScan` also works (`@SpringBootApplication` scans the startup class package by default).
|
|
377
|
+
|
|
378
|
+
### RestTemplate must have timeouts
|
|
379
|
+
|
|
380
|
+
Default is **infinite wait**. A hung downstream can exhaust Tomcat / worker threads.
|
|
381
|
+
|
|
382
|
+
```java
|
|
383
|
+
// ❌ Bare new RestTemplate() — no timeouts
|
|
384
|
+
return new RestTemplate();
|
|
385
|
+
|
|
386
|
+
// ✅ Boot 2.1+ Duration-based timeouts
|
|
387
|
+
@Bean
|
|
388
|
+
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
|
389
|
+
return builder
|
|
390
|
+
.setConnectTimeout(Duration.ofSeconds(2))
|
|
391
|
+
.setReadTimeout(Duration.ofSeconds(5))
|
|
392
|
+
.build();
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
If you customize `ClientHttpRequestFactory` (e.g. Apache HttpClient), set connect / read / connectionRequest timeouts on the factory too — otherwise builder timeouts may be ignored.
|
|
397
|
+
|
|
398
|
+
### Transaction proxy trap (same on Boot 2/3)
|
|
399
|
+
|
|
400
|
+
```java
|
|
401
|
+
// ❌ Same-class self-call — @Transactional does not apply (proxy not invoked)
|
|
402
|
+
public void create(Order o) {
|
|
403
|
+
save(o); // internal call
|
|
404
|
+
}
|
|
405
|
+
@Transactional
|
|
406
|
+
public void save(Order o) { ... }
|
|
407
|
+
|
|
408
|
+
// ✅ Put the transaction boundary on the public entry point, or split into another bean
|
|
409
|
+
@Transactional
|
|
410
|
+
public void create(Order o) { saveInternal(o); }
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`@Transactional` on `private` methods is also ineffective.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## JPA / Hibernate 5
|
|
418
|
+
|
|
419
|
+
Entity annotations come from `javax.persistence.*`.
|
|
420
|
+
|
|
421
|
+
### N+1
|
|
422
|
+
|
|
423
|
+
> Cross-language background: [N+1 query guide](cross-cutting/n-plus-one-queries.md)
|
|
424
|
+
|
|
425
|
+
```java
|
|
426
|
+
// ❌ EAGER, or loops that trigger lazy loads
|
|
427
|
+
@OneToMany(fetch = FetchType.EAGER)
|
|
428
|
+
private List<Order> orders;
|
|
429
|
+
|
|
430
|
+
for (User u : userRepo.findAll()) {
|
|
431
|
+
u.getOrders().size(); // N queries when lazy
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ✅ JOIN FETCH / @EntityGraph; keep LAZY by default
|
|
435
|
+
@Query("SELECT u FROM User u JOIN FETCH u.orders")
|
|
436
|
+
List<User> findAllWithOrders();
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Transactions & read-only
|
|
440
|
+
|
|
441
|
+
```java
|
|
442
|
+
// ❌ Transactions opened in Controllers; or @Transactional on private methods
|
|
443
|
+
// ✅ Public Service methods; mark reads readOnly
|
|
444
|
+
@Transactional(readOnly = true)
|
|
445
|
+
public User get(Long id) { ... }
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Entities & Lombok
|
|
449
|
+
|
|
450
|
+
```java
|
|
451
|
+
// ❌ @Data equals/hashCode often pulls in lazy associations
|
|
452
|
+
@Entity
|
|
453
|
+
@Data
|
|
454
|
+
public class User { ... }
|
|
455
|
+
|
|
456
|
+
// ✅ @Getter/@Setter; equals/hashCode on a stable business key, or null-safe id
|
|
457
|
+
// ⚠️ Do not include lazy associations; new unsaved entities all have null id,
|
|
458
|
+
// so id-only equals treats them as unequal (usually acceptable)
|
|
459
|
+
@Entity
|
|
460
|
+
@Getter
|
|
461
|
+
@Setter
|
|
462
|
+
public class User {
|
|
463
|
+
@Id
|
|
464
|
+
private Long id;
|
|
465
|
+
|
|
466
|
+
@Override
|
|
467
|
+
public boolean equals(Object o) {
|
|
468
|
+
if (this == o) return true;
|
|
469
|
+
if (!(o instanceof User)) return false;
|
|
470
|
+
return id != null && id.equals(((User) o).id);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
@Override
|
|
474
|
+
public int hashCode() {
|
|
475
|
+
return getClass().hashCode();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Temporal fields
|
|
481
|
+
|
|
482
|
+
Hibernate 5 supports `java.time`, but mapping `ZonedDateTime` to a zone-less `TIMESTAMP` normalizes with the JVM zone and drifts across regions. Prefer:
|
|
483
|
+
|
|
484
|
+
- Store UTC: `Instant` or `OffsetDateTime`
|
|
485
|
+
- Set `spring.jpa.properties.hibernate.jdbc.time_zone=UTC` when the team agrees
|
|
486
|
+
- Migrate legacy `java.util.Date` fields deliberately; do not add new `Date` / `@Temporal` in new code
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Exception Handling
|
|
491
|
+
|
|
492
|
+
Boot 2 does not treat Spring 6 `ProblemDetail` as a first-class citizen (that is a Boot 3 story). Use a shared `@ControllerAdvice` with clear HTTP statuses.
|
|
493
|
+
|
|
494
|
+
```java
|
|
495
|
+
// ❌ Swallow exceptions, printStackTrace, return null to hide failure
|
|
496
|
+
try {
|
|
497
|
+
userService.create(user);
|
|
498
|
+
} catch (Exception e) {
|
|
499
|
+
e.printStackTrace();
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ✅ Domain exceptions + global handler
|
|
504
|
+
@RestControllerAdvice
|
|
505
|
+
public class GlobalExceptionHandler {
|
|
506
|
+
@ExceptionHandler(UserNotFoundException.class)
|
|
507
|
+
public ResponseEntity<ApiError> handleNotFound(UserNotFoundException e) {
|
|
508
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
509
|
+
.body(new ApiError("USER_NOT_FOUND", e.getMessage()));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Close resources with try-with-resources (Java 7+). Avoid hand-rolled `finally { close() }` that forgets null checks.
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
## Testing
|
|
519
|
+
|
|
520
|
+
```java
|
|
521
|
+
// ❌ @SpringBootTest for every unit test (slow and brittle)
|
|
522
|
+
@SpringBootTest
|
|
523
|
+
public class UserServiceTest { ... }
|
|
524
|
+
|
|
525
|
+
// ✅ Pure unit tests: JUnit 4/5 + Mockito
|
|
526
|
+
@RunWith(MockitoJUnitRunner.class) // JUnit 4
|
|
527
|
+
// or @ExtendWith(MockitoExtension.class) // JUnit 5
|
|
528
|
+
public class UserServiceTest {
|
|
529
|
+
@Mock private UserRepository repo;
|
|
530
|
+
@InjectMocks private UserService service;
|
|
531
|
+
|
|
532
|
+
@Test
|
|
533
|
+
public void shouldCreateUser() { ... }
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Inject `Clock` into time-sensitive logic so tests do not depend on `Instant.now()`.
|
|
538
|
+
|
|
539
|
+
Legacy stacks often use JUnit 4; if JUnit 5 is mixed in, keep one runner style per module.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## Review Checklist
|
|
544
|
+
|
|
545
|
+
### Version & scope
|
|
546
|
+
- [ ] Confirmed Java 8 / Boot 2 / `javax.*` — do not demand Java 17+ APIs or `jakarta.*`
|
|
547
|
+
- [ ] Do not treat "use records / virtual threads / text blocks" as blocking feedback
|
|
548
|
+
|
|
549
|
+
### Language features
|
|
550
|
+
- [ ] Lambdas stay short; prefer method references and standard functional interfaces
|
|
551
|
+
- [ ] Streams used for transform / filter / reduce, not simple side-effect loops
|
|
552
|
+
- [ ] Collection uses `Collectors.*` (no `Stream.toList()`)
|
|
553
|
+
- [ ] `toMap` handles null values and duplicate keys
|
|
554
|
+
- [ ] `parallelStream` is justified and has no shared mutable state
|
|
555
|
+
- [ ] Optional is return-type only; no `isPresent`+`get` abuse; expensive defaults use `orElseGet`
|
|
556
|
+
- [ ] Dates use `java.time`; no shared `SimpleDateFormat`; correct type (`Instant` vs `LocalDateTime`)
|
|
557
|
+
- [ ] Patterns use `yyyy`, not week-based `YYYY` (unless week-year is intentional)
|
|
558
|
+
|
|
559
|
+
### Concurrency
|
|
560
|
+
- [ ] Thread pools are bounded and shut down; I/O does not use `ForkJoinPool.commonPool()`
|
|
561
|
+
- [ ] CompletableFuture uses an explicit Executor; `thenCompose` for nested async; timeouts and error handling present
|
|
562
|
+
- [ ] Shared state uses concurrent collections; no static mutable `DateFormat`
|
|
563
|
+
|
|
564
|
+
### Spring Boot 2 / JPA
|
|
565
|
+
- [ ] Constructor injection; config via `@ConfigurationProperties`
|
|
566
|
+
- [ ] `RestTemplate` (and RequestFactory) has connect/read timeouts
|
|
567
|
+
- [ ] `@Transactional` on public entry points — no self-invocation / private-method failures
|
|
568
|
+
- [ ] No N+1; entities avoid `@Data`; temporal strategy is explicit (UTC)
|
|
569
|
+
- [ ] Packages stay consistently `javax.*` — no javax/jakarta mix
|
|
570
|
+
|
|
571
|
+
### Quality
|
|
572
|
+
- [ ] Exceptions are not swallowed; error responses are centralized
|
|
573
|
+
- [ ] try-with-resources for I/O and DB resources
|
|
574
|
+
- [ ] Core logic has unit tests; time is injectable via `Clock`
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## References
|
|
579
|
+
|
|
580
|
+
- [What to Look for in Java 8 Code (JetBrains)](https://blog.jetbrains.com/upsource/2016/08/03/what-to-look-for-in-java-8-code/)
|
|
581
|
+
- [JDK-8148463: Collectors.toMap fails on null values](https://bugs.openjdk.org/browse/JDK-8148463)
|
|
582
|
+
- [Oracle Tutorial: Parallelism](https://docs.oracle.com/javase/tutorial/collections/streams/parallelism.html)
|
|
583
|
+
- [Baeldung: Migrating to Java 8 Date/Time API](https://www.baeldung.com/migrating-to-java-8-date-time-api)
|
|
584
|
+
- [Baeldung: CompletableFuture and ThreadPool](https://www.baeldung.com/java-completablefuture-threadpool)
|
|
585
|
+
- [Spring Boot RestTemplate customization](https://docs.spring.io/spring-boot/docs/2.7.x/reference/html/io.html#io.rest-client.resttemplate)
|
|
586
|
+
- [Thorben Janssen: Hibernate/JPA Date and Time](https://thorben-janssen.com/hibernate-jpa-date-and-time/)
|