跳到主要内容Java 后端实习复盘:企业级项目架构与核心模块解析 | 极客日志JavaSaaSjava
Java 后端实习复盘:企业级项目架构与核心模块解析
Java 后端实习期间深入参与了企业级后台项目开发,重点实践了权限控制、认证授权与高并发场景下的数据处理。内容涵盖基于 Spring AOP 的权限注解实现、JWT 结合 Redis 的 Token 管理机制、缓存穿透防护的双重校验锁策略,以及分布式锁与自定义线程池的配置优化。同时复盘了缓存失效与 Token 续期安全等常见 Bug 的排查思路,提供了从环境搭建到核心模块落地的完整技术视角。
云朵棉花糖2 浏览 **摘要:**实习期间参与企业后台项目开发,熟悉企业开发流程与代码规范。


实习核心流程(结合实际经历)
由于进入的是一个小公司实习,当时项目刚好启动,参与了较多基础模块的开发。整体节奏可以概括为环境搭建、业务熟悉到独立开发的三个阶段。
一、基础准备与环境搭建阶段(入职 1-3 天)

- 办公权限配置:开通飞书账号、个人邮箱等基础权限。
- 代码拉取与环境搭建:
- 使用 Git/SVN 拉取项目代码(公司采用阿里云云效),配置后端 + 前端开发环境。
- 解决依赖问题,确保项目能正常跑起来,熟悉配置文件结构。
- 掌握开发工具快捷键与 Debug 技巧,减少操作耗时。
二、项目熟悉阶段(入职 1-2 周)
这个阶段主要是熟悉环境和通用封装。除了阅读代码,我还尝试寻找少量 Bug 并提交给 Mentor 审核,同时完成简单的 Demo 任务来验证框架掌握程度。
1. 基础认知
- 学习公司核心业务范围及业务流程,明确岗位能力要求。
- 由组长讲解项目核心模块划分、技术架构及上下游依赖关系。
2. 深度熟悉
- 梳理目录结构与模块交互逻辑,深入理解数据库表设计。
- 熟读 Common 通用包,掌握工具类调用方式。
- 拆解业务三层架构(控制层、服务层、数据层),对照接口文档理解实现思路。
3. 代码实践
- 基于现有框架完成简单 Demo,覆盖核心业务场景的基础流程。
三、初步实践阶段(入职 2 周后)
熟悉业务后快速投入开发,负责管理模块的增删改查。期间也参与了复杂任务的讨论,涉及 Redisson 分布式锁、第三方平台 Token 无感续期等场景。重点观察了技术负责人如何利用 Redis 缓存、Redisson 结合自定义注解+AOP 实现方法级分布式锁,以及自定义线程池处理异步日志等实际问题的方案。
熟悉企业项目
权限管理模块
这里展示了一个自定义权限注解的实现,通常用于标记需要权限校验的类或方法。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public Auth {
String[] value() ;
}
@interface
default
""
配合 AOP 切面 AuthAspect 进行拦截。核心在于利用 Spring AOP 的 @Pointcut 定义切点表达式 @annotation(auth),拦截所有标注了 @Auth 的方法,并将注解实例注入通知方法中。
@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class AuthAop {
@Pointcut("@annotation(auth)")
public void controllerAspect(Auth auth) { }
@Around(value = "controllerAspect(auth)", argNames = "proceedingJoinPoint,auth")
public Object aroundAuth(ProceedingJoinPoint proceedingJoinPoint, Auth auth) throws Throwable {
LoginUser loginUser = LoginUserHandler.getLoginUser();
if (loginUser == null) {
throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR);
}
String role = Optional.ofNullable(loginUser.getRole()).orElse("").toUpperCase();
if (!Arrays.asList(auth.value()).contains(role)) {
throw new BusinessException(ResponseCode.INSUFFICIENT_PERMISSIONS);
}
Object[] args = proceedingJoinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
arg = loginUser;
}
}
return proceedingJoinPoint.proceed(args);
}
}
此外,登录过滤器 LoginFilter 作为请求进入系统的认证关卡,执行流程如下:
- 初始化阶段:将配置的白名单 URL 解析为
PathPattern 格式并缓存。
- 核心过滤阶段:所有请求进入后,先判断是否匹配白名单路径,若匹配则直接放行;否则进入认证处理。
- 认证处理:区分 OpenAPI 请求与普通用户认证。普通用户需从请求头获取 Token 并解析,OpenAPI 则校验 API Key 和版本信息。
- 上下文清理:无论成功与否,最终在
finally 块中清理用户上下文,避免内存泄漏。
配置绑定类 SecurityIgnoreUrls 通过 @ConfigurationProperties 将 secure.ignored.urls 前缀的配置绑定到 urls 属性上,方便代码直接获取白名单列表。
Token 管理模块
Token 服务 TokenService 负责 JWT 的创建、解析、刷新与注销。核心流程包括:
- Token 创建:生成 UUID 作为唯一标识,构建 JWT 载荷,使用 HS512 签名,并存入 Redis 设置过期时间。
- Token 解析:从请求头提取 Token,验证签名后从 Redis 获取用户信息。
- Token 刷新:当检测到剩余有效期不足时,重新生成 JWT 并更新 Redis 中的过期时间。
- Token 注销:删除 Redis 中对应的 Key,使 Token 失效。
@Slf4j
@Component
public class TokenService {
public static final String TOKEN_PREFIX = "Bearer ";
public LoginUser getLoginUser(HttpServletRequest request) {
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String username = claims.get(USERNAME, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
return redisService.get(key, LoginUser.class);
} catch (Exception e) {
log.error("获取用户信息异常:{}", e.getMessage());
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL);
}
}
public String createToken(LoginUser loginUser) {
String uuid = UUID.randomUUID().toString();
Map<String, Object> claims = new HashMap<>();
claims.put(TOKEN, uuid);
claims.put(USERNAME, loginUser.getUsername());
String token = Jwts.builder()
.claims(claims)
.expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000))
.signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512)
.compact();
loginUser.setToken(token);
refreshToken(loginUser, uuid);
return token;
}
public void refreshToken(LoginUser loginUser, String uuid) {
try {
Map<String, Object> claims = new HashMap<>();
claims.put(TOKEN, uuid);
claims.put(USERNAME, loginUser.getUsername());
String newToken = Jwts.builder()
.claims(claims)
.expiration(new Date(System.currentTimeMillis() + expirationHours * 3600 * 1000))
.signWith(Keys.hmacShaKeyFor(secretKey), Jwts.SIG.HS512)
.compact();
loginUser.setToken(newToken);
String key = String.format(FORMAT, LOGIN_USER, uuid, loginUser.getUsername());
redisService.set(key, loginUser, Duration.ofHours(expirationHours));
log.debug("Token 刷新成功,用户名:{},新 Token 过期时间:{}小时", loginUser.getUsername(), expirationHours);
} catch (Exception e) {
log.error("Token 刷新异常,用户名:{}", loginUser.getUsername(), e);
throw new BusinessException("Token 续期失败!");
}
}
public String getToken(HttpServletRequest request) {
String token = request.getHeader(header);
if (StringUtils.isNotBlank(token) && token.startsWith(TOKEN_PREFIX)) {
return token.substring(TOKEN_PREFIX.length());
}
throw new BusinessException(ResponseCode.JWT_TOKEN_PARSING_ERROR);
}
public void logout(String username) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(
RequestAttributes.REFERENCE_REQUEST);
if (request == null) {
return;
}
try {
Claims claims = parseToken(getToken(request));
String tokenId = claims.get(TOKEN, String.class);
String key = String.format(FORMAT, LOGIN_USER, tokenId, username);
redisService.del(key);
} catch (Exception e) {
log.error("登出时删除 Token 异常:{}", e.getMessage());
}
}
}
参与企业项目
企业管理模块
cacheAll
这是企业级系统中'全量数据缓存加载'的典型逻辑,遵循'缓存查询→双重校验加锁→数据库查询→缓存写入'的流程。
public List<Company> cacheAll() {
final String cacheKey = "company:all";
List<Company> cachedList = redisService.get(cacheKey, List.class);
if (cachedList != null) {
return cachedList;
}
synchronized (cacheLock) {
cachedList = redisService.get(cacheKey, List.class);
if (cachedList != null) {
return cachedList;
}
List<Company> list = list();
redisService.set(cacheKey, list, 3600);
return list;
}
}
核心在于双重校验加锁,避免缓存击穿。一级缓存未命中时,加本地同步锁再次检查,确保只有一个线程去查库。
getCompanyFromCache
基于全量缓存的单条实体精准查询,复用 cacheAll() 的结果,通过 Stream 流过滤定位目标实体,返回 Optional 类型保证空值安全。
getCompanyList
分页列表查询结合了参数校验与数据转换。@Validated 注解自动校验入参合法性,避免手动编写大量 if-else。Controller 层接收 DTO,Service 层调用 Repository 进行分页查询,最后通过 PageUtil.getPage 工具类将 Entity 转换为 VO 并透传分页元数据。
@PostMapping("/list")
public CommonResult<SimplePage<CompanyListVO>> getCompanyList(@RequestBody @Validated CompanyListDTO dto){
return CommonResult.success(companyService.getCompanyList(dto,U.get()));
}
getCompanyDetailById
单实体详情查询同样优先走缓存。如果缓存命中且权限校验通过则直接返回;若未命中则查库兜底,并主动更新缓存以保证后续命中率。
getCompanyDropDownList
下拉列表数据查询优化。优先调用 cacheAll() 加载全量数据替代直接查库,通过流式过滤实现多维度条件校验(匹配 ID、数据权限、排除系统默认项),排序后映射为专用 VO。
addCompany
新增实体的典型实现。包含唯一性校验(防止重复创建)、参数转换、事务持久化、缓存清理以及扩展业务处理(如创建企业主账号)。特别使用了 @Lock4j 基于企业名称加锁,解决高并发下的重复创建问题。
updateCompany
实体更新逻辑。先校验目标是否存在,再校验当前用户权限(管理员或归属企业),最后执行更新并清理缓存。同样通过 @Lock4j 基于企业 ID 加分布式锁保障并发安全。
delCompany
实体删除逻辑。除存在性校验外,还包含特殊规则校验(禁止删除核心企业)和关联数据校验(检查是否有设备或子账号)。删除成功后调用 removeCache() 清理缓存,避免脏数据。
企业用户账号管理模块
sysUserList
系统用户分页列表查询。Mapper 层通过 XML 构建动态 SQL,支持多条件过滤及权限隔离(企业用户只能看自己公司的数据)。Service 层将角色字符串拆分为列表补充到 VO 中。
userDetail
用户详情查询。参数强制非空校验,查询结果同样需要将角色字符串拆分格式化。
systemUserResetPwd
密码重设功能。包含权限校验(仅本人或管理员可操作)、密码加密存储、分布式锁防并发重置,以及重置后强制旧 Token 失效(安全登出)。
addSysUser
添加系统用户。校验用户名唯一性及关联企业存在性,密码加密后保存,支持批量角色分配。
第三方系统访问模块
getToken
开放平台 Token 生成逻辑。验证 API Key 和 Secret,撤销旧 Token 保证唯一性,生成新 Token 并持久化到 Redis,设置过期时间。
refreshToken
Token 刷新逻辑。检测剩余有效期,不足 30 分钟时生成新 Token 并同步 Redis,同时必须失效旧 Token 以防止多 Token 有效风险。
代码规范
判空工具类
推荐使用 Hutool 的 ObjectUtil 覆盖 99% 的场景。
- 字符串判空:
isEmpty 判断 null 或空串,isBlank 判断 null、空串或纯空格。
- 基本数据类型:不能判 null,只能判断默认值(如
num == 0)。
- 包装类:兼顾引用为空与值为默认值的情况。
- 集合/数组:兼顾引用为空与长度为 0。
判空注解
@NotNull:仅判定是否为 null。
@NotBlank:针对 String,要求非 null 且去除空格后长度大于 0。
@NotEmpty:针对 String 或集合,要求非 null 且长度/大小大于 0。
日志框架
Logback 配置示例,包含文件输出、控制台输出及异步优化。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<appender name="FILE">
<rollingPolicy>
<FileNamePattern>/var/log/${appName}/%d{yyyy-MM-dd,aux}/%d{yyyy-MM-dd HH}.log</FileNamePattern>
<maxHistory>744</maxHistory>
</rollingPolicy>
<encoder>
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg %n</Pattern>
</encoder>
</appender>
<appender name="STDOUT">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
<filter>
<level>DEBUG</level>
</filter>
</appender>
<appender name="ASYNC_FILE">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="FILE"/>
</appender>
<appender name="ASYNC_STDOUT">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="STDOUT"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ASYNC_STDOUT"/>
</root>
</configuration>
核心作用包括:按小时切分日志文件、保留 30 天历史、异步输出避免阻塞主线程、统一全局日志级别。
Optional 判空
简化传统判空写法,例如 Optional.ofNullable(e.getMessage()).orElse("")。
切面优先级
@Transactional 默认优先级最低,自定义切面可通过 @Order 调整。数值越小优先级越高,确保 LockAop 在事务提交前执行加锁逻辑。
通用能力封装
RedisService 封装类
统一 Redis 配置与操作接口,包括序列化配置、缓存管理器及各类数据结构(String, Hash, Set, List)的操作封装。
@EnableCaching
@Configuration
@Slf4j
public class BaseRedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisSerializer<Object> serializer = redisSerializer();
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisSerializer<Object> redisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
return new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
}
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
@Bean
public RedisService redisService(RedisTemplate<String, Object> redisTemplate) {
return new RedisServiceImpl(redisTemplate);
}
}
Redission 实现分布式锁
通过自定义 @Lock4j 注解结合 AOP 落地 Redisson 分布式锁。核心流程包括 SpEL 表达式解析 Key、参数适配超时时间、加锁尝试、业务执行及 finally 块解锁。
@Aspect
@Slf4j
@Order(-10)
@Component
public class LockAop {
public static final String SEPARATOR = ":";
private final long lockWatchdog;
private final long attempt;
private final RedissonClient redissonClient;
public LockAop(@Value("${redisson.lockWatchdog}") long lockWatchdog,
@Value("${redisson.attempt}") long attempt,
RedissonClient redissonClient) {
this.lockWatchdog = lockWatchdog;
this.attempt = attempt;
this.redissonClient = redissonClient;
}
@Pointcut("@annotation(lock4j)")
public void controllerAspect(Lock4j lock4j) { }
@Around(value = "controllerAspect(lock4j)", argNames = "proceedingJoinPoint,lock4j")
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) throws Throwable {
String keys = lock4j.key();
if (!StringUtils.hasText(keys)) {
throw new BusinessException(ResponseCode.KEYS_EMPTY);
}
String key = getRedissonKey(proceedingJoinPoint, lock4j);
long attemptTimeout = lock4j.attemptTimeout() == 0 ? attempt : lock4j.attemptTimeout();
long lockWatchdogTimeout = lock4j.lockWatchdogTimeout() == 0 ? lockWatchdog : lock4j.lockWatchdogTimeout();
boolean res = false;
RLock rLock = redissonClient.getLock(lock4j.lockType() + SEPARATOR + key);
if (rLock != null) {
try {
if (attemptTimeout == -1) {
res = true;
rLock.lock(lockWatchdogTimeout, TimeUnit.MILLISECONDS);
} else {
res = rLock.tryLock(attemptTimeout, lockWatchdogTimeout, TimeUnit.MILLISECONDS);
}
log.info("Lock:{},interrupted:{},hold:{},threadId:{} ", rLock.getName(), Thread.currentThread().isInterrupted(), rLock.isHeldByCurrentThread(), Thread.currentThread().threadId());
if (res) {
return proceedingJoinPoint.proceed();
} else {
throw new BusinessException(ResponseCode.GET_LOCK_ERROR);
}
} finally {
if (res && rLock.isLocked()) {
rLock.unlock();
}
}
}
throw new BusinessException(ResponseCode.REPEATED_SUBMIT);
}
private String getRedissonKey(ProceedingJoinPoint proceedingJoinPoint, Lock4j lock4j) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
String[] parameterNames = new StandardReflectionParameterNameDiscoverer().getParameterNames(method);
Object[] parameterValues = proceedingJoinPoint.getArgs();
for (int i = 0; i < Objects.requireNonNull(parameterNames).length; i++) {
context.setVariable(parameterNames[i], parameterValues[i]);
}
Expression expression = parser.parseExpression(lock4j.key());
Object realValue = expression.getValue(context);
return lock4j.lockType() + realValue;
}
}
CompletableUtil 工具包
封装 CompletableFuture 支持线程/虚拟线程异步执行。相比传统 Thread,优势在于异步回调、多任务组合、异常处理及线程池适配。
基础用法包括异步执行无返回值、有返回值、结果回调及异常处理。进阶用法支持串行嵌套、并行全部完成合并、任意一个完成返回等场景。
定时任务线程池
配置 ThreadPoolTaskScheduler 替代默认单线程调度器,设置核心线程数、线程命名前缀及优雅停机策略,提升定时任务并发能力。
自定义线程池实现
根据 CPU 核心动态设置核心线程数,使用有界队列、自定义命名线程工厂与安全拒绝策略(CallerRunsPolicy),避免资源耗尽与 OOM。
AOP 实现日志记录
通过 @AfterReturning 和 @AfterThrowing 记录操作日志,过滤敏感字段(如密码),异步保存日志至数据库,避免影响主业务性能。
Bug 处理
缓存失效问题
发现缓存机制未接入实际业务流程,核心业务方法直接操作数据库,导致 removeCache() 形同虚设。解决方案是确保所有对外核心业务方法均正确读写缓存,并在变更数据时及时清理对应缓存键。
续期后旧 Token 未失效
Token 续期时仅生成新 Token 并同步 Redis,未删除旧 Token,导致同一账号多 Token 有效。解决办法是在续期逻辑中新增调用 invalidateOldToken 方法,检索并删除 Redis 中旧 Token 的缓存 Key,彻底禁用旧 Token。
public String refreshToken(HttpServletRequest request) {
Long expireTime = tokenService.getExpireTime(request);
if (expireTime < 30 * 60) {
String oldToken = tokenService.getToken(request);
LoginUser loginUser = tokenService.getLoginUser(request);
if (loginUser == null) {
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "旧 Token 无效,无法刷新");
}
String newToken = tokenService.createToken(loginUser);
tokenService.invalidateOldToken(oldToken);
return newToken;
}
return tokenService.getToken(request);
}
public void invalidateOldToken(String oldToken) {
if (StringUtils.isBlank(oldToken)) {
log.warn("失效旧 Token 失败:Token 为空");
return;
}
try {
Claims claims = parseToken(oldToken);
String oldTokenId = claims.get(TOKEN, String.class);
String username = claims.get(USERNAME, String.class);
String oldKey = String.format(FORMAT, LOGIN_USER, oldTokenId, username);
if (redisService.del(oldKey)) {
log.info("失效旧 Token 成功,username:{},tokenId:{}", username, oldTokenId);
} else {
log.warn("失效旧 Token 失败:Redis 中未找到该 Token 缓存,username:{},tokenId:{}", username, oldTokenId);
}
} catch (Exception e) {
log.error("失效旧 Token 异常:{}", e.getMessage(), e);
throw new BusinessException(ResponseCode.AUTHENTICATION_FAIL, "失效旧 Token 失败");
}
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online