
示例1静态 keyCacheable(value capitalOptions, key all) public CapitalAllOptionDTO queryCapitalAllOption() {示例2动态 key一个参数Cacheable(value users, key #userName) public User queryByUserName(String userName) { return userMapper.selectByUserName(userName); }示例3动态 key多个参数Cacheable(value users, key #commandType - #id) public ListUser queryUserListByCommand(String commandType, Integer id) {示例4动态 key使用 SpEL 调用静态方法Cacheable(value tasks, key T(com.weiyu.util.PublicUtils).getUserName()) public ListTask queryTask() { return taskMessageMapper.selectTask(PublicUtils.getUserName()); }示例5动态 key对象单个属性并且不能为空Cacheable(value startActivityInstances, key #p0?.activityId, unless #p0?.activityId null) private ActivityInstance getStartActivityInstance(ActivityInstanceRequest activityInstanceRequest) {示例6动态key对象多个属性并且都不能为空Cacheable(value rejectOptions, key #p0?.activityId - #p0?.activityInstanceId - #p0?.workflowInstanceId, unless #p0?.activityId null || #p0?.activityInstanceId null || #p0?.workflowInstanceId null) public ActivityInstanceRejectOptionDTO queryRejectOption(ActivityInstanceRequest activityInstanceRequest) { 推荐写法兼顾安全与清晰这样既避免了 SpEL 空指针又防止了null作为缓存键。但注意unless表达式中的?.也是可选的因为#p0不会为null参数非null但为了一致性可以保留。p0是指第一个参数可以这样设置缓存 key但需要注意几个关键点✅ 为什么可行Cacheable(value activityInstanceRequests, key #activityInstanceRequest.getActivityId())使用了 SpEL 表达式会调用参数的getActivityId()方法作为缓存键。如果ActivityInstanceRequest对象在每次查询时都提供了正确的activityId且相同activityId对应的返回结果一致那么缓存就能正确命中。⚠️ 潜在问题与改进建议参数名解析SpEL 中的#activityInstanceRequest依赖于方法参数名。如果编译时没有保留参数名即未使用-parameters编译选项Spring 将无法识别这个名称导致表达式失效。建议使用基于索引的写法如#p0或#a0更安全javaCacheable(value activityInstanceRequests, key #p0.getActivityId()) // 或 Cacheable(value activityInstanceRequests, key #a0.activityId)空值处理如果activityId可能为null缓存键会变为null某些缓存实现如 Caffeine会抛出异常。建议确保activityId不会为空或使用#p0?.activityId做空安全处理返回null时缓存不会生效。直接使用属性访问SpEL 支持直接访问属性会自动调用 getter因此#p0.activityId比显式调用getActivityId()更简洁javaCacheable(value activityInstanceRequests, key #p0.activityId)与其他方法冲突如果同一个缓存区域activityInstanceRequests被其他方法使用且 key 生成逻辑不同可能导致数据混乱。建议为该区域单独命名或确保所有方法的 key 生成逻辑一致。最终推荐写法javaCacheable(value activityInstanceRequests, key #p0.activityId) public ActivityInstanceRejectOptionDTO queryRejectOption(ActivityInstanceRequest activityInstanceRequest) { // ... }这样既避免了参数名解析问题又简洁明了。你的写法Cacheable(value activityInstanceRequests, key #p0?.activityId)确实考虑了空安全如果activityId为null表达式返回null而不是抛出异常这比直接#p0.activityId更健壮。但它是否“最好”还需结合以下分析✅ 优点使用参数索引#p0避免了参数名解析问题兼容性好。安全导航操作符?.防止了activityId为null时 SpEL 解析抛出空指针异常。⚠️ 潜在问题null作为缓存键即使 SpEL 返回了null大多数缓存管理器如 Caffeine、Redis不允许null作为键会抛出NullPointerException或类似异常。因此仅靠?.并不能完全避免错误你需要确保activityId永远不为null或者在注解中显式排除null的情况。 更好的做法确保activityId不为null如果业务上activityId不可能为null那么直接使用#p0.activityId或#p0?.activityId都可以但加上?.是更安全的选择。结合condition或unless过滤javaCacheable(value activityInstanceRequests, key #p0.activityId, condition #p0.activityId ! null)或javaCacheable(value activityInstanceRequests, key #p0.activityId, unless #p0.activityId null)这样当activityId为null时方法不会被缓存但每次都会执行。如果编译时保留了参数名使用参数名更清晰javaCacheable(value activityInstanceRequests, key #request?.activityId) 推荐写法兼顾安全与清晰javaCacheable(value activityInstanceRequests, key #p0?.activityId, unless #p0?.activityId null)这样既避免了 SpEL 空指针又防止了null作为缓存键。但注意unless表达式中的?.也是可选的因为#p0不会为null参数非null但为了一致性可以保留。最终结论你提出的写法是一个很好的改进但并不是“最好”的完整形态。结合unless或业务逻辑确保activityId非空后它就能成为可靠且健壮的缓存键写法。