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, reversed domain | com.example.project |
| Class name | UpperCamelCase, noun/noun phrase | UserService, HttpClient |
| Method name | lowerCamelCase, starts with a verb | findById, isValid |
| Constant | All uppercase with underscore separators | MAX_RETRY_COUNT |
| Boolean return value | 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 |
|---|---|
| ❌ Do not hand-write getters/setters | DTO, VO, Request, and Response classes must all use Lombok |
✅ Use @Data |
ordinary DTO |
✅ Use @Value |
immutable DTO |
✅ Use @Builder |
used together when there are many fields |
⚠️ Use with caution on Entity classes @Data |
JPA Entity's 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 |
|---|---|
| ❌ Do not use IN clauses with more than 500 parameters | SQL parsing overhead is high, and the execution plan is unstable |
| ✅ Query in batches when exceeded | 500 per batch, merge the results |
| ✅ Encapsulate common utility methods | Avoid hand-writing 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 |
|---|---|
| ❌ Do not call Repository/Mapper inside a loop | Each iteration inside stream/forEach/for triggers one query |
| ✅ Batch query outside the loop, convert results to a Map | Number of queries drops from N to 1 (or the distinct count) |
// ❌ 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 |
|---|---|
| ❌ Do not read-modify-write | Read the balance then write it back; concurrent access causes lost updates |
| ❌ Do not check-then-act without fallback | Check first then act; conditions fail 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 Value 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;
}
| Topic | One-sentence rule |
|---|---|
| Injection method | Use @RequiredArgsConstructor constructor injection, do not use @Autowired field injection |
| Auth downgrade | When an optional-auth path encounters an invalid/expired token, downgrade to anonymous instead of returning 401/403 |
| Circular dependency | Spring Boot 3.x prohibits constructor circular dependencies by default. When splitting a large Service into sub-Services, first use a utility class to share methods, then consider @Lazy |
| self-invocation | this.method() does not go through the proxy,@Transactional/@Async/@Cacheable all become ineffective; use self-injection or split the Bean |
@Modifying JPA |
must be called within @Transactional , otherwise it throws Executing an update/delete query |
| Pagination parameters | Full stack uniformly 0-based, Controller defaultValue = "0", Service directly PageRequest.of(page, size) |
For detailed rules, fix templates, and checklists, see
references/spring.md
Input Validation Specification
| Rule | Description |
|---|---|
❌ Prohibited @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 massive 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 (negative numbers 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 from 1 |
| Percentage rate | @Min(0) @Max(100) |
Define the range according to 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" chapter |
| Concatenating strings in a loop | Use StringBuilder |
| Frequent boxing and 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 when calling domestic platform APIs |
By 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 client, no CDN compatibility issues |
✅ Or configure HttpComponentsClientHttpRequestFactory |
to make RestTemplate use Apache HttpClient at the underlying level |
Diagnostic characteristics: HTTP error + empty body + extremely minimal response headers (only Connection/Content-Length) = CDN-layer interception, not the API's own response. When the same API's GET works normally but POST fails, first suspect HTTP client compatibility.
Native SQL Specification
Avoid MySQL reserved words in aliases
@Query(nativeQuery = true) If a column alias in is a MySQL reserved word, it will cause a syntax error.
High-Frequency Reserved Word Pitfalls: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` |
| ❌ Prohibited to 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 Specification
// ✅ 参数化日志
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);
Spring Bean Naming Conflicts
Scenario: At Spring startup BeanDefinitionStoreException, or "no qualifying bean
of type X found" where it seems the Bean cannot be found.
Root Cause
Spring generates the bean name by default using the class name with the first letter lowercased. There are two types of conflicts:
- Sub-scenario A: Same name, same type — two
@Service/@Controllerclasses with the same name (different packages) - Sub-scenario B: Same name, different type ——
@Bean(name=X)The registered object collides with the default bean name of the@Component/@Serviceclass ⚠️ More insidious
Sub-scenario A: Two Controllers / Services with the same name
// ❌ 两个包下同名类,bean name 都是 miniAppConfigController
// com.trade.controller.MiniAppConfigController
@RestController
public class MiniAppConfigController { }
// com.trade.controller.mini.MiniAppConfigController
@RestController
public class MiniAppConfigController { }
// → 启动报 BeanDefinitionStoreException
Fix:
// ✅ 方案1: 改类名(推荐)
@RestController
public class MiniPublicConfigController { }
// ✅ 方案2: 显式指定 bean name
@RestController("miniPublicConfigController")
public class MiniAppConfigController { }
Sub-scenario B:@Bean and @Service different types with the same name ⚠️
The most treacherous kind — the error message may appear as "Bean not found", but the root cause is naming override.
// ❌ AsyncConfig 注册线程池
@Configuration
public class AsyncConfig {
@Bean(name = "priceBatchUpdateExecutor") // 名字和下面那个 Service 冲突
public Executor priceBatchUpdateExecutor() { ... }
}
// ❌ 同时 @Service 类的默认 bean name 就是 priceBatchUpdateExecutor
@Service
public class PriceBatchUpdateExecutor { ... }
// ❌ 业务代码注入
@RequiredArgsConstructor
public class PriceBatchUpdateService {
private final PriceBatchUpdateExecutor priceBatchUpdateExecutor;
// 启动时报: no qualifying bean of type PriceBatchUpdateExecutor
}
Two consequences, both bad:
| State | Behavior | Probability of misdiagnosis |
|---|---|---|
| overriding not enabled | BeanDefinitionStoreException, clear localization |
Low |
| overriding enabled | Thread pool Bean overrides Service Bean → when injecting PriceBatchUpdateExecutor what is found is of type Executor → reports no qualifying bean of type, the root cause is masked |
High, often takes hours to troubleshoot |
Never fix it this way:
# ❌ AI 反射式开全局开关,掩盖问题
spring.main.allow-bean-definition-overriding=true
See
.codex/global/AGENTS.mdquality requirements: do not mask errors by enabling a global permissive switch.
Correct Fix:
// ✅ 区分语义:线程池叫 ThreadPool / TaskExecutor,业务类保持类名
@Bean(name = "priceBatchUpdateThreadPool")
public Executor priceBatchUpdateThreadPool() { ... }
@Service
public class PriceBatchUpdateExecutor { ... } // 业务类不动
// 调用方同步改 @Qualifier
@RequiredArgsConstructor
public class PriceBatchUpdateTaskScheduler {
@Qualifier("priceBatchUpdateThreadPool")
private final Executor threadPool;
private final PriceBatchUpdateExecutor executor; // 注入业务 Bean
}
Bean naming conventions (avoid conflicts)
| Type | Suffix / naming convention |
|---|---|
| Business service | XxxService / XxxManager / XxxExecutor(if "executor" is a business concept) |
| Thread pool Bean | XxxThreadPool / XxxTaskExecutor |
| Configuration Bean | XxxConfig / XxxProperties |
| Data source Bean | XxxDataSource / XxxJdbcTemplate |
| HTTP client | XxxRestTemplate / XxxHttpClient |
| MQ Bean | XxxListenerContainer / XxxTemplate |
Iron rule:@Bean(name=) naming must not collide with the default Bean name of any @Component/@Service/@Controller/@Repository
class.
Troubleshooting commands
# 在项目里搜索是否有重名
rg '@Bean\(name = "priceBatchUpdateExecutor"' src/
rg 'class PriceBatchUpdateExecutor' src/
# 启动时打开 DEBUG 看 Bean 注册
# logging.level.org.springframework.beans.factory=DEBUG
Checklist
- When adding a new Controller/Service, do you check whether a class with the same name already exists in the project
- When adding a new
@Bean(name=X), do you check whetherclass Xexists - Do classes with the same name under different packages use different class names or explicit bean names
- When startup reports
BeanDefinitionStoreExceptionorno qualifying bean of type,prioritize checking for same-named Beans - After temporarily enabling
allow-bean-definition-overriding=true, do youremove it in timeafter fixing the root cause (in the vast majority of cases it should not be enabled)
Detailed reference
| File | Content |
|---|---|
references/java-style.md |
Naming conventions, exception handling, Spring Boot, testing standards |
references/spring.md |
Complete Spring Boot standards: injection, Auth degradation, circular dependencies, self-invocation pitfalls, pagination parameters |
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 arithmetic, accounting period calculation, no month-end alignment |
references/http-client.md |
Third-party API HTTP client selection, CDN compatibility issues |
📋 This reply follows:
cc-java-dev- [specific section]