Java Development Standards
References: Google Java Style Guide, Alibaba Java Development Manual
Toolchain
# Maven
mvn clean compile # 编译
mvn test # 运行测试
mvn verify # 运行所有检查
# Gradle
./gradlew build # 构建
./gradlew test # 运行测试
Naming Conventions
| Type |
Rule |
Example |
| Package name |
All lowercase, reverse domain name |
com.example.project |
| Class name |
UpperCamelCase, noun/noun phrase |
UserService, HttpClient |
| Method name |
lowerCamelCase, starting with a verb |
findById, isValid |
| Constants |
All uppercase with underscore separators |
MAX_RETRY_COUNT |
| Boolean return values |
is/has/can prefix |
isActive(), hasPermission() |
Class Member Order
public class Example {
// 1. 静态常量
public static final String CONSTANT = "value";
// 2. 静态变量
private static Logger logger = LoggerFactory.getLogger(Example.class);
// 3. 实例变量
private Long id;
// 4. 构造函数
public Example() { }
// 5. 静态方法
public static Example create() { return new Example(); }
// 6. 实例方法(公共 → 私有)
public void doSomething() { }
private void helperMethod() { }
// 7. getter/setter(或使用 Lombok)
}
DTO/VO Class Standards
| Rule |
Description |
| ❌ Handwritten getter/setter prohibited |
DTO, VO, Request, and Response classes must all use Lombok |
✅ Use @Data |
Regular DTO |
✅ Use @Value |
Immutable DTO |
✅ Use @Builder |
Use together when there are many fields |
⚠️ Use with caution on Entity classes @Data |
JPA Entity equals/hashCode can affect Hibernate proxies |
// ❌ 手写 getter/setter
public class UserDTO {
private Long id;
private String name;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
// ... 大量样板代码
}
// ✅ 使用 Lombok
@Data
public class UserDTO {
private Long id;
private String name;
}
Batch Query Standards
| Rule |
Description |
| ❌ IN clauses with more than 500 parameters prohibited |
High SQL parsing overhead, unstable execution plan |
| ✅ Query in batches when exceeded |
500 per batch, merge results |
| ✅ Encapsulate a common utility method |
Avoid handwriting batching logic everywhere |
// ❌ 1700 个 ID 一次查询
List<User> users = userRepository.findByIdIn(allIds); // IN 子句过长
// ✅ 分批查询工具方法
public static <T, R> List<R> batchQuery(List<T> params, int batchSize,
Function<List<T>, List<R>> queryFn) {
List<R> result = new ArrayList<>();
for (int i = 0; i < params.size(); i += batchSize) {
List<T> batch = params.subList(i, Math.min(i + batchSize, params.size()));
result.addAll(queryFn.apply(batch));
}
return result;
}
// 使用
List<User> users = batchQuery(allIds, 500, ids -> userRepository.findByIdIn(ids));
N+1 Query Prevention
| Rule |
Description |
| ❌ Calling Repository/Mapper inside a loop prohibited |
Each iteration in stream/forEach/for triggers a query |
| ✅ Query in batches outside the loop, convert results to a Map |
Query count drops from N to 1 (or the number of distinct values) |
// ❌ N+1:循环内逐行查询 count
records.forEach(record -> {
long count = deviceRepo.countByDeviceId(record.getDeviceId()); // 每条触发一次查询
record.setDeviceCount(count);
});
// ✅ 循环外批量查询 + Map 查找
List<String> deviceIds = records.stream()
.map(Record::getDeviceId).distinct().collect(Collectors.toList());
Map<String, Long> countMap = deviceRepo.countByDeviceIdIn(deviceIds).stream()
.collect(Collectors.toMap(CountDTO::getDeviceId, CountDTO::getCount));
records.forEach(r -> r.setDeviceCount(countMap.getOrDefault(r.getDeviceId(), 0L)));
Common N+1 scenarios and fix patterns:
| Scenario |
Inside the loop (❌) |
Outside the loop (✅) |
| count |
repo.countByXxx(id) |
repo.countByXxxIn(ids) → Map<id, count> |
| findById |
repo.findById(id) |
repo.findByIdIn(ids) → Map<id, entity> |
| exists |
repo.existsByXxx(id) |
repo.findXxxIn(ids) → Set<id> + set.contains() |
Concurrency Safety Standards
| Rule |
Description |
| ❌ read-modify-write prohibited |
Read the balance first and write it back; lost updates under concurrency |
| ❌ check-then-act without a fallback prohibited |
Check first and then act; conditions become invalid under concurrency |
| ✅ Use atomic update SQL |
UPDATE SET balance = balance + :delta WHERE id = :id |
| ✅ Or use optimistic locking |
@Version field + retry mechanism |
| ✅ Unique index as a fallback |
The last line of defense against duplicate inserts |
// ❌ read-modify-write 竞态条件
PointsAccount account = accountRepo.findById(id);
account.setBalance(account.getBalance() + points); // 并发时丢失更新
accountRepo.save(account);
// ✅ 方案一:原子更新 SQL
@Modifying
@Query("UPDATE PointsAccount SET balance = balance + :points WHERE id = :id")
int addBalance(@Param("id") Long id, @Param("points") int points);
// ✅ 方案二:乐观锁
@Version
private Long version; // Entity 中添加版本字段
// ❌ check-then-act 无兜底(并发下可能重复结算)
if (!rewardRepo.existsByTenantIdAndPeriod(tenantId, period)) {
rewardRepo.save(new RankingReward(...));
}
// ✅ 唯一索引兜底 + 异常捕获
// DDL: UNIQUE INDEX uk_tenant_period (tenant_id, ranking_type, period, rank_position)
try {
rewardRepo.save(new RankingReward(...));
} catch (DataIntegrityViolationException e) {
log.warn("重复结算已被唯一索引拦截: tenantId={}, period={}", tenantId, period);
}
Exception Handling
// ✅ 好:捕获具体异常,添加上下文
try {
user = userRepository.findById(id);
} catch (DataAccessException e) {
throw new ServiceException("Failed to find user: " + id, e);
}
// ✅ 好:资源自动关闭
try (InputStream is = new FileInputStream(file)) {
// 使用资源
}
// ❌ 差:捕获过宽
catch (Exception e) { e.printStackTrace(); }
Null Handling
// ✅ 使用 Optional
public Optional<User> findById(Long id) {
return userRepository.findById(id);
}
// ✅ 参数校验
public void updateUser(User user) {
Objects.requireNonNull(user, "user must not be null");
}
// ✅ 安全的空值处理
String name = Optional.ofNullable(user)
.map(User::getName)
.orElse("Unknown");
Concurrent Programming
// ✅ 使用 ExecutorService
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<Result> future = executor.submit(() -> doWork());
// ✅ 使用 CompletableFuture
CompletableFuture<User> future = CompletableFuture
.supplyAsync(() -> findUser(id))
.thenApply(user -> enrichUser(user));
// ❌ 差:直接创建线程
new Thread(() -> doWork()).start();
Testing Standards (JUnit 5)
class UserServiceTest {
@Test
@DisplayName("根据 ID 查找用户 - 用户存在时返回用户")
void findById_whenUserExists_returnsUser() {
// given
when(userRepository.findById(1L)).thenReturn(Optional.of(expected));
// when
Optional<User> result = userService.findById(1L);
// then
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("test");
}
}
Spring Boot Standards
// ✅ 构造函数注入
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
}
// ✅ REST Controller
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public ResponseEntity<UserDto> findById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
Auth Filter Degradation Principles
| Rule |
Description |
| ✅ optional-auth paths degrade to anonymous access when encountering invalid/expired/incomplete tokens |
Should not return 401/403 |
| ❌ Users with partial credentials must not have a worse experience than anonymous users |
E.g., a temporary token returns 403 on a public API |
Circular Dependency Prevention (Spring Boot 3.x)
Spring Boot 3.x prohibits constructor circular dependencies by default. When splitting a large Service into sub-Services, the dependency direction must be checked.
| Approach |
Priority |
Applicable Scenario |
| Extract common methods into a standalone utility class |
✅ Preferred |
Pure utility methods (e.g., resolveTenantIds) |
@Lazy field injection |
⚠️ Emergency |
When bidirectional calls are truly needed |
Function<> Callback |
⚠️ Alternative |
Flexible but adds complexity |
// ❌ 拆分后子服务回调父服务 → 循环依赖
@RequiredArgsConstructor
public class ReportInvoiceService {
private final ReportService reportService; // 启动失败!
}
// ✅ 提取公共方法到独立类
@Component
public class TenantHelper {
public List<Long> resolveTenantIds(Long tenantId) { ... }
}
See refactor-safety skill - Pitfall #5
Pagination Parameter Convention (Spring Data JPA)
Spring Data JPA pagination index starts at 0. When refactoring pagination parameters, the index base must be kept consistent between frontend and backend.
| Rule |
Description |
| Full-stack unified 0-based |
Frontend, Controller, Service, and JPA all use 0-based indexes |
| Controller default value must be 0 |
@RequestParam(defaultValue = "0") int page |
| Service directly uses page |
PageRequest.of(page, size), do not page - 1 |
// ❌ 错误:前端传 page=0,Controller 默认值 1,Service 内部 -1
@GetMapping("/list")
public Page<Product> list(@RequestParam(defaultValue = "1") int page) {
return service.list(page); // Service 内部 PageRequest.of(page - 1, size)
}
// 问题:前端传 page=0 → Service 计算 page - 1 = -1 → 异常
// ✅ 正确:全栈统一 0-based
@GetMapping("/list")
public Page<Product> list(@RequestParam(defaultValue = "0") int page) {
return service.list(page); // Service 内部 PageRequest.of(page, size)
}
Refactoring Checklist:
- Frontend calls pass
page: 0(page 1)
- Controller default value is
0
- Service uses
PageRequest.of(page, size)(do not subtract 1)
- Test
page=0 and page=1 both return data normally
Input Validation Convention
| Rule |
Description |
❌ Forbidden @RequestBody without @Valid |
All request bodies must be validated |
| ✅ Add constraint annotations to DTO fields |
@NotBlank、@Size、@Pattern etc. |
| ✅ Add range constraints to numeric fields |
@Min、@Max、@Positive etc. |
| ✅ Add an upper limit to pagination parameters |
size must @Max(100) to prevent large queries |
| ✅ Whitelist validation for enum/status fields |
Custom validator or @Pattern |
Common DTO Field Validation Quick Reference:
| Field Type |
Required Annotation |
Description |
| Quantity quantity |
@NotNull @Min(1) |
Prevent 0 or negative numbers (negatives can cause reverse operations) |
| Amount amount/price |
@NotNull @Positive |
or @DecimalMin("0.01") |
| Pagination size |
@Min(1) @Max(100) |
Prevent size=999999 from overwhelming the database |
| Pagination page |
@Min(1) |
Page numbers start at 1 |
| Percentage rate |
@Min(0) @Max(100) |
Define range based on business needs |
// ❌ 无校验,任意输入直接进入业务逻辑
@PostMapping("/ship")
public Result ship(@RequestBody ShippingRequest request) { ... }
// ✅ 完整校验
@PostMapping("/ship")
public Result ship(@RequestBody @Valid ShippingRequest request) { ... }
public record ShippingRequest(
@NotNull Long orderId,
@NotBlank @Size(max = 500) String shippingInfo,
@Pattern(regexp = "pending|shipped|delivered") String giftStatus
) {}
// ❌ quantity 只有 @NotNull,负数会导致 Redis DECRBY 反向加库存
public record CreateOrderRequest(
@NotNull Integer quantity // 可提交 0 或负数
) {}
// ✅ 数量必须 >= 1
public record CreateOrderRequest(
@NotNull @Min(1) Integer quantity
) {}
// ❌ 分页无上限,用户可传 size=999999
@GetMapping("/orders")
public Result list(@RequestParam int page, @RequestParam int size) { ... }
// ✅ 分页参数加约束
@GetMapping("/orders")
public Result list(@RequestParam @Min(1) int page,
@RequestParam @Min(1) @Max(100) int size) { ... }
Performance Optimization
| Pitfall |
Solution |
| N+1 query |
See the "N+1 Query Prevention" section |
| String concatenation in a loop |
Use StringBuilder |
| Frequent boxing/unboxing |
Use primitive type streams |
| Collection initial capacity not specified |
new ArrayList<>(size) |
Third-Party API HTTP Client Selection
| Rule |
Description |
❌ Avoid RestTemplate default client for calling domestic platform APIs |
Default HttpURLConnection POST requests have compatibility issues with CDNs such as WeChat/Alipay (known to trigger 412/403) |
✅ Prefer java.net.http.HttpClient(JDK 11+) |
modern HTTP clients, no CDN compatibility issues |
✅ Or configure HttpComponentsClientHttpRequestFactory |
to make RestTemplate use Apache HttpClient under the hood |
Diagnostic characteristics: HTTP error + empty body + extremely minimal response headers (only Connection/Content-Length) = interception at the CDN layer, not a response from the API itself. When the same API's GET works normally but POST is abnormal, first suspect HTTP client compatibility.
Native SQL conventions
Aliases avoid MySQL reserved words
@Query(nativeQuery = true) If a column alias in is a MySQL reserved word, it will cause a syntax error.
Frequently encountered reserved words:year_month, order, status, key, value, name, type, date, time, rank, range, rows, column, user, role, group
| Rule |
Description |
| ✅ Use short aliases or abbreviations |
ym, ord_status, cnt |
| ✅ Or escape with backticks |
`year_month` |
| ❌ Do not use reserved words directly as aliases |
as year_month、as order、as rank |
// ❌ year_month 是 MySQL 保留字(INTERVAL YEAR_MONTH)
@Query(value = """
SELECT DATE_FORMAT(o.order_date, '%Y-%m') as year_month,
COUNT(*) as cnt
FROM orders o
GROUP BY year_month
""", nativeQuery = true)
// ✅ 改用非保留字别名
@Query(value = """
SELECT DATE_FORMAT(o.order_date, '%Y-%m') as ym,
COUNT(*) as cnt
FROM orders o
GROUP BY ym
""", nativeQuery = true)
Logging conventions
// ✅ 参数化日志
log.debug("Finding user by id: {}", userId);
log.info("User {} logged in successfully", username);
log.error("Failed to process order {}", orderId, exception);
// ❌ 差:字符串拼接
log.debug("Finding user by id: " + userId);
Detailed reference
| File |
Content |
references/java-style.md |
Naming conventions, exception handling, Spring Boot, testing conventions |
references/collections.md |
Immutable collections (Guava), string splitting |
references/concurrency.md |
Thread pool configuration, CompletableFuture timeout |
references/concurrency-db-patterns.md |
Get-Or-Create concurrency, N+1 prevention, atomic updates, Redis+DB consistency |
references/code-patterns.md |
Guard clauses, enum optimization, strategy factory pattern |
references/date-time.md |
Date addition and subtraction, accounting period calculation, prohibit month-end alignment |
references/http-client.md |
Third-party API HTTP client selection, CDN compatibility issues |
📋 This reply follows:java-dev - [specific section]