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;
}
浙公网安备 33010602011771号