/**
* str 变量持有的是 String 对象的内存地址。传递到函数中时,形参str2 是str 值拷贝(内存地址)。
* 由于String 类的特殊性,重新赋值就相当于给str2 赋值新的内存地址。和原有的str 没有关系。
*
* 运行结果:“字符串修改后:zhangsan”
*/
public class TestStr {
public static void main(String[] args) {
String str = "zhangsan";
changeStr(str);
System.out.println("字符串修改后:"+str);
}
/**
* @param str2 函数的形参是被调用时所传实参的副本。引用类型,对应的value 是地址副本
*/
private static void changeStr(String str2) {
// 相当于 str2 = new String("lisi");
str2 = "lisi";
}
}
值传递(pass by value):在调用函数时,将实际参数复制一份传递到函数中,这样在函数中对参数进行修改,就不会影响到原来的实际参数;
When a parameter is pass-by-value, the caller and the callee method operate on two different variables which are copies of each other. Any changes to one variable don’t modify the other.
It means that while calling a method, parameters passed to the callee method will be clones of original parameters. Any modification done in callee method will have no effect on the original parameters in caller method.
引用传递(pass by reference): 在调用函数时,将实际参数的地址直接传递到函数中。这样在函数中对参数进行的修改,就会影响到实际参数;
When a parameter is pass-by-reference, the caller and the callee operate on the same object.
It means that when a variable is pass-by-reference, the unique identifier of the object is sent to the method. Any changes to the parameter’s instance members will result in that change being made to the original value.
在 Java 中,原始类型变量( Primitive variables )存储的就是实际的值,非原始变量存储的引用变量( reference variables )(指向它们所引用的对象的地址 the addresses of the objects)。
值和引用都存储在堆栈内存中 ( Both values and references are stored in the stack memory )。
ECMAScript中所有函数的参数都是按值传递的。- 《JavaScript高级程序设计》
var name = 'zhangsan';
function setName (n) {
n = 'lisi';
console.log(n); //output: lisi
}
setName(name);
console.log(name); //output: zhangsan
#include<iostream>
using namespace std;
void change(int &i) {
i = i+2;
}
int main() {
int i = 0;
cout << "Value of i before change is :" << i << endl;
change(i);
cout << "Value of i now is :" << i << endl;
}
使用方式
$ g++ change.cpp -o change
$ ./change
[Pass-By-Value as a Parameter Passing Mechanism in Java | Baeldung](https://www.baeldung.com/java-pass-by-value-or-pass-by-reference) |
<resultMap id="detailResultMap" type="com.foo.model.FooBase">
<id column="id" property="id" jdbcType="INTEGER" />
<result column="name" property="name" jdbcType="VARCHAR" />
<!-- 通过内嵌查询,实现类似Hibernate 级联查询的功能,支持延时查询-->
<collection property="detailList" column="id" javaType="java.util.ArrayList"
ofType="com.foo.model.FooDetail"
<!-- 注意:select 支持默认补充 namespace。如果查询语句在当前 namespace, 可以省略 -->
select="com.foo.dao.FooDetailMapper.selectByBaseId" fetchType="lazy"/>
</resultMap>
关键信息:
ResultMapping#nestedQueryId
/**
* mybatis 解析resultMap childNodes
*
* @see org.apache.ibatis.builder.xml.XMLMapperBuilder#buildResultMappingFromContext
*/
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
// 关键属性select, 对应 ResultMapping#nestedQueryId
// Tips: 后续调用applyCurrentNamespace() 检测和补充查询方法的namespace,检测符号“.”
String nestedSelect = context.getStringAttribute("select");
// 注意,内嵌query 和内嵌resultMap 的两种使用方法,彼此互斥。
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping> emptyList()));
// 懒加载属性,默认 configuration.isLazyLoadingEnabled() = false。
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
}
嵌套查询的过程:先执行主查询,在返回结果ResultSet 匹配到ResultMap过程中,发现有nestedQuery,就会再次发起查询(或者lazy模式下,缓存查询的对象、方法和参数,必要的时候发起查询)
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
final String nestedQueryId = propertyMapping.getNestedQueryId();
final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
Object value = null;
// 首先判断查询的参数不能为空
if (nestedQueryParameterObject != null) {
final Class<?> targetType = propertyMapping.getJavaType();
// 扩展 DeferredLoad 缓存机制,本次不做解读
if (executor.isCached(nestedQuery, key)) {
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
value = DEFERED;
} else {
// 延时加载委托类ResultLoader,保存必要的查询信息,用于嵌套查询使用
// 必要的信息对应的是, BaseExecutor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
if (propertyMapping.isLazy()) {
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERED;
} else {
// 正常情况下,非延时,直接发起查询。BaseExecutor.query(...)
value = resultLoader.loadResult();
}
}
}
return value;
}
mybatis 延时查询原理:在将结果集转化为值对象时,会对映射到的属性进行实例化操作。借助动态代理,会将lazyLoader 作为属性添加到值对象中。真正对该值对象读取操作时,代理类拦截并发起真正的数据库查询。
注意:仅限嵌套查询才有延时查询的概念。
/**
* @see org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createResultObject
*/
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
// 值对象实例化,constructor.newInstance()
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// 检测到存在嵌套查询,并且需要延时加载,对原对象进行代理操作。
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
// proxy.callback = lazyLoader, 拦截getSet方法,在realObject 读写之前,结束延时的状态。
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty();
return resultObject;
}
/**
* 借助查询委托类resultLoader,查询得到的结果,赋值到realObject,达到延时替换的目的。
* @see org.apache.ibatis.executor.loader.ResultLoaderMap.LoadPair#load
*/
public void load(final Object userObject) throws SQLException {
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
Namespaces are now required and have a purpose beyond simply isolating statements with longer, fully-qualified names.
为了隔离和区分 statements, result maps, caches,Mybatis 在xml 解析过程中,对针对一些id 进行默认的 namespace 补充。smart !!!
/**
* 在需要进行唯一识别的场景,会进行默认namespace 补充
* @see org.apache.ibatis.builder.MapperBuilderAssistant#applyCurrentNamespace
*/
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
if (isReference) {
// is it qualified with any namespace yet?
if (base.contains(".")) {
return base;
}
} else {
// is it qualified with this namespace yet?
if (base.startsWith(currentNamespace + ".")) {
return base;
}
if (base.contains(".")) {
throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
}
}
return currentNamespace + "." + base;
}
来源:org.springframework.core.convert.ConversionService
private static volatile DefaultConversionService sharedInstance;
/**
* Return a shared default ConversionService instance, lazily building it once needed.
*
* @return the shared {@code ConversionService} instance (never {@code null})
* @since 4.3.5
*/
public static ConversionService getSharedInstance() {
if (sharedInstance == null) {
synchronized (DefaultConversionService.class) {
if (sharedInstance == null) {
sharedInstance = new DefaultConversionService();
}
}
}
return sharedInstance;
}
来源:com.atomikos.thread.TaskManager
public enum TaskManager {
SINGLETON;
private ThreadPoolExecutor executor;
private void init() {
SynchronousQueue<Runnable> synchronousQueue = new SynchronousQueue<Runnable>();
executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, new Long(60L),
TimeUnit.SECONDS, synchronousQueue, new AtomikosThreadFactory());
}
}
最近在看 H2 数据库的实现原理,竟然支持 XA 协议。正好顺路对分布式事务温习下,感觉无用的知识又增加了。
通过学习分布式事务的解决方案,积累分布式设计和开发的经验,举一反三。
国内使用较多的 mysql ,在 XA 协议的支持上一般,同时两阶段提交的方案在性能上和实现上都有一定的困难,因此可以称之为“无用的知识”。另一方面,从 atomikos 商业版的定价上看,真正的大公司才用得起,一般公司用不起也没必要这么用。
行业内比较实用的方案:事务消息+最终一致性。
XA 协议是由 X/Open 组织提出的分布式事务处理规范,主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。
分布式事务的两阶段提交是把整个事务提交分为 prepare 和 commit
两个阶段。
第一阶段,事务协调者向事务参与者发送 prepare 请求,事务参与者收到请求后,如果可以提交事务,回复 yes,否则回复 no。
第二阶段,如果所有事务参与者都回复了 yes,事务协调者向所有事务参与者发送 commit 请求,否则发送 rollback 请求。
JTA实现分布式事务的两个阶段。
第一阶段首执行 XA 开启、执行 sql、XA 结束三个步骤,之后直接执行 XA prepare。
第二阶段执行 XA commit/rollback。
使用XA 模式前提是:数据库对 XA 协议的支持。atomikos、seata、bitronix是作为全局的调度者
的角色参与到方案中。
seata 是阿里推出的一款开源分布式事务解决方案,目前有 AT、TCC、SAGA、XA 四种模式。
Manage your distributed transactions and protect your mission critical data.
轻量、开源、免费、收费版本(ExtremeTransactions)提供技术支持(14500 €/year)。
轻量是指不依赖应用服务器(Websphere、Jboss)。
insight 思路是顺着 javax.transaction.xa.XAResource 定义的接口来看协调者做了哪些工作。主要侧重XA Api操作步骤,涉及到XA 协议的状态管理、嵌套事务等实现,不在此处关注。
参考:com.atomikos.datasource.xa.XAResourceTransaction#resume
关键类:
A participant for (distributed) two-phase commit of composite transactions. Implementations can be added as a 2PC participant in the icatch kernel
拦截 JdbcConnection 的操作,根据操作选择纳入协调器管理、开启XA事务或者阻止在XA事务中直接commit等。
注意:不同于普通的 Jdbc 事务,JTA的事务开启并不是在 AbstractPlatformTransactionManager#doBegin 方法中调用。设想多个数据源,到底是应该开启哪个的事务,只有真正发生 Jdbc 操作时,才会判断并开启XA事务,同时把该数据源纳入到协调器中。
根据执行情况分为 prepare + commit、 prepare + rollback、 rollback。
参考: com.atomikos.icatch.jta.UserTransactionImp#commit
关键类:
Represents a nested part of a global composite transaction. 委托协调器发起事务的提交。
protected void terminate(boolean commit) throws Exception {
synchronized (fsm_) {
if (commit) {
if (participants_.size() <= 1) {
// 如果只涉及到单数据库的操作,那么简化为普通的jdbc操作,直接commit
commit(true);
} else {
// 对多个participant 发起prepare操作,第一阶段协调
int prepareResult = prepare();
// make sure to only do commit if NOT read only
if (prepareResult != Participant.READ_ONLY)
commit(false);
}
} else {
// 或者直接回滚,应用逻辑出错,不需要协调
rollback();
}
}
}
protected int prepare() throws Exception {
Vector<Participant> participants = getCoordinator().getParticipants();
CoordinatorStateHandler nextStateHandler = null;
// 省略其他判断...
try {
getCoordinator().setState(TxState.PREPARING);
result = new PrepareResult(participants.size());
Enumeration<Participant> enumm = participants.elements();
// 分别对 Participant 发起prepare(), 默认是同步提交,可以启用线程池异步发起prepare()
while (enumm.hasMoreElements()) {
Participant p = (Participant) enumm.nextElement();
PrepareMessage pm = new PrepareMessage(p, result);
// 省略其他判断...
getPropagator().submitPropagationMessage(pm);
} // while
result.waitForReplies();
boolean voteOK = result.allYes();
if (!voteOK) {
try {
rollbackWithAfterCompletionNotification(new RollbackCallback() {
public void doRollback() {
rollbackFromWithinCallback(true, false);
}
});
}
}
} catch (RuntimeException runerr) {
throw new SysException("Error in prepare: " + runerr.getMessage(), runerr);
}
return ret;
}
清单目的:在使用到mybatis 冷门特性时候,可以快速参考查阅
方法入参封装,根据方法参数的个数,有不同的处理方法。
参数个数 | 处理方法 | 备注 |
---|---|---|
0 | return null | |
1 (非特殊类型 or @Param) | return 原始入参对象 | 需要特别注意,如果在动态SQL中使用,则不能为String、Integer等简单类型。 |
>=2 | return HashMap | name即参数的name,通过@Param指定的name或者JDK反射得到的name。 |
方法中使用到的方法参数,是在mybatis 启动过程中加载对应method并解析得到的。需要主要到@Param
和JDK8 Parameter
对mybatis 解析的影响。
如果使用JDK8,对于接口的参数,是可以获取到参数编写的命名,否则得到参数name类似”arg0”
针对第1步中的封装结果,如果是单个参数的情况,则需要针对数组和集合进行单独封装。
参数类型 | 处理方法 | 备注 |
---|---|---|
Collection | return HashMap(“collection”, object) | 固定name=collection |
List | return HashMap(“collection”, object, “list”, object) | 固定name=list |
Array | return HashMap(“array”, object) | 固定name=array |
此时,需要根据入参把动态sql转化为StaticSqlSource,再将StaticSqlSource 转换为sql String。
上述参数封装的结果,就是把各种入参对象整合到单个对象里(DynamicContext
),方便统一读取(MetaObject
)。
后续的处理中,直接使用DynamicContext 获取参数,不再使用原始的参数。
foreach 表达式的解析,是把collection拆散的过程,可以理解为把集合类型的参数拆成n个参数。
# foreach 表达式静态转换示例
# DynamicContext 中会新增(__frch_item_0:id1, __frch_item_1:id2)的参数
# org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply
select xxx from candy WHERE id IN ( #{__frch_item_0}, #{__frch_item_1} );
StaticSqlSource 转换为BoundSql,也就是转为真正的SQL的过程。SQL中占位符只存在?符号。
上述的foreach 最终的结果为
# An actual SQL String
# 占位符对应的参数,存储到 org.apache.ibatis.mapping.BoundSql#parameterMappings
select xxx from candy WHERE id IN ( ?, ? );
以BoundSql 为基础,调用java.sql.PreparedStatement#setString,完成JDBC调用。
从方法中获取参数的命名,是个麻烦的事情,特别是从接口中。JDK8 以后支持这种操作,不过需要编译参数启用。
如果使用maven,那么可以加上如下的配置,启用这个特性。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>