项目开发与学习记录:字段注入优化
前言
我总有一种什么大的要来了的危机感。还是尽快把项目做起来吧,现在全在弄底层的框架。这是一个两天的blog,前一天bug没修好,气到连blog都没写。
日程
5月7日
- 晚上7点:本来想玩游戏的,但转念一想,还是学习吧。
- 晚上9点:做完blog后针对并发安全性进行了优化,看看网课,补补学科内容。
学习记录
计算机网络
- CIDR
- 路由聚合
操作系统
- 虚拟内存
- 请求分页管理:缺页中断结构
- 页面置换算法
学习内容
省流
- params字段注入
1. params字段注入
这部分要完成的任务看上去比较简单。这是原来的手动注入模式:
String sql = "UPDATE movies SET " +
"title = COALESCE(?, title), " +
"release_date = COALESCE(?, release_date), " +
"poster_url = COALESCE(?, poster_url), " +
"duration = COALESCE(?, duration), " +
"genre = COALESCE(?, genre), " +
"rating = COALESCE(?, rating), " +
"status = COALESCE(?, status), " +
"updated_at = NOW() " +
"WHERE id = ?";
JdbcUtils.executeUpdate(sql,
movie.getTitle(),
movie.getReleaseDate(),
movie.getPosterUrl(),
movie.getDuration(),
movie.getGenre(),
movie.getRating(),
movie.getStatus(),
movie.getId());
目标是将SQL改造成这样,然后直接通过movie的字段进行注入:
String sql = "UPDATE movies SET " +
"title = COALESCE(#{title}, title), " +
"release_date = COALESCE(#{releaseDate}, release_date), " +
"poster_url = COALESCE(#{posterUrl}, poster_url), " +
"duration = COALESCE(#{duration}, duration), " +
"genre = COALESCE(#{genre}, genre), " +
"rating = COALESCE(#{rating}, rating), " +
"status = COALESCE(#{status}, status), " +
"updated_at = NOW() " +
"WHERE id = #{id}";
一开始想实现像mybatis那样的注解扫描模式。但发现这是一个大工程,因为我原有的bean注册器没有针对代理类的处理,而且这需要额外构建JdbcUtils
的方法句柄,这动摇到了我一开始的分模块理念。工具类是不应该出现在核心模块当中的,一个代理类的方法,外面再套了一层AOP,我不知道这中间的通知链是否会出问题(猜测不会)。除此之外,一旦进行了代理,调试起来就会变得比较复杂,而我连最基本的注入处理都没有弄好。所以这个想法暂时放弃了(真不是我懒哈)。
现在的方案是基于实体类的字段反射注入。
1)向外提供SQL参数提取方法
public static Object[] extractParams(String sql, Object... params) {
SqlTemplate template = Cache.SQL_TEMPLATE_CACHE.computeIfAbsent(sql, SqlTemplate::new);
Object[] result = new Object[template.paramNames.size()];
for (int i = 0; i < template.paramNames.size(); i++) {
String paramName = template.paramNames.get(i);
result[i] = findParamValue(paramName, params);
}
return result;
}
2)关键方法:从实体类中查找字段
for (Object param : params) {
if (param == null || param instanceof Map || param instanceof String) {
continue;
}
Class<?> clazz = param.getClass();
Map<String, MethodHandle[]> accessors = Cache.ACCESSOR_CACHE.computeIfAbsent(clazz, KatSimpleMapper::createAccessors);
MethodHandle getter;
try {
getter = accessors.get(paramName)[0]; // [0]是getter
} catch (Exception e) {
log.error("Failed to access field: {}", paramName);
log.error("Check accessors: {}", accessors);
throw new RuntimeException(e);
}
if (getter != null) {
try {
return getter.invoke(param); //调用实体类param的getter方法句柄获取值
} catch (Throwable e) {
throw new RuntimeException("Failed to access field: " + paramName, e);
}
}
}
增加对map和显式键值对的兼容:
// 1:查找显式键值对(如 "id", 123)
for (int i = 0; i < params.length; i++) {
if (params[i] instanceof String key && i + 1 < params.length) {
if (key.equals(paramName)) {
return params[i + 1];
}
}
}
// 2:查找 Map 中的参数
for (Object param : params) {
if (param instanceof Map<?, ?> map) {
if (map.containsKey(paramName)) {
return map.get(paramName);
}
}
}
并确定了查找优先级:键值对 > Map > 实体类字段。
3)缓存优化
对sql模板进行了缓存(貌似没有优化多少):
//sql模板缓存
private static final Map<String, SqlTemplate> SQL_TEMPLATE_CACHE = new ConcurrentHashMap<>();
将原来的setter缓存改成了更通用的字段缓存,这样无论是进行结果集映射还是字段注入,同一个类只需要缓存一次:
//缓存访问器数组 [getter, setter]
static final Map<Class<?>, Map<String, MethodHandle[]>> ACCESSOR_CACHE = new ConcurrentHashMap<>();
4)正则替换,将当前的特殊sql转回防注入sql
/**
* 将#{param}替换为?
*/
public static String replaceParamPlaceholders(String sql) {
return sql.replaceAll("#\\{\\w+}", "?");
}
结语
抓紧时间bro。