如何优化PersistenceManager类中CrudRepository的泛型参数设计?
Great question—raw types like CrudRepository are best avoided because they disable Java's compile-time type safety, leaving you open to unexpected runtime bugs. Let's walk through how to refine your code to use generics properly, while keeping all your existing functionality intact.
The Core Problem with Raw Types
Your current Map<Class<?>, CrudRepository> uses a raw CrudRepository type, which means the compiler can't verify that you're calling save() or findById() with the correct entity/ID types. This could lead to accidental mismatches (e.g., passing a User entity to a ProductRepository) that won't be caught until runtime.
Step-by-Step Generic Fix
We can refactor the PersistenceManager to use bounded wildcards and generic captures to enforce type safety without breaking your existing methods.
1. Fix the Repository Map Type
First, update the repositories map to use a bounded wildcard that ensures all stored repositories are for BaseEntity subclasses:
private final Map<Class<? extends BaseEntity>, CrudRepository<? extends BaseEntity, ?>> repositories;
This tells the compiler: "Each key is a class of a BaseEntity subclass, and the corresponding value is a repository for that (or a superclass) entity type, with some unknown ID type."
2. Refine the Constructor
The constructor can stay mostly the same, but we'll add a bounded wildcard to the input list to ensure only valid repositories are accepted:
public PersistenceManager(List<CrudRepository<? extends BaseEntity, ?>> repositories) { this.repositories = repositories.stream() .collect(Collectors.toMap( CrudRepository::getEntityClass, rep -> rep )); }
This prevents anyone from accidentally passing a repository for a non-BaseEntity type into the PersistenceManager.
3. Make the store() Method Type-Safe
We can use a generic capture helper method to avoid unchecked casts and ensure the correct repository is used for the entity:
public void store(EntityWrapper<? extends BaseEntity> entity) { Class<? extends BaseEntity> entityType = entity.getType(); CrudRepository<? extends BaseEntity, ?> repository = repositories.get(entityType); if (repository == null) { throw new IllegalStateException("No repository class for entity " + entityType); } // Use a helper method to capture the generic type and call save safely saveEntity(repository, entity.getObject()); } // Generic helper to capture the entity type private <T extends BaseEntity> void saveEntity(CrudRepository<T, ?> repository, T entity) { repository.save(entity); }
The helper method saveEntity captures the specific entity type T, so the compiler can verify that we're passing the correct entity type to the repository's save() method.
4. Fix the findById() Method
Similarly, we can use a generic capture to ensure the repository returns the correct entity type:
public <T extends BaseEntity, ID> Optional<T> findById(Class<T> clazz, ID id) { CrudRepository<? extends BaseEntity, ?> repository = repositories.get(clazz); if (repository == null) { throw new IllegalStateException("No repository class for entity " + clazz.getSimpleName()); } // Cast is safe because our map guarantees the repository matches the entity class @SuppressWarnings("unchecked") CrudRepository<T, ID> typedRepository = (CrudRepository<T, ID>) repository; return typedRepository.findById(id); }
The unchecked cast here is safe because our map guarantees that the repository for Class<T> is a CrudRepository<T, ?>, and we're specifying the ID type as ID (any mismatch here would be a configuration error, not a type safety flaw).
Full Optimized Code
Here's the complete refactored PersistenceManager:
import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class PersistenceManager { private final Map<Class<? extends BaseEntity>, CrudRepository<? extends BaseEntity, ?>> repositories; public PersistenceManager(List<CrudRepository<? extends BaseEntity, ?>> repositories) { this.repositories = repositories.stream() .collect(Collectors.toMap( CrudRepository::getEntityClass, rep -> rep )); } public void store(EntityWrapper<? extends BaseEntity> entity) { Class<? extends BaseEntity> entityType = entity.getType(); CrudRepository<? extends BaseEntity, ?> repository = repositories.get(entityType); if (repository == null) { throw new IllegalStateException("No repository class for entity " + entityType); } saveEntity(repository, entity.getObject()); } public <T extends BaseEntity, ID> Optional<T> findById(Class<T> clazz, ID id) { CrudRepository<? extends BaseEntity, ?> repository = repositories.get(clazz); if (repository == null) { throw new IllegalStateException("No repository class for entity " + clazz.getSimpleName()); } @SuppressWarnings("unchecked") CrudRepository<T, ID> typedRepository = (CrudRepository<T, ID>) repository; return typedRepository.findById(id); } private <T extends BaseEntity> void saveEntity(CrudRepository<T, ?> repository, T entity) { repository.save(entity); } }
Key Improvements
- No more raw types: All repository uses are checked at compile time.
- Type safety: You can't accidentally pass the wrong entity type to a repository, or get back an incorrect entity type from
findById(). - Backward compatible: Your existing
store()andfindById()method signatures remain the same, so no changes are needed in code that uses this class.
Is the Original Code "Good Enough"?
If your code is working and you don't anticipate adding new entity types or repositories, the original code might function. However, using raw types is a code smell—they bypass Java's generic type system, which is designed to catch bugs early. The refactored version is more maintainable and less error-prone, especially as your codebase grows.
内容的提问来源于stack exchange,提问作者Alexey Usharovski

