最近在项目开发中,出现了一些见鬼的问题。保存或删除某些操作,bug的出现是偶发性出现,bug 还讲 概率事件啊,伤不起啊。在刚开始开发中,也偶尔出现,但是当时还不以为然。可是,项目上线后,用户也反映这个问题。 唉,这不是严重影响用户体验么。看来,这已经成了一个必然的bug, 不改不行了。
看来, Java中 偶发性 bug,多半跟 多线程 有关系。
先贴代码
spring-context.xml spring 的配置文件
<bean id="yyzy" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>yyzy_bwcs</value>
<!-- <value>java:comp/env/jndi/yyzy_db2</value> -->
</property>
</bean>
<bean id="metadata" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>meta_db2</value>
<!-- <value>java:comp/env/jndi/meta_db2</value> -->
</property>
</bean>
<!-- 交换平台 -->
<bean id="jhpt" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>jhpt_ds</value>
<!-- <value>java:comp/env/jndi/jhpt_ds</value> -->
</property>
</bean>
<bean id="dynamicDataSource" class="com.conserv.tsas.common.bean.DynamicDataSource">
<property name="targetDataSources">
<map key-type="java.lang.String">
<entry value-ref="yyzy" key="yyzy"></entry>
<entry value-ref="metadata" key="metadata"></entry>
<entry value-ref="jhpt" key="jhpt"></entry>
</map>
</property>
<property name="defaultTargetDataSource" ref="yyzy">
</property>
</bean>
<!-- sessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
<property name="configLocation" value="classpath:mybatis-content.xml"></property>
<property name="mapperLocations" value="classpath:com/conserv/tsas/**/mapper/*.xml"/>
<property name="typeAliasesPackage" value="com.conserv.tsas.test.entity,com.conserv.tsas.front.entity"/>
</bean>
com.conserv.tsas.common.bean.DynamicDataSource.java
package com.conserv.tsas.common.bean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getCustomerType();
}
}
DatabaseContextHolder.java
package com.conserv.tsas.common.bean;
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
public static final String YYZY = "yyzy";
public static final String METADATA = "metadata";
public static final String JHPT = "jhpt";
public enum DS {
YYZY, METADATA,JHPT
};
public static void setCustomerType(String customerType) {
contextHolder.set(customerType);
}
public static String getCustomerType() {
return contextHolder.get();
}
public static void clearCustomerType() {
contextHolder.remove();
}
}
上面的代码就是 在web 程序执行的时候,通过 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();这个变量来获取 数据源名称。
那么有如何给 private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>(); 中设置值呢?
采用面向切面的方式 进行 java 的自定义注释方式 给 contextHolder 中存储值。
首先实现Java的自定义注释。
DataSource.java
package com.conserv.tsas.common.bean;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.conserv.tsas.common.bean.DatabaseContextHolder.DS;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DataSource {
public DS value()default DS.YYZY;
}
注释如何实用呢?
在 DAO 层中,在需要切换数据源的方法(method)前面加上 @DataSource(value=DS.YYZY) , DS.YYZY就是我要的数据源
例子:
@DataSource(value=DS.JHPT)
@Override
public void deleteNotWriteOff_DDS(Map params) {
externalProcessMapper.deleteNotWriteOff_DDS(params);
}
DynamicDataSourceAspect.java
package com.conserv.tsas.common.bean;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import com.conserv.tsas.common.bean.DatabaseContextHolder.DS;
@Component
@Aspect
@Order(1)
public class DynamicDataSourceAspect {
@Pointcut("execution(* com.conserv.tsas.*.dao..*.*(..))")
public void serviceExecution(){}
@Before("serviceExecution()")
public void setDynamicDataSource(JoinPoint jp) throws SecurityException, NoSuchMethodException {
String methodName = jp.getSignature().getName();
Method [] ms = jp.getTarget().getClass().getMethods();
Method method = null;
for(int i=0;i<ms.length;i++){
if(ms[i].getName().equals(methodName)){
method = ms[i];
break;
}
}
DataSource ds = method.getAnnotation(DataSource.class);
if(ds != null){
if(ds.value() == DS.YYZY){
DatabaseContextHolder.setCustomerType(DatabaseContextHolder.YYZY);
}else if(ds.value() == DS.METADATA){
DatabaseContextHolder.setCustomerType(DatabaseContextHolder.METADATA);
} else if (ds.value() == DS.JHPT) {
DatabaseContextHolder.setCustomerType(DatabaseContextHolder.JHPT);
}
}else{
DatabaseContextHolder.setCustomerType(DatabaseContextHolder.YYZY);
}
}
}
本文重点, 缺陷。
这种方式实现多数据源的切换,是通过 ThreadLocal 方式来共享 数据。 如果在service 层中使用 事务 @Transactional
例:
@Override
@Transactional
public void deleteT_YYZY_WJG_YL_ZCB() {
externalProcessDao.deleteT_YYZY_WJG_YL_CB();
externalProcessDao.deleteT_YYZY_WJG_YL_ZB();
}
此时,数据源容易混淆, 即本该执行 A 数据源,却执行了 C 数据源。
这是因为 ThreadLocal 类的内部
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
无论是set() 还是 get(),都是通过Thread.currentThread() 来作为 键值 进行存储。
而在 Java web 开发中, 在 service 层使用了事务,对 DAO 的 数据源拦截造成错乱,也即是ThreadLocal 中获取当前线程,出现错乱。