myorm【重点】
原work log,2020.6.23迁移至此
1类加载器自定义,解决jdbc jar包冲突;testcase
2基础类型crud:oracle日期插入,sequence,秒数据截断,oracle日期类型接收,BigDecimal类型转换
3join,循环依赖,借鉴spring ioc
4logback集成自定义类加载器
5threadlocal支持事务,包括oracle;select可重复读
6时区
7对业务层无感的testcase体系
8日志统一实践
9oracle主键回写
10事务支持隔离级别和只读
11结合自定义注解的事务动态代理切面,支持spring与guice(扫描service与dao包,扫到注解,则将代理注入容器),支持特定异常不回滚;被代理类的依赖对象运行期注入
12事务不能传播导致的问题推测
13事务结束时,修复autocommit和readonly,忽略隔离级别
14事务的简单传播,start,end,commit,rollback
15乐观锁
16join循环依赖缓存自动清除
17多数据源,mysql8必要参数时区伪装
18多数据源事务切面产生问题
19alias jdbc处理
20多数据源支持多时区,自定义注解+运行期类型反射识别
21多数据源支持多事务管理
22多数据源session继承关系梳理
23修复事务切面,代理类非代理方法,反射调用时的原始异常暴露
24queryForObject.clone 处理NO_FIELD 由于循环依赖的预缓存机制导致被覆盖问题
25 事务代理、synchronized、db rc及以上隔离级别 三者的矛盾
26 迁移到mybatis ,前后2个方案;mybatis orm低侵入整体解决方案
27 javassist运行期加载前篡改字节码
28 动态代理究竟会不会遗失被代理类,field和method上的注解
29 mybatis-guice源码,事务代理切面
30 aop代理,入站出站的log deprecated
32 夏令时冬令时解决方案 insert / update
33 夏令时冬令时解决方案 select
34 自定义动态代理切面ioc补偿性能
35 接11,结合自定义注解的事务动态代理切面(cglib)
36 静态资源基础数据加载
37 夏令时冬令时解决方案 select 修复,完美解决收官
38 mybatis-guice运行期修改数据源 mybatis guice 事务代理切面 5
#主导抽象ceftable重构
39 cglib asm冲突解决
40 接35,结合自定义注解的事务动态代理切面(cglib切子函数,真正的cglib) 当动态代理遇到ioc (四)真正的cglib
41 代理侵入框架与guice
42 一些java 日志实践 2021.2.10,此后若无特殊情况,不再在正文标注,仅在这个目录标注
43 当动态代理遇到ioc (五)使用cglib切面与自定义类加载器构建独有环境aop日志 2021.2.10
44 当动态代理遇到ioc (四)真正的cglib 8 事务切面调整为cglib后,一个大bug 2021.4.3 ~ 2021.4.28
45 当动态代理遇到ioc (四)真正的cglib 10 食物切面支持按名称getBeanfromguice 2021.4.30
46 2021.7.27 timecut/cache
使用threadlocal
47 jdk/cglib 继承抽象重构 2022.6.24 transaction 继承备案
48 cglib支持继承类 当动态代理遇到ioc (四)真正的cglib 11 2022.7.16
49 当动态代理遇到ioc (四)真正的cglib 12 cglib遇上debug的诡异问题 2022.7.16
50 transaction继承备案2 多数据源 mybatis guice 事务代理切面 6 多数据源事务 2022.7.25
=================正文
2020.2.18
spring+jdbc+myclassloader+pool2+junit
1)类加载器隔离朴实案例 意图、实现、问题
2)
//@ImportResource("testspringxml.xml") 跑testcase junit时提示找不到testspringxml.xml @ImportResource("classpath:testspringxml.xml")
@Bean @Qualifier("sybaseJdbcNativePool") // 注入工场的名字是下面这个,上面这个只对autowired有效,对getbean无效 public GenericObjectPool<AbstractJdbcUtil> sybaseJdbcNativePool(@Autowired SybaseProperties sybaseProperties) {
3)jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
4)如果使用spring boot 的 ApplicationContext 及 getbean中不实现~Aware的方式,运行test时Context注入失败
5)AbstractJdbcUtil中
static { DriverManager.setLoginTimeout(5); }
jdbc超时设置
public Connection getConnection() { if (conn == null) { synchronized (this) { if(conn == null) { try { String proxyName = "JdbcProxy"; if(classLoader == ClassLoader.getSystemClassLoader()) { proxyName = JdbcProxy.class.getName(); } Class.forName(driver, true, classLoader); Class proxy = classLoader.loadClass(proxyName); classLoader.loadClass(proxyName); Method method = proxy.getMethod("getConnection", String.class, String.class, String.class); conn = (Connection)method.invoke(null, url, user, pwd); // conn = DriverManager.getConnection(url, user, pwd); } catch (InvocationTargetException e) { throw new DBException(e.getTargetException()); } catch (Exception e) { throw new DBException(e); } } } } return conn; }
e.getTargetException,与超时设置两者配合,爆出:
java.sql.SQLException: JZ00M: Login timed out. Check that your database server is running on the host and port number you specified. Also, check the database server for other conditions (such as a full tempdb) that might be causing it to hang. com...trending.config.jdbc.DBException: java.sql.SQLException: JZ00M: Login timed out. Check that your database server is running on the host and port number you specified. Also, check the database server for other conditions (such as a full tempdb) that might be causing it to hang. at com...trending.config.jdbc.AbstractJdbcUtil.getConnection(AbstractJdbcUtil.java:64) ~[classes/:na]
此处的连接url用了xxx,是真的xxx哦,然后到时间爆了超时而不是直接连接异常抛出,jdbc是jconn3-6.0.jar,sybase
2020.2.27 orm
0)背景:类加载器隔离朴实案例
1)field反射要注意:getDeclaredField和getField的区别
2)field反射要注意一些静态成员变量,测试时加静态变量干扰
3)仅支持单列主键,不支持bean继承
4)映射类型有待完善
5)自定义类加载的类类型比较:类的相同通过对是否为同一个类加载器进行判断中的2、3
6)/
7)使用oracle sequence主键自增策略
new StringBuilder("select ").append(sequenceName).append(".nextval from sys.dual")
8)oracle插入、更新日期需要to_date函数(秒)to_timestamp(毫秒),不像mysql,直接给字符串
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
//to_date('2019-06-06 23:44:43', 'yyyy-mm-dd hh24:mi:ss')
to_timestamp('2012-5-13 18:45:34.567','yyyy-mm-dd hh24:mi:ss:ff3') db类型为timestamp(6)
9)java.sql.timestamp extends java.util.Date,所以mysql的datetime和oracle的timestamp(6)在jdbc层面都用java.sql.timestimp接收,然后再放到java.util.Date类型字段中没出错,BigDecimal(oracle的number类型、mysql的decimal类型使用的jdbc层接收类型)到Double就不行,要显式转换,第5)点中有详细代码参考
10)秒级数据截断
// Class oracleDate = getOracleJdbcClassLoader().loadClass(oracle.sql.TIMESTAMP.class.getName()); // System.out.println(System.identityHashCode(oracle.sql.TIMESTAMP.class)); // System.out.println(System.identityHashCode(oracleDate)); // System.out.println(System.identityHashCode(val.getClass())); // if(val.getClass() == oracleDate) { // if(fcl == Date.class) { // Method method = oracleDate.getMethod("timestampValue"); // val = method.invoke(val); // if(cutMilliSecond) { // Date temp = (Date)val; // val = new Date(temp.getTime() / 1000 * 1000); // } // } // }
2020.3.20
orm join
1)select oneone manyone onemany
insert update oneone,manyone支持从bean取出id插入更新,onemany不支持也不需要
insert update delete不支持联合操作
2)2020.3.23 oracle.sql.BLOB
Class oracleBlob = getOracleJdbcClassLoader().loadClass(oracle.sql.BLOB.class.getName()); if(val.getClass() == oracleBlob) { if(fcl == byte [].class) { InputStream is = null; byte[] b = null; Method method = oracleBlob.getMethod("getBinaryStream"); Method methodLength = oracleBlob.getMethod("length"); try { is = (InputStream)method.invoke(val); if(is == null) break; b = new byte[((Long) methodLength.invoke(val)).intValue()]; is.read(b); } catch (Exception e) { throw new DBException("fail to read oracle blob", e); } finally { try { is.close(); is = null; } catch (IOException e) { ; } } val = b; } }
3)2020.3.24-25 OneToMany ManyToOne循环依赖,同样适用于OneToOne之间,参照spring ioc循环依赖的解决方案:spring 循环依赖,构造函数注入与setter注入(属性注入)有什么优劣,三种注入方式又有什么优劣
4)这种循环依赖的bean作为rest返回值,会导致json序列化失败,只能在debug模式内存调试
2020.3.27
2020.4.7
原理:事务的原理 学习笔记
private static final ThreadLocal<AbstractJdbcUtil> utilThreadLocal = new ThreadLocal<>(); public void startTransaction() { AbstractJdbcUtil util = null; try { util = getJdbcPool().borrowObject(); utilThreadLocal.set(util); 【重要,即使后面两句报错,也能在finally中的endTransaction将连接还给连接池,不造成泄漏】 Connection connection = util.getConnection(); connection.setAutoCommit(false); } catch (Exception e) { throw new DBException("start transaction error"); } } public void commit() { try { utilThreadLocal.get().getConnection().commit(); } catch (Exception e) { throw new DBException("transaction commit error"); } } public void rollback() { try { utilThreadLocal.get().getConnection().rollback(); } catch (Exception e) { throw new DBException("transaction rollback error"); } } public void endTransaction() { if(utilThreadLocal.get() != null) { getJdbcPool().returnObject(utilThreadLocal.get()); utilThreadLocal.remove(); } }
try{ scefOrmSession.startTransaction(); // some insert, update or delete here scefOrmSession.commit(); } catch (Exception e) { scefOrmSession.rollback(); } finally { scefOrmSession.endTransaction(); }
有几个key:
1)调试
set tx_isolation = 'read-uncommitted';
select @@tx_isolation; (相关联的文章:mysql 隔离级别 幻读 测试)
隔离级别的设定与连接有关,与是否开启事务 autocommit false无关,autocommit true,也有事务隔离级别,只不过是对于single sql而言;默认REPEATABLE-READ
2)开启事务
con.setAutocommit(false);
本次,【为什么select不参与】select的连接不参与threadlocal,用单独的connection,因为代码改动量大,而且没必要——即使不开启事务,仅设置隔离级别,也能达到事务隔离级别的效果——因为隔离级别仅与连接connection有关,与是否开启事务 autocommit false无关
比如
function {
con1.setAutocommit(false);
con2.setisolation(read-uncommited);(jdbc有这个函数)
con1.insert();
con2.select(); 可以得到未被提交的insert数据
con1.commit();
}
【反转】几天后,select也加入事务,因为此前忽视了:
-同一个函数(或一组函数组成的请求)启用事务,本事务insert/update/delete的已提交或未提交的结果应该对本事务所有select无条件可见,其他事务已提交未提交的结果对本事务的可见效应视本事务con的隔离级别而定,因此他们最好事务内共用一个connection;如果单独如上设置隔离级别,确实可能事务内其他连接的select也可以达到效果,但是你得每个都这样设置ru,其他selec又要rc,影响代码封装性;而且这个con2(ru)读取了同函数的con1未提及的数据同时,也连带读取了其他事务的未提交数据,影响了隔离的灵活性
-事务还有个功能被忽略了,即for update;先select for update,获取锁,该锁只有本事务(连接)的sql可以操作数据,假如后面是个update,用了其他连接,则阻塞,自己阻塞自己,死锁;故要将select也纳入threadlocal《connection》考虑范围,只读事务与普通读 一种mysql jvm死锁
-上面的伪代码,事务内多次其它连接的select无法应对事务内rr级不可重复读要求,因为这些select不在connection内,也就不在事务内,前后2次select mysql的事务号不同,看到的数据会不同,因此select必须启用common connection,只读事务与普通读 ,共用connection是同一个事务(同时享受事务除回滚外的其它功能,如锁,rr)的充分非必要条件
3)我们用threadlocal,对于tomcat nio是否可用?
nio,如果在threadLocal中放channel相关的东西,比如response,肯定是不行的,一个线程服务多个channel,因此才有netty channel.attr,与channel绑定的threadLocal,netty(六)WebSocket实践
但我们在里面放channel无关的东西,请看4)
*****************
那么bio呢?比如,放用户信息,只有长连接可以,比如用户发起连接-服务端分配一个线程-发起http登陆请求-同一个连接发起http数据请求-服务端同一个线程校验threadlocal,这个过程是可以的
但如果是短链接,用户发起连接-服务端分配一个线程-发起http登陆请求-关闭连接-另一个连接发起http数据请求-服务端另一个线程校验threadlocal,找不到,因为线程变了
nio就更是了,用户1发起连接-服务端分配一个线程-用户1发起http登陆请求-用户2发起连接-服务端分配同一个线程-用户2发起http数据请求-服务端在该线程下threadlocal找到了用户1的信息,完了
所以登陆信息一般放在httpheader里面,做成无状态的,session就是这样做的
*****************
4)一个线程会不会运行到一半去运行其他代码?这也就是我们在tomcat nio环境下使用threadlocal的顾虑,因为一个线程不停的被调度去执行许多channel的工作
代码1 代码2
set autocommit false
sth
commit
commit(报错)
为什么会有这个疑问呢?
因为nio的同一个线程就被调度去处理多个channel,一直给我一种感觉,线程一个channel read到一半会被调度去处理另一个channel,实际上是不会的
应该是这种模式,select - 发现有可读 -threadpool.submit( new Runnable (channel.channelhandler.read)),当然一个channel自始至终绑定一个线程,但始终是一个一个read挨着执行的,参考从实践模拟角度再议bio nio【重点】得出此结论
所以我们始终能够在一个线程内保持 set autocommit false ---- commit/rollback的连续执行,换言之,多个这个过程的代码在同一个线程中执行周期不重合;比如这样的场景,一个http请求,拦截器里获取userid,放入threadlocal,service层get threadlocal,这个过程是连续的,中间,即使是nio,该线程不会被调度到其他连接去,除非代码非要这样写,channel1.read-channel1.controller.set-channel2.read-channel2.controller.set-channel1.service.get error,一般我们用netty不会也没机会这样写
有点像spring的session
5)在orm join第3)点中,缓存用的是成员变量ConcurrentHashMap,并发时多线程之间清缓存会干扰,在本次第3)4)点的思考基础上,改用threadLocal<HashMap>
private static final ThreadLocal<Map<String, Object>> mapCacheThreadLocal = new ThreadLocal<>(); public void clearCache() { if(mapCacheThreadLocal.get() != null) { mapCacheThreadLocal.get().clear(); mapCacheThreadLocal.remove(); } } if(mapCacheThreadLocal.get() == null) mapCacheThreadLocal.set(new HashMap<>()); Object cache = mapCacheThreadLocal.get().putIfAbsent(selectSql.toString(), obj);
threadLocal对象本身不需要考虑线程安全,注意remove
threadLocal对应的value,如果是common的对象,需要考虑线程安全,比如,set了一个公共对象,每个线程虽然借用threadlocal,但搞了半天还是操作同一个对象
6)经过oracle事务实践可行,oracle也是用autocommit来控制事务
2020.4.13 时区
时区 1-8
2020.4.22 非spring环境的junit
架设testcase体系,对数据源在运行环境以及testcase进行切面并不同处理,对业务层无感
2020.4.23 日志统一
类加载器隔离朴实案例(二)logback 9~11
参考:从源码来理解slf4j的绑定,以及logback对配置文件的加载
2020.4.24 oracle回写主键
jdbcPool.returnObject(jdbcUtil); } Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); SCEF_DB_FIELD_MAIN_ID main_id = field.getAnnotation(SCEF_DB_FIELD_MAIN_ID.class); if (main_id != null) { try { Class fcl = field.getType(); Object objId = null; String strId = String.valueOf(id); if (fcl == Integer.class) objId = Integer.parseInt(strId); else if (fcl == Long.class) objId = Long.parseLong(strId); else if (fcl == BigInteger.class) objId = new BigInteger(strId); else if (fcl == Short.class) objId = Short.parseShort(strId); else if (fcl == String.class) objId = strId; else throw new DBException("unknown type of main field to set sequence into."); field.set(obj, objId); } catch (DBException e) { throw e; } catch (Exception e) { throw new DBException("Try to set mainKey value to obj error."); } } } return String.valueOf(id);
2020.4.28
在2020.4.7 原理:事务的原理 学习笔记 中,追加readOnly与隔离级别设定
public void startTransaction() {
startTransaction(false, -1);
}
public void startTransaction(boolean readOnly, int isolation) {
AbstractJdbcUtil util = null;
try {
util = getJdbcPool().borrowObject();
utilThreadLocal.set(util);
Connection connection = util.getConnection();
connection.setAutoCommit(false);
if(readOnly)
connection.setReadOnly(true);
if(isolation != -1)
connection.setTransactionIsolation(isolation);
} catch (Exception e) {
throw new DBException("start transaction error");
}
}
只读事务与普通读 中有对只读事务不能执行insert的实践
2020.4.28 事务代理切面
在2020.4.7 原理:事务的原理 学习笔记 中,所有service 的db操作都得 try commit catch rollback finally close,代码繁琐,模仿spring做一个aop
同时使用noRollBackFor
包扫描:Java遍历包中所有类方法注解
类增强:jdk动态代理源码底层(jdk生成字节码及5种字节码生产方式比较)
注入:结合自定义注解的 spring 动态注入 or guice
事务模型:本文2020.4.7
import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SCEF_DB_TRANSACTIONAL { Class<? extends Throwable>[] noRollbackFor() default {}; boolean readOnly() default false; int isolation() default -1; }
*参考:org.springframework.transaction.annotation.Transactional
package com.example.demo.testcase.orm.transactionaop; import com.example.demo.testcase.DBException; import com.example.demo.testcase.orm.ScefOrmSession; import com.example.demo.util.SpringContextUtil; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * Created by joyce on 2020/4/28. */ public class TransactionProxyFactory implements InvocationHandler { private Object target; public TransactionProxyFactory(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { SCEF_DB_TRANSACTIONAL scef_db_transactional = method.getAnnotation(SCEF_DB_TRANSACTIONAL.class); if(scef_db_transactional != null) { ScefOrmSession scefOrmSession = (ScefOrmSession)SpringContextUtil.getBean(ScefOrmSession.class); Boolean readOnly = scef_db_transactional.readOnly();
int isolation = scef_db_transactional.isolation(); try { scefOrmSession.startTransaction(readOnly, isolation); Object returnValue = method.invoke(target, args); scefOrmSession.commit(); return returnValue; } catch (InvocationTargetException ie) { Throwable throwable = ie.getTargetException(); Class c1 = throwable.getClass(); Class [] c2 = scef_db_transactional.noRollbackFor(); int sum = 0; for(Class c : c2) { if(c.equals(c1)) sum ++ ; } if(sum == 0) scefOrmSession.rollback(); else scefOrmSession.commit(); throw new DBException(throwable); } catch (Exception e) { throw new DBException(e); } finally { scefOrmSession.endTransaction(); } } else { Object returnValue = method.invoke(target, args); return returnValue; } } //给目标对象生成代理对象 public Object getProxyInstance(){ return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } }
5.14补充:
// throw new DBException(throwable); // 抛原始异常,原汁原味 throw throwable; } catch (Exception e) { // throw new DBException(e); throw e; } finally {
6.26:封装异常处理之坑
Orm2Service orm2Service = new Orm2ServiceImpl();
Orm2Service proxy = (Orm2Service) new TransactionProxyFactory(orm2Service).getProxyInstance();
proxy.xxxx();
(注入proxy)
包扫描:Java遍历包中所有类方法注解 由于使用guice注入,不使用spring批量注入,故也不需要这个功能
注入:结合自定义注解的 spring 动态注入 or guice ,使用guice单个注入,不需要代码动态批量注入
类增强:jdk动态代理源码底层(jdk生成字节码及5种字节码生产方式比较) done,3个坑:
1 jdk动态代理,必须在interface上加自定义注解,否则被增强类没有自定义注解,为什么?可看 当自定义注解遇到spring和类增强
2 jdk动态代理与ioc 当动态代理遇到ioc
根本原因:动态代理和cglib,会丢掉被代理类成员变量和方法上的注解
1)spring注入时增强: 使用@Bean(spring 循环依赖,构造函数注入与setter注入(属性注入)有什么优劣,三种注入方式又有什么优劣)
/** * https://www.cnblogs.com/silyvin/p/11900981.html * https://www.cnblogs.com/silyvin/p/12803333.html * @return */ @Bean Orm2Service orm2Service() { Orm2Service orm2Service = new Orm2ServiceImpl(); Orm2Service proxy = (Orm2Service) new TransactionProxyFactory(orm2Service).getProxyInstance(); return proxy; }
2)Controller依赖被代理类
@Controller @RequestMapping("/orm2") public class Orm2Controller { @Autowired private Orm2Service orm2Service;
3)被代理类依赖其它bean
public class Orm2ServiceImpl implements Orm2Service { @Autowired private Orm2Controller orm2Controller;【重点,看此是否装配 非null】
1)guice注入时增强:
bind(Service.class).toProvider(new TransactionProxyProvider<Service>(new ServiceImpl())).in(Singleton.class); //////////////////////////////////// private static final class TransactionProxyProvider<T> implements Provider<T> { private Object target; public TransactionProxyProvider(Object target) { this.target = target; } @Override public T get() { try { // Class cl = target.getClass(); // Field [] fields = cl.getDeclaredFields(); // for(Field field : fields) { // field.setAccessible(true); // if(field.isAnnotationPresent(Inject.class)) // throw new RuntimeException("proxy class do not allow com.google.inject annotation - " + cl.getName()); // } return (T)new TransactionProxyFactory(target).getProxyInstance(); } catch (Exception e) { loggerCommon.error(e.getMessage(), e); } return null; } }
2)其它环境类依赖被代理类
@Inject private Service service;
3)被代理类依赖其它bean
@Inject private Dao dao; @Inject private ScefOrmSession scefOrmSession
3 最终我这样的处理仍然不能实现事务的传播,意味着调用链上仅允许一个方法被增强,比如service func1被增强,dao func2不能了
以2020.4.7的代码,有以下可能
3.1 service事务,dao不事务
service | dao |
borrow con1 threadlocal.set |
|
do sql | |
threadlocal.get | |
do sql | |
threadlocal.remove return con1 |
可以运行,事务传播了,事务也可回滚
service层调用dao前con:1164562078
dao层con:1164562078
service层调用dao后con:1164562078
3.2 service事务,dao事务
service | dao |
borrow con1 threadlocal.set |
|
do sql | |
borrow con2 dao与servcie割裂了con,无法享受 2020.4.7 2)中的3点内容,但不算error,spring也有另起事务的操作 threadlocal.set 覆盖了con1 |
|
do sql | |
threadlocal.remove return con2 |
|
do sql threadlocal.get == null borrow con3 error1,变成非事务了,单独con(autocommit true),error2失去事务回滚 return con3 |
|
error3,return con1失败,因为没了,连接泄漏,而且还是一个autocommit为false且没有commit的连接 |
3个error
service层调用dao前con:330248030
driver:com.mysql.jdbc.Driver@5c1727d5:com.example.demo.testcase.FakeJdbcDriverClassLoader@7997b197
dao层con:1042812021
service层调用dao后con:0
4 注意,经实践,被增强的类,如果自己调用自己,不会又被代理,不会出现第3点的问题
因为this.xxx调用的的service未被代理也未被注入的对象,有点像spring自己调用自己的aop问题:spring aop 内部方法与aspectJ
2020.5.5 一次事故,关闭事务归还连接忘记autocommit归位导致连接没有自动提交
代理service | 之后其他非代理(无需事务)操作 |
borrow con1 set autocommit false or set readOnly true threadlocal.set |
|
threadlocal.remove return con1 |
|
退出代理 | |
borrow con1 延续了autocommit false or readOnly true |
|
con1.update db始终没有显示更新,因为没有commit return con1 |
|
(此处可以是其他代理service) borrow con1 set autocommit false or set readOnly true threadlocal.set do sth sql |
|
con1.commit(本例是刷新浏览器发出一次新的请求)后db终于显示更新 threadlocal.remove return con1 |
本质是由于开启事务时,对con有个性设置,关闭事务时没有归位,而又是连接池所致
只是太巧了,3次borrow都是同一个con1,问题的出现具有随机性
改正:
private void endTransactionReal() {
if(utilThreadLocal.get() != null) {
// 归位
try {
utilThreadLocal.get().getConnection().setAutoCommit(true);
utilThreadLocal.get().getConnection().setReadOnly(false);
} catch (SQLException e) {
e.printStackTrace();
}
getJdbcPool().returnObject(utilThreadLocal.get());
utilThreadLocal.remove();
}
}
2020.5.5 事务的传播
为了解决2020.4.28 3.2的3个问题
采用引用计数算法
保证嵌套注解的事务,或外层service@Transaction注解,内层dao无注解的代码,始终使用一个connection,经过一次startTransactionReal和一次endTransactionReal
效果相当于spring的PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
private static final ThreadLocal<Integer> hasInTransaction = new ThreadLocal<>();
public void startTransaction(boolean readOnly, int isolation) { // 起初的代码 dfsljdfls 导致嵌套问题 //startTransactionReal(readOnly, isolation); if(hasInTransaction.get() == null || hasInTransaction.get() == 0) { startTransactionReal(readOnly, isolation); hasInTransaction.set(1); } else { hasInTransaction.set(hasInTransaction.get() + 1); } } // dfsljdfls 断点,该函数在事务嵌套时只能调用一次 private void startTransactionReal(boolean readOnly, int isolation) { AbstractJdbcUtil util = null; try { util = getJdbcPool().borrowObject(); utilThreadLocal.set(util);//重要,即使后面两句报错,也能在finally中的endTransaction将连接还给连接池,不造成泄漏 Connection connection = util.getConnection(); connection.setAutoCommit(false); if(readOnly) connection.setReadOnly(true); if(isolation != -1) connection.setTransactionIsolation(isolation); } catch (Exception e) { throw new DBException("start transaction error"); } } public void endTransaction() { // 起初的代码 dfsljdfls 导致嵌套问题 //endTransactionReal(); hasInTransaction.set(hasInTransaction.get() - 1); if(hasInTransaction.get() == 0) { endTransactionReal(); hasInTransaction.remove(); } } // dfsljdfls 断点,该函数在事务嵌套时只能调用一次 private void endTransactionReal() { if(utilThreadLocal.get() != null) { // 归位 try { utilThreadLocal.get().getConnection().setAutoCommit(true); utilThreadLocal.get().getConnection().setReadOnly(false); } catch (SQLException e) { e.printStackTrace(); } getJdbcPool().returnObject(utilThreadLocal.get()); utilThreadLocal.remove(); } }
service层调用dao前con:1952079634
dao层con:1952079634
service层调用dao后con:1952079634
public int getThreadLocalUtilAddressHash() {
return System.identityHashCode(utilThreadLocal.get());
}
proxy service proxy dao |
proxy service non proxy dao |
non proxy service proxy dao |
non proxy service non proxy dao |
|
before dao | =0 | =0 | ||
dao | !=0 | =0 | ||
after dao | =0 | =0 | ||
条件 | before=dao=after>0 | before=dao=after>0 |
before=after=0 dao !=0 |
before=dao=after=0 |
然而忽略了commit和rollback
commit:
service | dao |
try | |
start 事务 | |
sql1 | |
try | |
start事务 | |
sql2 | |
commit sql1 sql2 | |
finally end事务 | |
sql3 | |
commit sql3 | |
finnally end事务 | |
看到:虽然不会有毁灭性的后果,sql1 sql2 、sql3被分开commit了,不太严谨
service | dao |
try | |
start 事务 | |
sql1 | |
try | |
start事务 | |
sql2 | |
commit sql1 sql2 | |
finally end事务 | |
sql3 | |
catch e rollback only sql3 throw e |
|
finnally end事务 | |
|
这个例子就没那么幸运了,2个sql被提前commit了,没法rollback
rollback
service | dao |
try | |
start 事务 | |
sql1 | |
try | |
start事务 | |
sql2 | |
catch | |
sql1 sql2 rollback | |
catch | throw e |
empty rollback | finally end事务 |
throw e | |
finally end事务 | |
|
看到:无什么影响,就是多了一个空的rollback,但考虑代码的工整对称,仍然处理
public void commit() { try { // 保证只有最外层的代理统一commit if(hasInTransaction.get() == 1) utilThreadLocal.get().getConnection().commit(); } catch (Exception e) { throw new DBException("transaction commit error"); } } public void rollback() { try { // 保证只有最外层的代理统一rollback if(hasInTransaction.get() == 1) utilThreadLocal.get().getConnection().rollback(); } catch (Exception e) { throw new DBException("transaction rollback error"); } }
2020.5.13 optimistic lock
hibernate中对应的乐观锁异常:
Exception in thread "main" org.hibernate.StaleObjectStateException:Row was updated or deleted by another transaction (or unsaved-value mapping wasincorrect): [Version.Student#4028818316cd6b460116cd6b50830001]
我们也实现一个
if(domain.getVersion()) { updateSql.append(" ").append(fieldName).append("=").append(fieldName).append("+1"); StringBuilder vApp = new StringBuilder(" and ").append(fieldName).append("=").append(fieldValue); version = vApp.toString(); continue; }
StringBuilder sql = new StringBuilder("update ").append(tableName).append(" set ").append(updateSql) .append(" where ").append(mainFieldName).append("=").append("'").append(mainFieldValue).append("' ").append(version);
int ret = preparedStatement.executeUpdate(); preparedStatement.close(); // jdbcUtil.executeNonQuery(sql.toString()); if(ret == 0 && !"".equals(version)) throw new ScefOptimisticLockException("table " + tableName + " version " + version); } catch (ScefOptimisticLockException e) { throw e; } catch (Exception e) { throw new DBException(e);
测试用例:
1 查询后更新,应显示version+1,顺利更新
2 构建一个没有@Version 的bean,更新id=2,无法命中,ret=0,但是理应不会触发乐观锁异常
3 查询后更新,期间手动改掉db的version,应出发乐观锁异常
@RequestMapping(value = "/lockRet1") @ResponseBody public TestMyLock lockRet1() { TestMyLock testMyLock = new TestMyLock(); testMyLock.setId(1); scefOrmSession.queryForObject(testMyLock); try { testMyLock.setNa("modified"); scefOrmSession.update(testMyLock); System.out.println("顺利更新"); } catch (ScefOptimisticLockException e) { System.out.println("乐观锁异常"); } catch (Exception e) { e.printStackTrace(); } return testMyLock; } @RequestMapping(value = "/lockRet0Success") @ResponseBody public TestMyLockNonLock lockRet0Success() { TestMyLockNonLock testMyLock = new TestMyLockNonLock(); testMyLock.setId(1); scefOrmSession.queryForObject(testMyLock); try { testMyLock.setId(2); scefOrmSession.update(testMyLock); System.out.println("顺利更新"); } catch (ScefOptimisticLockException e) { System.out.println("乐观锁异常"); } catch (Exception e) { e.printStackTrace(); } return testMyLock; } @RequestMapping(value = "/lockRet0Exception") @ResponseBody public TestMyLock lockRet0Exception() { TestMyLock testMyLock = new TestMyLock(); testMyLock.setId(1); scefOrmSession.queryForObject(testMyLock); try { // 手动version改变 Thread.sleep(8000); testMyLock.setNa("modified"); scefOrmSession.update(testMyLock); System.out.println("顺利更新"); } catch (ScefOptimisticLockException e) { System.out.println("乐观锁异常"); } catch (Exception e) { e.printStackTrace(); } return testMyLock; }
当结合while使用时,务必确保:
1)autocommit true
或
2)autocommit false,隔离级别非默认的rr
否则陷入mysql rr级别可重复读的无限循环中,jdk与mysql的cas
2020.5.19
queryforobject暴露给外部,统一进行clearCache
queryforobjectCache给循环依赖
public <T> T queryForObject(T obj) { clearCache(); return queryForObjectCache(obj); } protected <T> T queryForObjectCache(T obj) {
2020.5.21 多数据源,生产为sybase+oracle,我们本地用mysql5+mysql8模拟
我们此前的设计已经考虑多数据源,本次实践,并发现一些细节
0 测试代码:
mysql 5 query
mysql 8 query
1 mysql connector 8 url要求:
jdbc.oracle.url=jdbc:mysql://127.0.0.1:33306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
时区变成了一个必要的参数
2 mysql connector 5和8有类冲突
/** * 必须把8放在前面,否则导致加载com.mysql.jdbc.Driver时把com.mysql.cj.jdbc.Driver注册进去了 */new JarInputStream(inputStream), new JarInputStream(inputStreamMysql8),
把8放在后面——hashmap<name, byte[]> 8 的com.mysql.jdbc.driver覆盖了5的,我们看看8的定义:
package com.mysql.jdbc; import java.sql.SQLException; public class Driver extends com.mysql.cj.jdbc.Driver { public Driver() throws SQLException { } static { System.err.println("Loading class `com.mysql.jdbc.Driver\'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver\'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary."); } }
package com.mysql.cj.jdbc; import com.mysql.cj.jdbc.NonRegisteringDriver; import java.sql.DriverManager; import java.sql.SQLException; public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can\'t register driver!"); } } }
结果就是继承了com.mysql.cj.jdbc.Driver,注册时把8的注册进去了,导致第一步 mysql 5 query时,用的是driver 8,它问我们要必要的时区,而这在5中是不必要的,导致出错
mysql 5 query - 加载com.mysql.jdbc.Driver - 加载com.mysql.cj.jdbc.Driver - 注册com.mysql.cj.jdbc.Driver - Malformed database URL, failed to parse the connection string near ';characterEncoding=utf-8&useSSL=false'
3 既然mysql8的url要求有时区信息,而我们的orm有一套自己的时区处理,岂不是会错乱
经过实践
#url设置为UTC(db)会导致查询时orm TimezoneController返回错误,因为经过2次转换,相当于+16时区;insert由于是手动转换字符串不受影响
#url应设置为+8,与服务器一致,免去jdbc自行根据url的自动时区处理
#jdbc.sybase.url=jdbc:mysql://127.0.0.1:53306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=false&useTimezone=true&serverTimezone=UTC
jdbc.sybase.url=jdbc:mysql://127.0.0.1:53306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=false&useTimezone=true&serverTimezone=Asia/Shanghai
#jdbc.sybase.url=jdbc:mysql://127.0.0.1:53306/mytest?useUnicode=true&characterEncoding=utf-8&useSSL=false
故我们的url不能用db时区UPC,应该用服务器相同时区+8,免去jdbc自行根据url的自动时区处理
时区 9
4 生产环境sybase+oracle实践成功
5 目前的多数据源代码不支持各自时区,具体看时区 11
2020.5.28
修复TimeZone.getDefault()的java bug,具体看 时区 10
2020.6.2
2020.4.28 事务代理切面 + 2020.5.21 多数据源 产生问题:(第二数据源dao的调用链任意一层方法上有事务注解)
service 主数据源 |
dao 次数据源 |
有事务注解 | 无事务注解 |
开启事务 threadlocal<Con>.set |
|
dao.xxx | query |
con=threadlocal.get if(con==null) borrow one |
|
con.xxx | |
找不到表 | |
解决方案:
1)去除调用链上方法所有事务注解,有局限性
2)第2数据源定制非事务版本query,直接borrow,绕过threadlocal
/** * 第2个数据源 * Created by joyce on 2020/5/18. */ public class PropertiesOracleOrmSession extends ScefJoinOrmSession { @Override protected GenericObjectPool<AbstractJdbcUtil> getJdbcPool() { return SpringContextHolder.getBean("oracleJdbcNativePool"); } public List<Map<String, Object>> queryNonTransactional(String sql) { AbstractJdbcUtil jdbcUtil = null; GenericObjectPool<AbstractJdbcUtil> jdbcPool = null; List list = null; try { jdbcPool = getJdbcPool(); jdbcUtil = jdbcPool.borrowObject(); list = jdbcUtil.queryForList(sql); } catch (Exception e) { throw new DBException(e); } return list; } }
2020.6.10
此前使用getColumnName
oracle jconn2-5.5 select as 生效了,h2 1.2.199没生效
区别:getColumnName可能只能取到查询的数据库表的字段名称,而不是sql语句中用到的别名,而getColumnLabel取到的是sql语句中指定的名称(字段名或别名)
mysql在用两个方法获取sql语句名称时显然getColumnName不符合使用者的要求,取到的不是别名。但是oracle对于两种方法取到的值是一样的。因此一般情况下还是建议使用getColumnLabel方法
实测,mysql connector8,getColumnName返回原始fieldName,getColumnLabel返回alias
h2同样
2020.6.20
双数据源时区,具体看 时区 12
2020.6.24
第二数据源启用事务支持
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SCEF_DB_TRANSACTIONAL { Class<? extends Throwable>[] noRollbackFor() default {}; Class<? extends ScefOrmSession> sessionProvider() default PropertiesSybaseOrmSession.class; boolean readOnly() default false; int isolation() default -1; }
SCEF_DB_TRANSACTIONAL scef_db_transactional = method.getAnnotation(SCEF_DB_TRANSACTIONAL.class); if(scef_db_transactional != null) { Class cSession = scef_db_transactional.sessionProvider(); ScefOrmSession scefOrmSession = (ScefOrmSession)getBeanFromFactorySpring(cSession); if(scefOrmSession == null) throw new RuntimeException("no session found - " + cSession.getName()); // ScefOrmSession scefOrmSession = getBeanFromFactorySpring(ScefOrmSession.class); Boolean readOnly = scef_db_transactional.readOnly(); int isolation = scef_db_transactional.isolation(); try {
该法,仅支持以第二数据源开启事务,调用链上仅一个事务管理器,不同于mybatis Guice 事务源码解析支持调用链多数据源多事务管理器
2020.6.24
多数据源session继承关系梳理
2020.6.24
aop对于被代理类中没有被事务注解修饰的方法,异常修复
本质是因为,对于被代理类反射调用方法,抛出的异常均应取出原异常,在else中遗漏了,故把反射异常抛上去了
根本原因:动态代理和cglib,会丢掉被代理类成员变量和方法上的注解
2020.7.13
由于2020.3.20 orm join 中第3)点,因为:
1)图方便将入参obj0代替obj1,作为循环以来预缓存
2)而clone 时将db里查出来的obj3 clone to obj0,obj3是没有NO_FIELD字段value的,导致obj3的null覆盖了入参obj0的非null NO_FIELD字段,入参的非null NO_FILED字段被干掉了
2020.7.17
public boolean saveRecord(CEFItem newItem, CEFItem oldItem, String soeId) throws SCEFApplicationException { logger.info("saveRecord():Begin"); synchronized (CEFUtils.TABLE_LOCKER.get(newItem.getClass().getSimpleName())) { try { 。。。。。。 bind(CefItemService.class).toProvider(new TransactionProxyProvider<CefItemService>(new CefItemServiceImpl())).in(Singleton.class); private static final class TransactionProxyProvider<T> implements Provider<T> { private Object target; public TransactionProxyProvider(Object target) { this.target = target; } @Override public T get() { try { // Class cl = target.getClass(); // Field [] fields = cl.getDeclaredFields(); // for(Field field : fields) { // field.setAccessible(true); // if(field.isAnnotationPresent(Inject.class)) // throw new RuntimeException("proxy class do not allow com.google.inject annotation - " + cl.getName()); // } return (T)new TransactionProxyFactory(target).getProxyInstance(); } catch (Exception e) { loggerCommon.error(e.getMessage(), e); } return null; } }
这种事务代理+synchronzed+rc及以上隔离级别 会造成
线程1 | 线程2 |
start transaction 1 | start transaction 2 |
synchronized | synchronized |
取得锁 | |
db 查询 1 +1=2 update 2 |
|
让出锁 | 取得锁 |
db 查询 1(rc及以上隔离级别无法看到其他session未提交的数据) +1 udpate 2 |
|
提交事务 | 提交事务 |
2020.7.25 迁移方案一
要求改为mybatis,想出如下方案:
2020.7.30 $$27
mybatis处理时区时,涉及到 mapper拦截器,common包只允许select注解方法拦截,不不拦截update和insert
使用javassist运行期加载前篡改common中该类字节码 javassist 运行期改类
2020.7.31 迁移方案二 $$26
迁移Hibernate-》MyBatis,有以下关键点和风险点
多数据源框架(3个物理源,4个逻辑源,考虑到2个schema需要共同事务)
sequence
时区
二进制
testcase环境无感
联合 One Many
事务
乐观锁
观察到MyBatis的@Result注解中,除了one,many,还有一个typeHandler,灵活性极强,解决所有Hibernate-》MyBatis的迁移存在的关键技术痛点
1)typeHandler凭借一己之力,提供比hibernate、myorm强的多的高机动性orm,双向既共性又个性的拦截参数;居然顺便还能解决掉时区的问题,免去javassist暴力拦截器
2)老程序员不施力于代码堆积式编写,集中力量搭建框架、提供高效解决关键技术复杂点和风险点的方案,锁定切入点,最少而集中的代码改动,极大提升未来3周团队的生产力,极大的降低迁移风险
参考:https://www.cnblogs.com/lenve/p/10661934.html
2020.8.7 done
$$28
2020.8.1 动态代理究竟会不会遗失被代理类field和method上的注解
2020.4.28 事务代理切面中,提出:
1)事务注解要加到接口
2)注入动态代理类后,被代理类的@Autowired @Inject装配失效
做个实验:
TransactionProxyFactory
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { { //$$28 // 代理对象,只有一个成员变量,就是TransactionProxyFactory对象 Class proxyClass = proxy.getClass(); Field[] proxyFields = proxyClass.getDeclaredFields(); for (Field field : proxyFields) { field.setAccessible(true); if (field.getType().equals(Method.class)) continue; SCEF_DB_TRANSACTIONAL proxyTransaction = field.getAnnotation(SCEF_DB_TRANSACTIONAL.class); System.out.println("代理类成员是否有事务注解:" + field.getName() + (proxyTransaction != null)); } // 代理对象,所属类,有无transactional注解,跟着接口来 Method proxyMethod = proxyClass.getMethod(method.getName(), method.getParameterTypes()); if ("checkProxyServiceAndProxyDao".equals(method.getName())) { SCEF_DB_TRANSACTIONAL proxyTransaction = proxyMethod.getAnnotation(SCEF_DB_TRANSACTIONAL.class); System.out.println("代理类方法是否有事务注解:" + method.getName() + (proxyTransaction != null)); if (proxyTransaction != null) { System.out.println("代理类方法是否有事务注解:" + proxyTransaction.sessionProvider()); } } // 被代理的impl对象,可以取到方法上的注解 Method classMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes()); SCEF_DB_TRANSACTIONAL proxyTransaction = classMethod.getAnnotation(SCEF_DB_TRANSACTIONAL.class); if ("checkProxyServiceAndProxyDao".equals(method.getName())) { System.out.println("target类方法是否有事务注解:" + method.getName() + (proxyTransaction != null)); if (proxyTransaction != null) { System.out.println("target类方法是否有事务注解:" + proxyTransaction.sessionProvider()); } } }
结论是:
1 动态代理类method上注解,跟着接口的,因为它是用接口造出来的,jdk动态代理源码底层(jdk生成字节码及5种字节码生产方式比较)
2 被代理的impl对象,上面的注解是可以取到的
3 动态代理类里面,根本没有被代理类的成员变量,有无遗失成员上的注解更无从谈起了
是通过InvocationHandler.invoke中,
Object returnValue = method.invoke(target, args);
return returnValue;
通过被代理类对象target,来间接引用被代理类成员的;所以即使这些成员上有装配的注解,没用,因为注入的是代理类,这个被代理类只是维护在InvocationHandler中的一个普通对象,根本不在ioc中,自动装配也就无从谈起了
target原本要注入ioc为其@Autwired和@Inject获得自动装配,然后最终被注入的不是他而是啥成员也没有的代理类,所以才有了后来的对target成员的反哺
4 可以看到
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
中的第一个参数proxy,是代理类对象,也就是以接口名义
(xxService, xxServiceImpl, xxService proxy = (xxService)new TransactionProxyFactory(new xxServiceImpl()).getProxyInstance() )
注入到ioc中的对象
5 除了反哺,另一个可能的解决方案:
A
autowired B C D;
AProxy
A.mehod.invoke {
A.method() {
B.xxx();
}
}
A未注入,AProxy注入,但AProxy调用A方法内,有B.xxx(),B报null,
那么能不能让A也注入,让它参与对B C D的自动装配,注入AProxy,让A可以被其它类自动装配
这个方案在spring中可能会报错,因为A与AProxy类型相同
$$29
2020.8.7
ThreadLocal内存泄漏问题实践(三)非静态threadlocal
$$30
2020.8.15 aop代理日志
出入站
logback不同包不同日志
threadlocal解决当前上下文当前用户
deprecated
$$31
2020.8.17
时区,夏令时冬令时发现
时区 16
$$32
2020.9.18
时区,夏令时冬令时解决方案 insert/update
时区 17
$$33
2020.9.22
时区,夏令时冬令时解决方案 select
时区 17
$$34
2020.9.25
动态代理事务切面一次IOC
public class TransactionProxyFactory implements InvocationHandler { private Object target; private volatile boolean alreadyIOC = false;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if(!alreadyIOC) { synchronized (this) { if(!alreadyIOC) { System.out.println("首次处理 " + target.getClass() + "的ioc"); Class clProxy = target.getClass(); Field[] fields = clProxy.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); if (!field.isAnnotationPresent(getBeanInjectAnnotationSpring())) continue; String name = field.getName(); Class fieldType = field.getType(); Object obj = getBeanFromFactorySpring(fieldType); if (obj != null) { field.set(target, obj); System.out.println("处理 " + target.getClass() + "的" + field.getName()); } } alreadyIOC = true; } } }
$$35
2020.10.8
接11,结合自定义注解的事务动态代理切面(cglib)
jdk动态代理与cglib优势劣势以及jdk动态代理为什么要interface (二)
$$36
2020.10.9
静态资源基础数据加载 类加载的并发,单例模式,静态资源加载
$$37
2020.10.20
夏令时冬令时解决方案 select 修复,完美解决收官
时区 17 (6)
$$38
2020.10.29 运行期修改数据源mybatis guice 事务代理切面
#
2020.12.16 主导抽象ceftable重构
$$39
2021.1.5 cglib与asm冲突解决 当动态代理遇到ioc (三)cglib与asm jar包冲突
$$40
2020.1.5
接$$35,结合自定义注解的事务动态代理切面(cglib切子函数)当动态代理遇到ioc (四)真正的cglib
$$41
2020.1.7
$$46 2021.7.27 timecut/cache
注意,只在cglib中加入,因为jdk不能切子方法意义不大,此部分代码不进仓库
@Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { IOC(); SCEF_DB_TRANSACTIONAL scef_db_transactional = method.getAnnotation(SCEF_DB_TRANSACTIONAL.class); Class[] interfaces = target.getClass().getInterfaces(); if (scef_db_transactional == null && interfaces != null && interfaces.length > 0) { try { Method classMethod = interfaces[0].getMethod(method.getName(), method.getParameterTypes()); scef_db_transactional = classMethod.getAnnotation(SCEF_DB_TRANSACTIONAL.class); } catch (NoSuchMethodException noMethod) { ; } catch (Throwable e) { logger.error(e.getMessage(), e); } }
【【【以下有修改】】】 SCEF_DB_CACHE scef_db_cache = method.getAnnotation(SCEF_DB_CACHE.class); Object res = getCache(scef_db_cache, objects, method); long st = new Date().getTime(); try { if(res != null) return res; if (scef_db_transactional != null) { res = invokeHasTransactional(scef_db_transactional, null, objects, methodProxy, o); } else { res = invokeNoTransactional(null, objects, methodProxy, o); } } finally { long ed = new Date().getTime(); double x = ((double)(ed-st)) / 1000; printTime(method, x); } setCache(scef_db_cache, objects, method, res); return res; }
这里用了try fanally模型:finally throw return , 使用threadlocal时也应使用这种模型,确保threadlocal被remove ThreadLocal内存泄漏问题实践(二)
private void printTime(Method method, double x) { try { Injector injector = CRFGuiceContext.getInjector(); Configuration configuration = injector.getInstance(Configuration.class); if (configuration == null) { return; } Boolean print = configuration.getBoolean("SCEF.echomethodtime.enable", false); if (!print) { return; } Double cut = configuration.getDouble("SCEF.echomethodtime.cut", 0.1); if(x < cut) { return; } logger.info("scefcut {}\t{}\t{}", target.getClass().getSimpleName(), method.getName(), x); } catch (Exception e) { logger.error(e.getMessage(), e); } } private Object getCache(SCEF_DB_CACHE scef_db_cache, Object [] paras, Method method) { try { if (scef_db_cache == null || scef_db_cache.disabled()) return null; String key = getKey(scef_db_cache, paras, method); return cache.get(key); } catch (Exception e) { logger.error(e.getMessage(), e); return null; } } private void setCache(SCEF_DB_CACHE scef_db_cache, Object [] paras, Method method, Object object) { try { if (scef_db_cache == null || scef_db_cache.disabled()) return; String key = getKey(scef_db_cache, paras, method); cache.put(key, object); } catch (Exception e) { logger.error(e.getMessage(), e); } } private String getKey(SCEF_DB_CACHE scef_db_cache, Object [] paras, Method method) { final String SPLIT = ":"; String key = scef_db_cache.key(); if(StringUtils.EMPTY.equals(key)) { key = new StringBuilder(target.getClass().getSimpleName()).append(SPLIT).append(method.getName()).toString(); } StringBuilder value = new StringBuilder(key).append(SPLIT); for(Object para : paras) { if(!(para instanceof String)) continue; value.append(para).append(SPLIT); } return value.toString(); }
不支持null cache,null被认为没有获得cache,允许缓存穿透;只有String类型入参参与key的构建
import java.lang.annotation.*; @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SCEF_DB_CACHE { boolean disabled() default true; String key() default StringUtils.EMPTY; }