分享
获课地址:666it.top/15776/
SpringBoot实战项目教程 - 无惧面试
项目概述
这是一个完整的SpringBoot电商后台管理系统实战项目,涵盖了面试中常见的技术点和业务场景。
技术栈
后端: SpringBoot 2.7+, Spring Security, JWT, MyBatis-Plus, Redis
数据库: MySQL 8.0, Redis 7.0
工具: Maven, Swagger, Logback, Docker
项目结构
text
springboot-interview-project
├── src/main/java/com/interview
│ ├── config/ # 配置类
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ ├── security/ # 安全配置
│ └── utils/ # 工具类
├── resources/
│ ├── application.yml # 配置文件
│ ├── mapper/ # XML映射文件
│ └── sql/ # 数据库脚本
└── test/ # 测试代码
快速开始
1. 数据库准备
sql
CREATE DATABASE interview_db CHARACTER SET utf8mb4;
USE interview_db;
-- 用户表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL UNIQUE,
`password` varchar(100) NOT NULL,
`email` varchar(100),
`phone` varchar(20),
`status` tinyint DEFAULT 1 COMMENT '1-正常, 0-禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 商品表
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(200) NOT NULL,
`price` decimal(10,2) NOT NULL,
`stock` int NOT NULL DEFAULT 0,
`category_id` bigint,
`description` text,
`status` tinyint DEFAULT 1 COMMENT '1-上架, 0-下架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 核心代码实现
2.1 SpringBoot主启动类
java
@SpringBootApplication
@MapperScan("com.interview.mapper")
@EnableCaching
@EnableScheduling
public class InterviewApplication {
public static void main(String[] args) {
SpringApplication.run(InterviewApplication.class, args);
}
}
2.2 用户认证模块(JWT + Spring Security)
JWT工具类
java
@Component
public class JwtTokenUtil {
private final String secret = "interview-secret-key";
private final Long expiration = 86400000L; // 24小时
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", userDetails.getUsername());
claims.put("created", new Date());
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
Security配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
2.3 MyBatis-Plus配置与使用
配置类
java
@Configuration
@MapperScan("com.interview.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
BaseEntity(包含审计字段)
java
@Data
public abstract class BaseEntity {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@Version
private Integer version;
}
2.4 Redis缓存实现
Redis配置
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson序列化
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(mapper);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
商品服务层(缓存示例)
java
@Service
@Slf4j
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String PRODUCT_CACHE_KEY = "product:";
private static final String PRODUCT_LIST_CACHE_KEY = "product:list";
@Cacheable(value = "product", key = "#id")
public ProductVO getProductById(Long id) {
log.info("查询数据库获取商品: {}", id);
Product product = productMapper.selectById(id);
return convertToVO(product);
}
@CacheEvict(value = "product", key = "#id")
public void updateProduct(ProductDTO productDTO) {
Product product = convertToEntity(productDTO);
productMapper.updateById(product);
// 清除列表缓存
redisTemplate.delete(PRODUCT_LIST_CACHE_KEY);
}
@Cacheable(value = "product", key = "'list:' + #page + ':' + #size")
public PageResult<ProductVO> getProductList(int page, int size, String keyword) {
Page<Product> pageInfo = new Page<>(page, size);
LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Product::getName, keyword);
}
queryWrapper.orderByDesc(Product::getCreateTime);
Page<Product> result = productMapper.selectPage(pageInfo, queryWrapper);
return new PageResult<>(
result.getRecords().stream().map(this::convertToVO).collect(Collectors.toList()),
result.getTotal(),
result.getCurrent(),
result.getSize()
);
}
}
2.5 全局异常处理
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<?>> handleBusinessException(BusinessException e) {
log.error("业务异常: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(e.getCode(), e.getMessage()));
}
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<?>> handleAccessDeniedException(AccessDeniedException e) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(403, "权限不足"));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleException(Exception e) {
log.error("系统异常: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "系统异常,请稍后重试"));
}
}
2.6 日志配置(Logback)
xml
<!-- src/main/resources/logback-spring.xml -->
<configuration>
<property name="LOG_PATH" value="./logs"/>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/application.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- SQL日志 -->
<logger name="com.interview.mapper" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
</logger>
</configuration>
3. 面试重点功能实现
3.1 分布式ID生成(雪花算法)
java
@Component
public class SnowflakeIdGenerator {
private final long twepoch = 1625068800000L; // 起始时间戳
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxDatacenterId = ~(-1L << datacenterIdBits);
private final long sequenceMask = ~(-1L << sequenceBits);
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(@Value("${snowflake.worker-id:1}") long workerId,
@Value("${snowflake.datacenter-id:1}") long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId不合法");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId不合法");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨异常");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
}
3.2 异步处理与线程池
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(50);
// 队列容量
executor.setQueueCapacity(200);
// 线程存活时间
executor.setKeepAliveSeconds(60);
// 线程名前缀
executor.setThreadNamePrefix("async-task-");
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
@Service
@Slf4j
public class OrderService {
@Async("taskExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public CompletableFuture<Void> processOrderAsync(Long orderId) {
log.info("开始异步处理订单: {}", orderId);
// 模拟业务处理
try {
Thread.sleep(3000);
// 发送邮件通知
sendOrderEmail(orderId);
// 更新库存
updateStock(orderId);
// 记录日志
saveOrderLog(orderId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("订单处理完成: {}", orderId);
return CompletableFuture.completedFuture(null);
}
}
3.3 接口限流(Redis + Lua)
java
@Component
public class RateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String RATE_LIMIT_SCRIPT =
"local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local window = tonumber(ARGV[2])\n" +
"local current = redis.call('get', key)\n" +
"if current == false then\n" +
" redis.call('setex', key, window, 1)\n" +
" return 1\n" +
"elseif tonumber(current) < limit then\n" +
" redis.call('incr', key)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
public boolean tryAcquire(String key, int limit, int window) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(RATE_LIMIT_SCRIPT);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(window));
return result != null && result == 1;
}
}
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Autowired
private RateLimiter rateLimiter;
@Around("@annotation(rateLimit)")
public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = rateLimit.key();
int limit = rateLimit.limit();
int window = rateLimit.window();
if (!rateLimiter.tryAcquire(key, limit, window)) {
throw new BusinessException("请求过于频繁,请稍后重试");
}
return joinPoint.proceed();
}
}
4. 测试用例
java
@SpringBootTest
@AutoConfigureMockMvc
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
@WithMockUser(username = "admin", roles = {"ADMIN"})
void testCreateProduct() throws Exception {
ProductDTO productDTO = new ProductDTO();
productDTO.setName("测试商品");
productDTO.setPrice(new BigDecimal("99.99"));
productDTO.setStock(100);
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(productDTO)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data.name").value("测试商品"));
}
@Test
void testRateLimit() throws Exception {
for (int i = 0; i < 11; i++) {
mockMvc.perform(get("/api/public/test"))
.andDo(MockMvcResultHandlers.print());
}
// 第11次应该被限流
mockMvc.perform(get("/api/public/test"))
.andExpect(status().isTooManyRequests());
}
}
5. 部署配置
Docker部署文件
dockerfile
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/springboot-interview-1.0.0.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Docker Compose配置
yaml
version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: interview_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7.0-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
app:
build: .
depends_on:
- mysql
- redis
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/interview_db
SPRING_REDIS_HOST: redis
ports:
- "8080:8080"
volumes:
mysql_data:
redis_data:
6. 面试准备要点
高频面试问题
SpringBoot自动配置原理
@SpringBootApplication组合注解
spring.factories机制
条件注解@Conditional
Spring事务管理
声明式事务@Transactional
传播行为(Propagation)
隔离级别(Isolation)
数据库优化
索引设计与优化
分库分表策略
读写分离实现
缓存穿透/击穿/雪崩解决方案
布隆过滤器
缓存空对象
互斥锁
热点数据永不过期
分布式锁实现
Redis分布式锁(Redisson)
Zookeeper分布式锁
数据库乐观锁
项目亮点提炼
代码规范:统一异常处理、日志记录、API响应格式
性能优化:Redis缓存、数据库连接池、异步处理
安全性:JWT认证、接口限流、SQL防注入
可维护性:模块化设计、清晰的包结构、完整的文档
可扩展性:微服务化预留接口、配置外部化
7. 项目运行
克隆项目并导入IDE
修改application.yml中的数据库配置
运行SQL脚本初始化数据库
启动Redis服务
运行InterviewApplication
访问 http://localhost:8080/swagger-ui.html
这个项目涵盖了SpringBoot开发中的核心技术和面试常见问题,通过实际编码掌握这些内容,可以帮助你在面试中更有信心。建议在学习过程中多思考、多实践,理解每个技术点的原理和适用场景。
有疑问加站长微信联系(非本文作者))
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
关注微信12 次点击
添加一条新回复
(您需要 后才能回复 没有账号 ?)
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码` - 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传