做一个复杂的数据计算需求,执行过程被拆分为几十个Service(服务)和Calculator(计算器)类,且在其中又调用了十几个基础数据的Service类。计算逻辑开发好后,结构清楚,但执行时间很长,需要做性能优化。那么,应该优化哪些地方,即哪些地方是性能瓶颈?这需要对执行时间做统计和分析。
我们利用Spring的AOP功能实现对方法做执行时间计时。
例如,对于Service,做如下切面类:
@Aspect
@Component
@Slf4j
public class ServiceTimingAspect {
/**
* 切点
* <p>切点为{@code com.company.project}包下任意层级的{@code service}包中的所有类的所有方法。</p>
*/
@Pointcut("execution(* com.company.project..service..*(..))")
private void pointcut() {}
/**
* 环绕通知,执行并计时
* @param joinPoint 连接点
* @return 切点的返回值
* @throws Throwable 切点的抛出对象
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 执行并计时
var startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
var endTime = System.currentTimeMillis();
var duration = endTime - startTime;
// 方法签名
var methodSignature = joinPoint.toString();
methodSignature = methodSignature.substring("execution(".length(), methodSignature.length() - 1);
logger.debug("方法签名{}耗时: {}ms", methodSignature, duration);
// 实参列表
var args = Arrays.toString(joinPoint.getArgs());
args = args.substring(1, args.length() - 1);
args = args.replace("\n", "\\n").replace("\t", "\\t");
// 方法调用
var methodCall = methodSignature.substring(0, methodSignature.indexOf("(") + 1) + args + ")";
logger.debug("方法调用{}耗时: {}ms", methodCall, duration);
return result;
}
}
通过配置logback-spring.xml将切面类的日志同时输出到控制台和专门的计时日志文件:
<!-- 输出到计时日志文件的Appender -->
<appender name="TIMING_LOG_FILE" class="ch.qos.logback.core.FileAppender">
<file>log/timing.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 切面类的Logger -->
<logger name="切面类所在的包名" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="TIMING_LOG_FILE"/>
</logger>
编写TimingLogAnalyzer类,通过main()方法执行,读取计时日志文件,统计每个方法签名、方法调用的执行次数、总耗时、最小耗时、最大耗时、平均耗时,然后将统计结果输出到Excel文件中。
接下来,就可以在Excel文件中通过排序、统计图等手段,分析出性能瓶颈的所在,进而针对性地做性能优化。
功能增强:
- 对
Mapper、Calculator、Controller或者其他类做计时切面类。尤其是Mapper,可以用于分析缓慢的数据库查询。 - 通过配置和
@ConditionalOnProperty控制切面类的装配,线上关闭计时,本地也只在需要分析时才开启计时。