四、自动配置原理
4.1 自动配置机制
// @EnableAutoConfiguration 核心
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
// AutoConfigurationImportSelector 加载 META-INF/spring.factories 中的配置
public class AutoConfigurationImportSelector {
protected List<String> getCandidateConfigurations() {
// 从 META-INF/spring.factories 读取
return SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
}
}
// META-INF/spring.factories 示例
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
4.2 条件注解
// @Conditional - 条件注解
@Configuration
public class ConditionalConfig {
// 类路径存在指定类
@ConditionalOnClass(name = "redis.clients.jedis.Jedis")
@Bean
public RedisTemplate redisTemplate() {
return new RedisTemplate();
}
// 类路径不存在指定类
@ConditionalOnMissingClass("com.mysql.cj.jdbc.Driver")
@Bean
public DataSource h2DataSource() {
return new H2DataSource();
}
// Bean 存在
@ConditionalOnBean(DataSource.class)
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
// Bean 不存在
@ConditionalOnMissingBean(name = "cacheManager")
@Bean
public CacheManager defaultCacheManager() {
return new ConcurrentMapCacheManager();
}
// 配置属性
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
@Bean
public CacheManager redisCacheManager() {
return new RedisCacheManager();
}
// Web 应用
@ConditionalOnWebApplication
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new CustomWebMvcConfigurer();
}
// 非 Web 应用
@ConditionalOnNotWebApplication
@Bean
public CommandLineRunner commandLineRunner() {
return args -> System.out.println("非 Web 应用启动");
}
// Java 版本
@ConditionalOnJava(JavaVersion.EIGHT)
@Bean
public Java8Service java8Service() {
return new Java8Service();
}
// 表达式
@ConditionalOnExpression("${feature.enabled:true} && ${another.enabled:false}")
@Bean
public FeatureService featureService() {
return new FeatureService();
}
// 资源存在
@ConditionalOnResource(resources = "classpath:config.xml")
@Bean
public XmlParser xmlParser() {
return new XmlParser();
}
}
4.3 自定义自动配置
// 自定义自动配置类
@Configuration
@ConditionalOnClass(RedisTemplate.class)
@ConditionalOnProperty(name = "custom.cache.enabled", havingValue = "true")
@EnableConfigurationProperties(CustomCacheProperties.class)
public class CustomCacheAutoConfiguration {
@Autowired
private CustomCacheProperties properties;
@Bean
@ConditionalOnMissingBean
public CacheManager customCacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(properties.getTtl()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
@Bean
@ConditionalOnBean(CacheManager.class)
public CacheService cacheService(CacheManager cacheManager) {
return new CacheService(cacheManager);
}
}
// 配置属性
@ConfigurationProperties(prefix = "custom.cache")
public class CustomCacheProperties {
private boolean enabled = true;
private long ttl = 60;
private int maxSize = 1000;
private String keyPrefix = "custom:";
// getter/setter
}
// META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.autoconfigure.CustomCacheAutoConfiguration
五、Web 开发
5.1 RESTful API 开发
@RestController
@RequestMapping("/api/users")
@Validated
@Slf4j
public class UserController {
@Autowired
private UserService userService;
// GET /api/users
@GetMapping
public PageResult<User> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {
return userService.findByPage(page, size, keyword);
}
// GET /api/users/1
@GetMapping("/{id}")
public User getUser(@PathVariable @Min(1) Long id) {
return userService.findById(id);
}
// POST /api/users
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User createUser(@Valid @RequestBody User user) {
return userService.create(user);
}
// PUT /api/users/1
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @Valid @RequestBody User user) {
user.setId(id);
return userService.update(user);
}
// PATCH /api/users/1
@PatchMapping("/{id}")
public User patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
return userService.patch(id, updates);
}
// DELETE /api/users/1
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
// 批量操作
@PostMapping("/batch")
public List<User> batchCreate(@RequestBody List<User> users) {
return userService.batchCreate(users);
}
// 导出
@GetMapping("/export")
public void exportUsers(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=users.xlsx");
userService.export(response.getOutputStream());
}
}
5.2 统一响应格式
// 统一响应结果
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
private long timestamp = System.currentTimeMillis();
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> error(String message) {
return error(500, message);
}
public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null);
}
public static <T> Result<T> error(BusinessException e) {
return new Result<>(e.getCode(), e.getMessage(), null);
}
}
// 分页结果
@Data
public class PageResult<T> {
private List<T> records;
private long total;
private int page;
private int size;
private long pages;
public PageResult(List<T> records, long total, int page, int size) {
this.records = records;
this.total = total;
this.page = page;
this.size = size;
this.pages = (total + size - 1) / size;
}
}
5.3 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return Result.error(400, "参数校验失败", errors);
}
// 绑定异常
@ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<String> handleBindException(BindException ex) {
String message = ex.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 约束违反异常
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<String> handleConstraintViolation(ConstraintViolationException ex) {
String message = ex.getConstraintViolations().stream()
.map(violation -> violation.getPropertyPath() + ": " + violation.getMessage())
.collect(Collectors.joining(", "));
return Result.error(400, message);
}
// 业务异常
@ExceptionHandler(BusinessException.class)
public Result<String> handleBusinessException(BusinessException ex) {
log.warn("业务异常: {}", ex.getMessage());
return Result.error(ex.getCode(), ex.getMessage());
}
// 数据访问异常
@ExceptionHandler(DataAccessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<String> handleDataAccessException(DataAccessException ex) {
log.error("数据库异常", ex);
return Result.error(500, "数据库操作失败");
}
// 参数类型转换异常
@ExceptionHandler(HttpMessageConversionException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<String> handleConversionException(HttpMessageConversionException ex) {
return Result.error(400, "请求参数格式错误");
}
// 资源不存在
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Result<String> handleNotFoundException(NoHandlerFoundException ex) {
return Result.error(404, "请求的资源不存在");
}
// 全局异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<String> handleException(Exception ex) {
log.error("系统异常", ex);
return Result.error(500, "系统内部错误");
}
}
5.4 文件上传与下载
@RestController
@RequestMapping("/api/files")
@Slf4j
public class FileController {
@Value("${file.upload-dir}")
private String uploadDir;
// 单文件上传
@PostMapping("/upload")
public Result<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return Result.error("文件不能为空");
}
// 生成文件名
String originalName = file.getOriginalFilename();
String extension = originalName.substring(originalName.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + extension;
// 保存文件
Path path = Paths.get(uploadDir, fileName);
Files.createDirectories(path.getParent());
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return Result.success(fileName);
}
// 多文件上传
@PostMapping("/upload/batch")
public Result<List<String>> uploadFiles(@RequestParam("files") List<MultipartFile> files)
throws IOException {
List<String> fileNames = new ArrayList<>();
for (MultipartFile file : files) {
if (!file.isEmpty()) {
String fileName = saveFile(file);
fileNames.add(fileName);
}
}
return Result.success(fileNames);
}
// 带进度的文件上传
@PostMapping("/upload/progress")
public Result<String> uploadWithProgress(
@RequestParam("file") MultipartFile file,
HttpSession session) throws IOException {
ProgressListener listener = new ProgressListener(session);
MultipartFile progressFile = new ProgressMultipartFile(file, listener);
String fileName = saveFile(progressFile);
return Result.success(fileName);
}
// 文件下载
@GetMapping("/download/{fileName}")
public void downloadFile(@PathVariable String fileName,
HttpServletResponse response) throws IOException {
Path file = Paths.get(uploadDir, fileName);
if (!Files.exists(file)) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition",
"attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
response.setContentLengthLong(Files.size(file));
Files.copy(file, response.getOutputStream());
}
// 断点续传
@GetMapping("/download/resume")
public void downloadWithResume(@RequestParam String fileName,
@RequestHeader(required = false) String range,
HttpServletResponse response) throws IOException {
Path file = Paths.get(uploadDir, fileName);
long fileSize = Files.size(file);
if (range != null && range.startsWith("bytes=")) {
// 解析 Range 头
String[] ranges = range.substring(6).split("-");
long start = Long.parseLong(ranges[0]);
long end = ranges.length > 1 ? Long.parseLong(ranges[1]) : fileSize - 1;
response.setStatus(HttpStatus.PARTIAL_CONTENT.value());
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);
response.setContentLengthLong(end - start + 1);
try (RandomAccessFile raf = new RandomAccessFile(file.toFile(), "r")) {
raf.seek(start);
byte[] buffer = new byte[8192];
long remaining = end - start + 1;
while (remaining > 0) {
int len = (int) Math.min(buffer.length, remaining);
int read = raf.read(buffer, 0, len);
response.getOutputStream().write(buffer, 0, read);
remaining -= read;
}
}
} else {
response.setContentLengthLong(fileSize);
Files.copy(file, response.getOutputStream());
}
}
private String saveFile(MultipartFile file) throws IOException {
String originalName = file.getOriginalFilename();
String extension = originalName.substring(originalName.lastIndexOf("."));
String fileName = UUID.randomUUID().toString() + extension;
Path path = Paths.get(uploadDir, fileName);
Files.createDirectories(path.getParent());
Files.copy(file.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return fileName;
}
}
六、数据访问
6.1 JDBC 与 JPA
yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL8Dialect
format_sql: true
jdbc:
batch_size: 20
order_inserts: true
order_updates: true
java
// JPA 实体
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 50)
private String username;
@Column(nullable = false)
private String password;
@Column(length = 100)
private String email;
private Integer age;
@Enumerated(EnumType.STRING)
private UserStatus status;
@CreationTimestamp
private LocalDateTime createTime;
@UpdateTimestamp
private LocalDateTime updateTime;
@Version
private Integer version;
}
// JPA Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
List<User> findByAgeBetween(int minAge, int maxAge);
@Query("SELECT u FROM User u WHERE u.email LIKE %:domain%")
List<User> findByEmailDomain(@Param("domain") String domain);
@Modifying
@Transactional
@Query("UPDATE User u SET u.status = :status WHERE u.id = :id")
int updateStatus(@Param("id") Long id, @Param("status") UserStatus status);
@Query(value = "SELECT * FROM users WHERE age > :age", nativeQuery = true)
List<User> findByAgeNative(@Param("age") int age);
}
// JPA 使用示例
@Service
@Slf4j
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public User createUser(User user) {
return userRepository.save(user);
}
@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new BusinessException("用户不存在"));
}
@Transactional
public User updateUser(Long id, User user) {
User existing = findById(id);
existing.setUsername(user.getUsername());
existing.setEmail(user.getEmail());
existing.setAge(user.getAge());
return userRepository.save(existing);
}
@Transactional
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@Transactional
public void batchUpdate(List<User> users) {
userRepository.saveAll(users);
}
}
6.2 MyBatis 集成
xml
<!-- pom.xml 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
yaml
# application.yml
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
lazy-loading-enabled: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
java
// Mapper 接口
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE id = #{id}")
User selectById(Long id);
@Insert("INSERT INTO users(username, age) VALUES(#{username}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Update("UPDATE users SET age = #{age} WHERE id = #{id}")
int update(User user);
@Delete("DELETE FROM users WHERE id = #{id}")
int deleteById(Long id);
List<User> selectByCondition(@Param("username") String username,
@Param("age") Integer age);
}
// XML 映射文件
<!-- mapper/UserMapper.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="age" column="age"/>
</resultMap>
<select id="selectByCondition" resultMap="userMap">
SELECT * FROM users
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
</mapper>
6.3 多数据源配置
yaml
# application.yml
spring:
datasource:
primary:
url: jdbc:mysql://localhost:3306/primary_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
secondary:
url: jdbc:mysql://localhost:3306/secondary_db
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
java
// 主数据源配置
@Configuration
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
@Primary
public JdbcTemplate primaryJdbcTemplate(DataSource primaryDataSource) {
return new JdbcTemplate(primaryDataSource);
}
@Bean
@Primary
public PlatformTransactionManager primaryTransactionManager(DataSource primaryDataSource) {
return new DataSourceTransactionManager(primaryDataSource);
}
}
// 从数据源配置
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.secondary")
public class SecondaryDataSourceConfig {
@Bean
public DataSource secondaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(DataSource secondaryDataSource) {
return new JdbcTemplate(secondaryDataSource);
}
}
6.4 Redis 集成
yaml
# application.yml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(LazyJackson2TypedMapper.defaultTyping(),
ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
public void delete(String key) {
redisTemplate.delete(key);
}
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void increment(String key, long delta) {
redisTemplate.opsForValue().increment(key, delta);
}
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public void addToList(String key, Object value) {
redisTemplate.opsForList().rightPush(key, value);
}
public List<Object> getList(String key, long start, long end) {
return redisTemplate.opsForList().range(key, start, end);
}
public void addToHash(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public Object getFromHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
}
6.5 缓存管理
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(60))
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
@Service
public class UserService {
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUser(Long id) {
return userRepository.findById(id).orElse(null);
}
@Cacheable(value = "users", key = "'list:' + #page + ':' + #size")
public PageResult<User> getUsers(int page, int size) {
return userRepository.findAll(PageRequest.of(page - 1, size));
}
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearCache() {
// 清空所有缓存
}
@Caching(evict = {
@CacheEvict(value = "users", key = "#user.id"),
@CacheEvict(value = "users", key = "'list:*'", allEntries = true)
})
public User updateUserWithCacheClear(User user) {
return userRepository.save(user);
}
}