问题

前面说到,我们可以用 JSONObject 替代习惯使用的 JavaBean ,而之所以能用 JSONObject 主要就是因为它实现了 Map<String,Object>

实际使用我们就会发现,MyBatis 使用JSONObject 封装返回结果的时候很“智能”,数据库里字段是 varchar 类型,JSONObject 中返回值就是 String 类型,数据库字段是 int/float 类型,JSONObject 中返回值就是对应的数值类型。甚至通过 debug 发现数据库中保存了datetime 类型的数据,JSONObject 中保存的是 java.sql.timestamp 类型,而timestamp 类型继承了常见的 java.util.Date

为什么 MyBatis 可以用得这么爽呢?我们实现可完全没声明需要此字段的 javaType 呢。而且用得爽了,类型转换会不会导致程序性能大打折扣呢?

今天我们就从深入源码,探究一番 MyBatis 到底是怎样做到对未明确声明的字段处理返回类型的。

TypeHandler

MyBatis 类型转换的核心就是这个接口,定义的方法可以看做就两种 setParameter 和 getResult ,很好理解,我们传参和接收 sql 结果时就调用这个。

1
2
3
4
5
6
7
8
9
10
11
public interface TypeHandler<T> {

void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

T getResult(ResultSet rs, String columnName) throws SQLException;

T getResult(ResultSet rs, int columnIndex) throws SQLException;

T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

抽象类 BaseTypeHandler 部分实现了 TypeHandler ,主要完成了对空值的处理。 非空值的处理全部交给了子类完成。

BaseTypeHandler 子类非常多,对应了数据库的各种数据类型,实现都很简单,比如SqlTimestampTypeHandler 处理 Timestamp 类型。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Timestamp parameter, JdbcType jdbcType)
throws SQLException {
ps.setTimestamp(i, parameter);
}

@Override
public Timestamp getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getTimestamp(columnName);
}

都是直接调用了 PreparedStatement 和 ResultSet 处理相应类型字段的方法。

很明显,一旦指明了我们需要 MyBatis 给我们返回的此字段类型,MyBatis 肯定就去找到对应的 TypeHandler 实现类去处理。而我们没有指定返回类型的是怎么处理的呢?或者说,对于 Object 类型是怎么处理的呢?

ObjectTypeHandler

我们先来看看这个类,看名字就会猜可能估计未知类型全靠它了吧。提前预告下,并不是哦,getNullableResult 还算经常使用,入参赋值就没见用了,毕竟入参的 JavaType 我们通过反射还是可以找到的。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
ps.setObject(i, parameter);
}

@Override
public Object getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getObject(columnName);
}

一看这实现,居然还是调用的 JDBC 底层的对应方法。事实上 com.mysql.jdbc 在处理Object 类型是也是通过大量的 if-else 或 switch-case 来找到本数据真正的类型的。入参绑定依据 parameterObj instanceof 各种类型,返回结果类型依据 field.getSQLType 的各种类型。

UnknownTypeHandler

事实上,MyBatis 在很多我们没有指明参数类型的情况下,都是使用 UnknownTypeHandler 来解决类型转换的。UnknownTypeHandler 中的核心resolveTypeHandler 方法,就是查找对应数据的类型解析器(TypeHandler) , 再用这个合适的 typeHandler 进行解析。

resolveTypeHandler 方法的重载有3种,主要的两种就是一种处理入参的,一种处理返回结果的。

入参类型解析

1
2
3
4
5
6
7
8
9
10
11
12
13
private TypeHandler<? extends Object> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<? extends Object> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
// check if handler is null (issue #270)
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}

在入参为 null 或者实在找不到解析器的情况下,就会返回我们上面讲的 ObjectTypeHandler 。

而这里面关键的方法就是

1
typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);

typeHandlerRegistry 可以理解为全局共用的各种类型与解析器关系的注册表,后面的文章还会继续深入讲解,我们首先注意这个方法的入参,第一个参数拿到了参数的 class , 第二个参数拿到了 jdbcType 。这不就相当于javaType 和 jdbcType 都有了吗?那即使还没研究 typeHandlerRegistry 到底干了啥,但是条件给的这么充分了,注册表的任务也太轻松了吧!

等等,我们的 DAO 层给入参是 JSONObject 类型,里面 username 字段是 String 类型,money 字段是 float 类型,这些都能通过 getClass() 获取到确实没毛病。但是 jdbcType 是哪来的呢?我们现在可是在处理 PreparedStatement 呢!

通过 debug 我们发现,这里的 jdbcType 我们没有指明的情况下,确实都是 null 。说明 typeHandlerRegistry 里仅仅是通过 javaType 来寻找解析器的。所以 typeHandlerRegistry 还是有很多门道等着我们去探索哦。

返回结果类型解析

对 ResultSet 的解析有两种方式,首先查看此结果字段–比如nickname–在 field 中的序号,如果没序号,则直接返回 ObjectTypeHandler 。如果有序号,则进入下面的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private TypeHandler<?> resolveTypeHandler(ResultSetMetaData rsmd, Integer columnIndex) throws SQLException {
TypeHandler<?> handler = null;
//通过下面两个方法获取到jdbcType和javaType
JdbcType jdbcType = safeGetJdbcTypeForColumn(rsmd, columnIndex);
Class<?> javaType = safeGetClassForColumn(rsmd, columnIndex);
//后面的任务就还是交给了typeHandlerRegistry
if (javaType != null && jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
} else if (javaType != null) {
handler = typeHandlerRegistry.getTypeHandler(javaType);
} else if (jdbcType != null) {
handler = typeHandlerRegistry.getTypeHandler(jdbcType);
}
return handler;
}

其中的关键方法还是从 rsmd 中获取 jdbcType 和 javaType ,然后再通过 typeHandlerRegistry 去查找对应的 handler 。

debug 发现几乎每次从 rsmd 中获取 jdbcType 和 javaType 都获取到了,看来玄机都在safeGetJdbcTypeForColumnsafeGetClassForColumn 中了。

两个方法的关键代码分别如下

1
2
3
return JdbcType.forCode(rsmd.getColumnType(columnIndex));

return Resources.classForName(rsmd.getColumnClassName(columnIndex));

JdbcType

com.mysql.jdbc.ResultSetMetaData 实现了 java.sql.ResultSetMetaData 接口,此处我们调用了其中的

1
2
3
public int getColumnType(int column) throws SQLException {
return getField(column).getSQLType();
}

序号的作用就体现出来了,根据序号找到此 field ,再找其 SQLType ,根据 SQLType 去JdbcType 类 (enum类型) 内查找对应的 jdbcType。

JdbcType 类内维护了一个 map 类型静态变量 codeLookup ,类加载时为 codeLookup 添加了39个元素,key 值其实就是 SQLType , int 类型,value 就是本 jdbcType 。

因此根据 SQLType 在此处就直接能毫不费力地找出对应的 jdbcType。

1
2
3
4
5
static {
for (JdbcType type : JdbcType.values()) {
codeLookup.put(type.TYPE_CODE, type);
}
}

javaType

同样需要先找到 SQLType ,以及field 内的另外几个属性值,例如 isUnsigned 等一起进入 getClassNameForJavaType 方法找到对应类名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static String getClassNameForJavaType(int javaType, boolean isUnsigned, int mysqlTypeIfKnown, boolean isBinaryOrBlob, boolean isOpaqueBinary,
boolean treatYearAsDate) {
switch (javaType) {
case Types.BIT:
case Types.BOOLEAN:
return "java.lang.Boolean";
case Types.TINYINT:
if (isUnsigned) {
return "java.lang.Integer";
}
return "java.lang.Integer";
//......大量 case
default:
return "java.lang.Object";

而这里的大量的 swtich-case 终于算是解除了我们一部分的疑惑了!底层终归还是通过 swtich-case 这种最原始的操作来把 jdbcType 映射到 java 类里去的!

奇怪的现象

上面说到,如果没序号,则直接返回 ObjectTypeHandler。

序号是getColumnType(int column) 用于找到到对应的 field 的关键属性。

那么为什么有可能会没有序号呢?

通过 debug 我们发现,明明 fields 内有 8 个元素,每个字段的原始名和别名都清清楚楚,到 columnIndexLookup 里居然只剩下 6 个? 很显然,问题出在了

1
String name = rsmd.getColumnName(i);

中间有几次取出了重复的 name 。为什么会有重复的 name 呢,我们进入getColumnName 一探究竟。

1
2
3
4
5
6
7
8
9
10
11
12
13
public String getColumnName(int column) throws SQLException {
if (this.useOldAliasBehavior) {
//如果设置了使用别名的属性,就获取此field的别名。
return getField(column).getName();
}
//获取此field的name属性,而不是别名。
String name = getField(column).getNameNoAliases();
if (name != null && name.length() == 0) {
//如果连原始字段名都找不到,就还是获取别名
return getField(column).getName();
}
return name;
}

实际debug发现,我们每次都是通过 getNameNoAliases 找到 name 的。再底层的代码就不需要贴了,看到这里我们就明白了。这里的 name 其实是每个字段的数据库内的字段名,而不是我们定义的别名,所以才会出现重复的情况,比如 user 表有 id 字段,address 表同样会有 id 字段。

总结

今天我们分析完了类型转换器,发现对于未知的数据类型,有一部分是通过ObjectTypeHandler 解析,其底层的用了com.mysql.jdbc.ResultSetImpl.getObject 内的依据 Field.SQLType 的 swtich-case 。

另一部分则是通过UnknownTypeHandler 去查找合适的解析器来解析。

关于查找解析器的步骤,我们将进入下一层级TypeHandlerRegistry 来继续学习。

评论和共享

问题

上一文介绍了 JSONObject 接受 MyBatis 的结果集的简单用法,但是在处理一对多的情况时,单纯的JSONObject就不好使了。

比如要查询一个角色下的多个用户,resultMap如下定义

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.alibaba.fastjson.JSONObject">
<id column="roleId" property="roleId"/>
<result column="roleName" property="roleName"/>
<collection property="users" ofType="com.alibaba.fastjson.JSONObject">
<id column="userId" property="userId"/>
<result column="nickname" property="nickname"/>
</collection>
</resultMap>

期望查出来的users 属性对应着一个数组,

然而实际查出来只是一个对象,只有一条数据。

解决方案

只需要建一个实体类继承 JSONObject ,里面有你要的集合类型的成员变量,就足够了。

比如我建的 One2Many 类:

1
2
3
public class One2Many extends JSONObject {
private List<JSONObject> users;
}

然后xml改为

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.heeexy.example.util.model.One2Many">
<id column="roleId" property="roleId"/>
<result column="roleName" property="roleName"/>
<collection property="users" ofType="com.alibaba.fastjson.JSONObject">
<id column="userId" property="userId"/>
<result column="nickname" property="nickname"/>
</collection>
</resultMap>

是不是非常简单?

更棒的是,这个 One2Many 类是可以复用的,里面再添加其它的成员变量就 OK 了。而且 Dao 层不需要改动,外面正常的还是用 JSONObject 就可以了。

原理

MyBatis 在处理嵌套结果的时候,会判断这个属性的类型,如果是集合,就会初始化一个集合来接收这个属性,否则就只是一个普通的 Object 了。

什么,不满意这个答案?那就拿出源码来吧!

首先我们直接看到最底层判断这个属性是不是集合的这段源码:

DefaultResultSetHandler.instantiateCollectionPropertyIfAppropriate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
final String propertyName = resultMapping.getProperty();
//先去拿在metaObject已经存好的这个属性值,我们这里以users属性为例
Object propertyValue = metaObject.getValue(propertyName);
if (propertyValue == null) {
//在这里拿你的users的java类型,由于我们使用的是JSONObject,这里的类型都会返回Object,如果是One2Many,这里就会拿到List类型
Class<?> type = resultMapping.getJavaType();
if (type == null) {
type = metaObject.getSetterType(propertyName);
}
try {
//判断属性类型是不是集合,如果是结合才会初始化一个集合的值返回到下一步,否则都会返回null
if (objectFactory.isCollection(type)) {
propertyValue = objectFactory.create(type);
metaObject.setValue(propertyName, propertyValue);
return propertyValue;
}
} catch (Exception e) {
throw new ExecutorException("Error instantiating collection property for result '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
} else if (objectFactory.isCollection(propertyValue.getClass())) {
//propertyValue不为空的情况,即我们的JSONObject里已经塞入了users属性,即使是这样,MyBatis还是要求你塞入的users属性必须是集合,才返回到下一步,否则还是会返回null到下一步。
return propertyValue;
}
return null;
}

从上面这段代码我们就知道 MyBatis 确实有做这个判断,你定义的 users 属性到底是不是集合类型,

  1. 如果是并且没有初始化好的话,就帮你初始化一个集合到下一步,
  2. 如果已经初始化好了(通常这时候就是已经塞入了几个 user 对象了),就直接返回这个 users 的值到下一步
  3. 如果不是集合类型,就返回 null 到下一步。

那么下一步到底是干啥呢?正常情况下应该就是继续往 users 集合里添加元素吧。

DefaultResultSetHandler.linkObjects

1
2
3
4
5
6
7
8
9
10
11
private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
if (collectionProperty != null) {
//如果上一步返回来的不是null,那就向这个集合里添加元素
final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
targetMetaObject.add(rowValue);
} else {
//上一步返回了null,那就直接把这个属性赋上本值,就是这种情况导致了上面的第二张图片的情况,我们的users变成了一个对象,而不是想要的数组。
metaObject.setValue(resultMapping.getProperty(), rowValue);
}
}

评论和共享

问题背景

项目后端与前端全部使用 JSON 进行数据交互,比如查询用户列表,通常后端从数据库查出的数据放入实体类再转为 JSON 返回给前端,但是前端的小伙伴表示明明只需要 nickname,avatar,userId 三个字段,为什么传过来的数据多出 phone,address ,profile 之类十几个字段,里面都 null,0 等值?

其实是因为 User 实体类定义了太多字段,转为 JSON 的时候都被包括进去了。稍微多几个还能忍,但通常 User/Order 这些实体类字段特别多,对调试开发实在非常不友好。

因此本次任务就是:确保每个接口返回字段都与接口文档上一模一样,去除冗余字段。

解决过程就省略了,直接抛出我最后的解决方案吧:就是 MyBatis 的返回值几乎完全抛弃实体类,全部使用com.alibaba.fastjson.JSONObject

使用 JSONObject

先最简化直观的看一下 JSONObject 的使用吧。

Controller:

1
2
3
4
@GetMapping("/list")
public JSONObject listUser(@RequestBody JSONObject requestJson){
return userService.listUser(requestJson);
}

Service:

1
JSONObject listUser(JSONObject jsonObject);

ServiceImpl:

1
2
3
4
5
6
7
8
9
@Override
public JSONObject listUser(JSONObject jsonObject) {
//fillPageParam是自定义的封装分页参数
CommonUtil.fillPageParam(jsonObject);
int count = userDao.countUser(jsonObject);
List<JSONObject> list = userDao.listUser(jsonObject);
//自定义successPage封装分页结果
return CommonUtil.successPage(jsonObject, list, count);
}

Dao:

1
2
3
4
public interface UserDao {
int countUser(JSONObject jsonObject);
List<JSONObject> listUser(JSONObject jsonObject);
}

UserMapper.xml , 这里的 resultType 直接就是 JSONObject ,如果是一对多的情况,就要多加一步,请移步看这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="countUser" resultType="Integer">
SELECT count(0)
FROM sys_user u
WHERE age=#{age}
</select>
<select id="users" resultType="com.alibaba.fastjson.JSONObject">
SELECT
id userId,
nickname nickname,
avatar avatar
FROM sys_user
WHERE age=#{age}
LIMIT #{offSet}, #{pageRow}
</select>

为什么可以使用JSONObject

因为 JSONObject 实现了 Map<String,Object> , MyBatis 那边完全是把它当成 Map<String,Object> 处理的,相信不少人都直接用过 map 来接收MyBatis 返回结果。

fastjson 是这样的,Gson 并不是,所以 Gson 的 JsonObject 是不可以的。

有兴趣了解更深入的原理的话,请参考我此系列的其它文章。

优劣对比

便捷性

JSONObject 便捷到可以说是无脑,接收参数、 sql 传参、封装 sql 结果、返回到前端,全程使用。

JavaBean 则接口层接收参数转为实体类,返回给前端再转为 json,新增一个业务通常还需要多建一个实体类。

工具方法拓展

JSONObject 可以方便地封装出通用的工具方法,比如封装分页信息,比如封装处理结果的成功和失败信息,特别是校验参数字段非空,不同的接口通常需要校验的字段不同,如下面这个简单的方法就可以校验各字段是否都非空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void hasAllRequired(final JSONObject jsonObject, String requiredColumns) {
if (!StringTools.isNullOrEmpty(requiredColumns)) {
String[] columns = requiredColumns.split(",");
String missCol = "";
for (String column : columns) {
Object val = jsonObject.get(column.trim());
if (StringTools.isNullOrEmpty(val)) {
missCol += column + " ";
}
}
if (!StringTools.isNullOrEmpty(missCol)) {
jsonObject.clear();
jsonObject.put("returnCode", ErrorEnum.E_90003.getErrorCode());
jsonObject.put("returnMsg", "缺少必填参数:" + missCol.trim());
throw new CommonJsonException(jsonObject);
}
}
}

JavaBean 在处理分页上通常要引各自写的 PageBean 工具类 , 使用过程通常不会比上面ServiceImpl里的分页简单。

JavaBean 的参数校验都不用说,更头痛更复杂了,不知道有没有人有用起来很爽的方法。

返回字段

JSONObject 想返回几个字段就几个字段,而 JavaBean 如果只想返回某几个字段的话,就需要单独建个实体类。

字段有改动的话,JSONObject 灵活性就更强了,直接在 mapper.xml 里改下就完事。

业务层

不可否认的是,JSONObject 在业务层进行处理时,失去了 JavaBean 的编译提示,也失去了 IDE 的快捷补全,更有可能出现取错值的情况。

比如 jsonObject.getString(“pasword”) ,输入错了单词都可能没注意,最终取出来 null 。

可读性

通常大家可能认为 JavaBean 有更好的可读性,我觉得只要变量名取得好,JSONObject 也是没有什么问题的。毕竟类名是固定死的,变量名和参数名才能更好地反映此处业务的处理。

尤其是如今前后端分离的项目,前后端开发人员一般对照着文档确认字段含义。

总结

JSONObject 开发非常方便,具有更强的灵活性,适用于中小型项目,简单业务的开发,也适用于文档至上的前后端分离开发与微服务项目。

更具体的用法,可以参考 Github 项目

评论和共享

​ MyBatis 想要打印日志,时不时想要来句 log.debug() 、log.error() ,需要个打印机 ,可是自己又不想去实现(而且跟着整个项目用同样的打印系统才是王道啊),需要去用别人家的打印机,要用别人家的产品啊,那问题可就来了。

问题 1

​ 市面上各家的打印机 slf4j、java.util.logging、log4j 甚至 System.out 都是各种不同的用法,这使用起来就太麻烦了。

思路

​ 不管市面上打印机有多少型号,我家 MyBatis 包里的类只用自家的统一接口,我家的类只管 log.debug()、log.error()…

​ 定义好了接口,就需要实现类 impl 来实现这些 debug()、error() 方法了 , 咱假装是自己来实现,其实去调用真正打印机 slf4、log4j 的方法,这样就把别人家的打印机和咱自家的接口关联起来啦。

​ 这就是适配器模式

​ 我家的每个实现类其实就是一个适配器,每个适配器去适配一种打印机。比如 slf4jLoggerImpl 就完成了对 slf4j 打印机的适配,slf4jLoggerImpl .debug() 调用了slf4j.Logger.debug()。

​ 这样市面上每多一种打印机,比如想用 log4j2 了,我就只需要加一种适配器 log4j2LoggerImpl 去适配它就可以了。

代码

​ 定义接口

1
2
3
4
5
public interface Log {
boolean isDebugEnabled();
void debug(String s);
...
}

​ 在实现类里完成适配,比如适配 slf4j 的 Slf4jLoggerImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;

class Slf4jLoggerImpl implements Log {
private final Logger log;

//注意这个地方,入参为 org.slf4j.Logger
//说明咱这个适配器要用起来,是需要传入一个真正的 slf4j 家的打印机进来的
public Slf4jLoggerImpl(Logger logger) {
log = logger;
}

//看似外面调用咱 MyBatis 的 Log 的 debug()
//其实是调用 org.slf4j.Logger 的 debug()
@Override
public void debug(String s) {
log.debug(s);
}
}

问题 2

​ 适配器咱是做好了,slf4j、log4j、stdout…做了那么多适配器,可是怎么用啊?我真正要打印机的时候,怎么才能知道我该调用哪款适配器啊?难道每次取打印机的时候,都去查一遍咱的系统配了那款打印机(jar) 包么?而且以后多加了1种打印机,我还去每个地方都改一遍,都多加一道判断么?

思路

​ 这其实就是获取实例的时候的问题,咱获取实例太累了,不如来个统一的工厂吧,我每次想打印的时候,都去找工厂要一台打印机,你工厂按照我 Log 的接口给我一个实例就是了。我也不管你给的具体实现到底是 slf4j 家的,还是 log4j 的,甚至可能是 NoLoggingImpl 每次调接口都不处理的这种空壳打印机。

​ 这样一来,有了统一的工厂,判断系统用哪种适配器的任务就可以在工厂完成了。

​ 而且以后就算新加了打印机,也只要改改工厂的代码,在工厂里多加一重判断就可以了。

​ 这就是工厂模式

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class LogFactory {
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
}

private LogFactory() {
// disable construction
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
}
}
}

​ LogFactory 提供了对外的 getLog(String logger) 方法,给需要 logger 的地方提供一个 Log 的实例。

​ 内部的实现依靠 logConstructor 这个构造器通过反射来实例化一个 Log 的 Impl ,也就是之前的适配器,比如 Slf4jLoggerImpl 。

​ 判断采用哪种logConstructor 的任务则在类初始化的时候就执行了,依次尝试了我们的每一款适配器,碰上报错就说明没 jar 包,下一款,全都没有的话,就用 NoLoggingImpl 了。

评论和共享

谷歌云搭建SSR

发布在 飞跃长城

在全国喜迎大会胜利召开的时候,墙出不去了,经常看到大家有这么一种想法 : 等会开完就好了……

为什么用GCE

  1. 速度快. 有台湾服务器,南京电信ping 稳定在40-60 ,youtube4K无压力. Chrome可以愉快地把Google用作默认浏览器了

  2. 免费一年. 只是需要提供一张外币信用卡,但是不会扣款,更不会在一年到期后自动扣款!(被AWS坑了几美元去)

如何申请GCE

​ 网上搜到教程很多,就不细述了

​ 有几个可能会卡时间的步骤包括

  • 选用静态IP

  • 和AWS一样, 放开你要分配给SSR的端口 ,如果找不到这个菜单可以搜索Firewall rules

  • Xshell生成的SSH的私钥可能要在后面加上

    google-ssh {"userName":"xxxxx@gmail.com","expireOn":"2017-11-10T04:39:03+0000"}

如何部署SSR

步骤

  1. 获取root权限
1
sudo su
  1. 安装SSR
1
wget -N --no-check-certificate https://raw.githubusercontent.com/91yun/shadowsocks_install/master/shadowsocksR.sh && bash shadowsocksR.sh

​ 安装完成的时候会有一串成功的提示信息, 可以复制记下你的ip/端口/密码/加密方式等信息

  1. 安装BBR
1
wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh
1
chmod +x bbr.sh
1
./bbr.sh
  1. 重置VM实例

  2. sudo su

  3. 检查是否成功

    1
    sysctl net.ipv4.tcp_available_congestion_control

    看是不是出现

    1
    net.ipv4.tcp_available_congestion_control = bbr cubic reno

    类似有 bbr 的字样,有就表示BBR也装好了

常用命令

/etc/init.d/shadowsocks restart 重启SSR

vi /etc/shadowsocks.json 修改配置文件

SSR客户端

下载网址 github

或者这里直接下载

配置和以前的SS也差不多, 有几点注意的是

  1. 填写加密/协议/混淆, 默认的是chacha20/auth_sha1_v4/tls1.2_ticket_auth
  2. 在选项设置里将本地端口设置成你SwitchOmega配置的本地端口.比如我就用了10801而不是默认的1080

其它SSR

https://52world.top

​ 不知道在哪看来的这个SSR网站,有免费SSR,也可以充钱升级专业版. 目前还是能用的 , 可以收藏着防止哪天其它翻墙方式失效吧.

​ 毕竟翻墙第一定律就是:永远不要在一种翻墙方式上吊死!


参考网站

  1. https://suiyuanjian.com/124.html
  2. http://www.jianshu.com/p/6bd66829a1ce
  3. http://suppore.cn/512.html

评论和共享

前后端分离实践小结

发布在 小结

背景

​ 公司项目转型,要开新的运营管理平台,我提议借此开新项目的机会,进行前后端分离,由我负责带领小组新技术学习/分享,探坑填坑.

分离原因

  1. 最关键一点,受不了jsp/freemarker里一塌糊涂的代码,分离之后,代码按规范写,简洁,好管理
  2. 原本我们后端就一直在给Android/iOS提供接口,本次分离,可以方便以后H5端的项目重构,实现Android+iOS+H5的统一,一套接口可以供三端同时使用,大量节省工作量,也可以更好地保证公司产品质量的统一性.
  3. 方便后端专心处理数据,前端实现页面效果.而不是前端仅仅画页面写css,后端还需要复制粘贴过来,套数据,修改已有页面时,前端更是不方便插手.

技术选型

​ 组员后端技术都是SSM,前端都只是略接触过AngularJS/Vue.

​ 我之前接触了一下Spring Boot,感觉配置很清爽,搭框架轻松,业务写起来快,可以让组员无缝切过来,无需费时学习,因此后端框架选定Spring Boot.而登录控制与权限管理一直是公司以前几个项目的弱项,因此本次决定引入shiro,而组员无一对shiro熟悉的,研究shiro的任务自然由我承担.

​ 前端框架是本次前后端分离的重点,由于我们组无人熟悉前端的新框架,所以这次势必每个人都需要学习许多新的前端知识.我拿公司的H5项目的几个页面进行过搭建vue框架重构练手,认为vue文档清晰,资料丰富,相关开源方案够多,我们这次可以放心学习,使用.(当然最主要的还是因为坑全都得由我来填,得挑个熟悉的)

目前进展

​ 后端:Spring Boot框架搭建完成,shiro可以进行权限管理,自定义拦截器,常用工具类完成,对常用的增删改查,返回结果,异常处理都可以快速搞定.

​ 前端:数据交互封装完毕,路由熟悉,前端权限管理初步熟悉,选用饿了么开源的Element框架,常用的增删改查的页面元素及工具方法都已让大家掌握.

​ 本周一开始教组员使用Intellij IDEA,教前端快速铺页面的方法,介绍前后端我封装的各种小轮子.经过一周时间,全部都已熟悉这套新的前后端框架,每个人都可以独立快速地推出常用页面.

接口端小技巧

​ 因为后端全部返回统一json格式的接口,所以我设计了一些小的工具方法,方便快速推出新接口,节省重复代码.

  1. 使用JSONObject而不是实体类

    ​ 因为后端业务不算复杂,所以舍弃了实体类的语义性,转而使用阿里的fastjson的JSONObject接收MyBatis返回的结果.比如一个简单的查询只需要

    1
    2
    3
    4
    5
    6
    7
    8
    <select id="getSimple" resultType="com.alibaba.fastjson.JSONObject">
    SELECT
    s.order_id shopOrderId,
    s.operation operation,
    date_format(s.create_time, '%Y.%m.%d %H:%i:%s') createTime
    FROM shop_order_log s
    WHERE s.user_delete_status = "1"
    </select>

    就可以将三个字段shopOrderId,operation,createTime放入json内,字段名称更加灵活,也免去了大量的建实体类,写resultMap的代码.

    ​ 更好的一点是,不使用实体类,可以在返回结果中避免掉很多空的需不要的字段.

    ​ 还有,因为所有的接口层都返回JSONObject,所以编写工具方法,快速返回成功/失败结果也很简单,我写了多个工具方法,包括入参转json,入参非空校验,返回成功/失败结果,分页,这里就不一一贴出代码了.

  2. 自定义Exception

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class CommonJsonException extends RuntimeException {
    private JSONObject resultJson;

    /**
    * 调用时可以在任何代码处直接throws这个Exception,
    * 都会统一被拦截,并封装好json返回给前台
    *
    * @param errorEnum 以错误的ErrorEnum做参数
    */
    public CommonJsonException(ErrorEnum errorEnum) {
    JSONObject jsonObject = new JSONObject();
    CommonUtil.returnJson(jsonObject, errorEnum);
    this.resultJson = jsonObject;
    }

    public CommonJsonException(JSONObject resultJson) {
    this.resultJson = resultJson;
    }

    public JSONObject getResultJson() {
    return resultJson;
    }
    }

    @ControllerAdvice
    @ResponseBody
    public class GlobalExceptionHandler {
    @ExceptionHandler(CommonJsonException.class)
    public JSONObject CommonJsonExceptionHandler(CommonJsonException commonJsonException) throws Exception {
    return commonJsonException.getResultJson();
    }
    }

    在需要返回给前端错误代码编号时,比如校验参数非空,校验手机号失败,可以直接抛出此异常,经错误拦截器拦截到此异常后,可以直接返回错误码给前端,节省大量的判断/返回的代码.这一点也是前阵子从Spring源码中学到的.

存在的问题

  1. 近期主要问题基本都是待我去研究深入的技术,比如shiro的动态权限与vue-router的异步路由的结合,vue组件的通信,vuex的状态管理
  2. 其次是组员需要对vue更加熟悉,我这次要求了大家抛弃jQuery,用数据绑定的思想去写前端.目前大家的前端水平也还只是可以快速复制粘贴出页面,对于复杂的页面和组件,甚至对我们前端项目的目录结构与各部分功能,都不算掌握.
  3. 目前我们所谓的前后端分离其实还只是初步的业务和代码上的分离,以后如果要重构H5项目,可能还需要开NodeJS项目来解决和Android/iOS一样的接口加密问题,统一session管理问题,以及更多的前后端分离可能给我们带来的问题.

评论和共享

​ 最近在做前后端分离的新框架,选用了后端springboot+shiro,前端vue+elementUI,第一次搭SSM之外的非demo项目,尤其shiro更是之前从未接触,折腾了很多天,遇到很多问题,大部分能百度出来,剩下的非常费时的问题且称之为坑吧.

跨域

​ 一大部分问题就是跨域造成的,本身vue-cli搭建的项目可以用

1
2
3
4
5
6
7
8
9
10
11
// 1. axios的baseURL设置为/api
// 2. 如下设置
proxyTable: {
'/api': {
target: 'http://127.0.0.1:8080/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
}
},

来解决开发环境跨域的问题,生产环境反正可以打包静态文件到springboot项目中直接跑.

而我还是脑抽得选择了强行跨域,假装自己要把静态文件单独放个服务器跑…为此遇到很多问题,折腾好几天.

因此强力推荐上面的方案,简单快捷地解决跨域!

如果生产环境真的需要跨域的话,再按下面方法设置

大部分跨域的配置都能百度搜出来:

  1. axios要 withCredentials: true

  2. 用session而不用啥特殊的token之类的话,就不用 config.headers['X-Token'] = getToken() ,这句是vueAdmin-template 中带的,需要删掉.

  3. 后端需要配置允许跨域

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Configuration
    public class CorsConfig {
    private CorsConfiguration buildConfig() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
    corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
    corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
    corsConfiguration.setAllowCredentials(true);
    return corsConfiguration;
    }

    @Bean
    public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
    return new CorsFilter(source);
    }
    }

shiro拦截ajax返回json

​ shiro拦截到需要登录而用户尚未的请求时,会重定向至 /login/auth (未配置时是login.jsp),而ajax是不允许重定向的,ajax会收到302错误码,报错

Failed to load http://localhost:8080/test: Redirect from ‘http://localhost:8080/test‘ to ‘http://localhost:8080/login/auth‘ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://localhost:9528‘ is therefore not allowed access.

因此我们需要对拦截器进行改造,不要它默认的重定向了,我们直接去response里把json写好输出给前端.

自定义filter,(因为我的后端全部返回json,所以这里不判断是否ajax了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class AjaxPermissionsAuthorizationFilter extends FormAuthenticationFilter {

@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("returnMsg", "未登录或登录已失效");
if (this.isLoginRequest(request, response)) {
if (this.isLoginSubmission(request, response)) {
return this.executeLogin(request, response);
} else {
return true;
}
} else {
PrintWriter out = null;
HttpServletResponse res = (HttpServletResponse) response;
//下面这几行也是后面要讲的坑, 这里的Access-Control-Allow-Origin 设置为*的话,前端还是会报错.
res.setHeader("Access-Control-Allow-Origin", "http://localhost:9528");
// response1.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
try {
res.setCharacterEncoding("UTF-8");//设置编码
res.setContentType("application/json");//设置返回类型
out = response.getWriter();
out.println(jsonObject);//输出
} catch (Exception e) {
} finally {
if (null != out) {
out.flush();
out.close();
}
}
return false;
}
}
}

接下来就是最坑的地方了,拦截器的注入.

原本搜到的方法是在ShiroConfiguration类中注入.

1
2
3
4
5
6
7
8
9
@Bean(name = "ajaxPermissionsAuthorizationFilter")
public AjaxPermissionsAuthorizationFilter ajaxPermissionsAuthorizationFilter(){
return new AjaxPermissionsAuthorizationFilter();
}

//然后shiroFilterFactoryBean设置过滤器
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("authc", new AjaxPermissionsAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);

结果自定义的拦截器把所有的请求都拦截了,直接无视了我设置的

1
2
3
4
5
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/error", "anon");
filterChainDefinitionMap.put("/**", "authc");

各种百度,能搜到的相关的资料也就只有segmentfault的提问没解决,题主给我的回复也不能解决 , 百度知道提问的没解决 , 简书方案并不能解决 .

最终在某个百度结果的第三页找到这篇博客 ,博主对问题一步一步的排查分析,debug源码,最终知道

Springboot 先加载了我们自定义的 Filter,然后再加载了 ShiroFilter

解决方法:
在自定义的filter里加上下面的代码

1
2
3
4
5
6
@Bean
public FilterRegistrationBean registration(自定义Filter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}

在configration类里就不需要声明这个bean,只需要直接调用 filterMap.put("authc", new 自定义Filter());

Access-Control-Allow-Origin

在上面的自定义filter里,

1
res.setHeader("Access-Control-Allow-Origin", "http://localhost:9528");

如果设置为*的话,前端虽然可以收到json,但还是会报错

Failed to load http://localhost:8080/test: The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ‘*’ when the request’s credentials mode is ‘include’. Origin ‘http://localhost:9528‘ is therefore not allowed access. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.

不允许设置为通配符* .

虽然此处设置了”http://localhost:9528“ 但是这种做法终究不合适.

百度继续搜到此博客

得到解决方法

1
res.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));

评论和共享

获取单例bean getSingleton

明明之前一篇已经讲过了获取单例,为什么这里又是获取单例bean呢?

两天不看书,果断又忘了.前面学的是从缓存中获取,这里是真正的获取.

DefaultSingletonBeanRegistry 中重载此方法,第二参数为ObjectFactory<?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
//这里再次看到了这个singletonObjects,上篇文章介绍过,是维护了单例对象
//书中说法是:用于保存BeanName和创建bean实例之间的关系
Map var3 = this.singletonObjects;
synchronized(this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
//首先获取一遍,如果不存在,才去创建
if (singletonObject == null) {
//工厂如果正在销毁,这时候获取bean就会报错
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while the singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
}

if (this.logger.isDebugEnabled()) {
//打印日志,正在创建单例bean(beanName)的共享的实例
this.logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
//标志这个beanName正在创建,如果同时重复创建,会报错
this.beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = this.suppressedExceptions == null;
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet();
}

try {
//=============这里应该是最关键的创建bean的步骤===============
singletonObject = singletonFactory.getObject();
//标识新创建出来的单例
newSingleton = true;
} catch (IllegalStateException var16) {
//非法状态错误,应该是创建过程中发现存在其他线程已创建此单例
//所以此处catch里面再次调用了singletonObjects.get(beanName);
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw var16;
}
} catch (BeanCreationException var17) {
//其它创建bean错误
BeanCreationException ex = var17;
if (recordSuppressedExceptions) {
Iterator var8 = this.suppressedExceptions.iterator();
while(var8.hasNext()) {
Exception suppressedException = (Exception)var8.next();
//把其它recordSuppressedExceptions塞入这个ex一并抛出
ex.addRelatedCause(suppressedException);
}
}
throw ex;
} finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
//确认这个beanName在几个set中的状态
this.afterSingletonCreation(beanName);
}
if (newSingleton) {
//如果是新创建的,则加入缓存,移除几个新创建的标识,名字加入registeredSingletons
this.addSingleton(beanName, singletonObject);
}
}
//返回,如果是NULL_OBJECT也作为null返回.
//这个NULL_OBJECT应该是在前面的获取方法中有可能的特殊返回值
return singletonObject != NULL_OBJECT ? singletonObject : null;
}
}

从上可以看出,获取单例还是通过 synchronized(this.singletonObjects) 加锁来实现,先从 singletonObjects 查一遍有没有已存在的,若没有则再进行创建。

而创建的步骤,则是在入参的 ObjectFactory<?> 中完成,调用其 getObject() 依然还是调用我们本身AbstractBeanFactorycreateBean 方法。


我们先不急着往下看,先理一理,首先第一点要知道的是单例加锁都是 synchronized(this.singletonObjects) 。其次获取单例 bean 的大致步骤,无非就是:

  1. 上一文中介绍的,从缓存中拿,并且允许“早期引用” ,即从 earlySingletonObjects 中拿。

  2. 如果缓存中没有,则自己创建,

    2.1 在各种 map 里记录它的 创建、销毁等信息

    2.2 通过createBean 方法去具体创建这个实例。

评论和共享

缓存中获取单例bean getSingleton

DefaultSingletonBeanRegistry 中实现此方法

1
2
3
4
5
6
7
8
//这个map维护了单例对象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//当前正在创建中的单例对象
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap(16));
//早期单例对象
private final Map<String, Object> earlySingletonObjects = new HashMap(16);
//单例工厂?
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
//如果根据这个beanName没取到对象,但发现这个对象还在创建中.....
if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
Map var4 = this.singletonObjects;
synchronized(this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
//如果早期单例对象中没有此bean,并且允许早期依赖
if (singletonObject == null && allowEarlyReference) {
//当某些方法需要提前初始化的时候则会调用 addSingletonFactory方法将对应的ObjectFactory初始化策略存储在singletonFactories
ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
if (singletonFactory != null) {
//调用预先设定的getObject方法
singletonObject = singletonFactory.getObject();
//记录在缓存中,earlySingletonObjects 和 singletonFactories互斥
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject != NULL_OBJECT ? singletonObject : null;
}

这里还是看得迷迷糊糊的,毕竟一来就各种取值, 往后看看在哪塞值的吧!

从bean的实例中获取对象 getObjectForBeanInstance

​ 我们得到bean的实例后要做的第一步都是调用这个方法来检测一下正确性,其实就是用于检测当前bean是否是FactoryBean 类型的bean,如果是,就要调用getObject() 方法作为返回值

FactoryBean 在之前我们便已经接触过.

getObjectForBeanInstance 方法就不贴了, 主要就是各种验证, 取缓存等等, 里面需要注释的一段如下

1
2
3
4
5
   	//将存储XML配置文件的GernericBeanDefinition 转换为RootBeanDefinition,
//如果指定BeanName是子bean的话,同时会合并父类的相关属性
if (mbd == null && this.containsBeanDefinition(beanName)) {
mbd = this.getMergedLocalBeanDefinition(beanName);
}

​ 方法中的关键是 getObjectFromFactoryBean , 实现类为 FactoryBeanRegistrySupport

​ 而 getObjectFromFactoryBean 方法中也还不是最核心的, 其代码主要是保证单例bean的全局唯一. 如果是单例,那就不用重复创建,可以使用缓存来提高性能. 另外方法还根据需要对bean进行了后处理postProcessObjectFromFactoryBean()

后处理即是遍历所有的bean后处理器,在bean初始化后调用它的处理方法,后面还会详细学习.

​ 它的关键是 doGetObjectFromFactoryBean ,方法名前面多加了个do…

在这个方法里面,我们终于看到想要看到的 `object = factory.getObject();`  ,虽然早已知道必须要走到这,但是到这一步`getObject()` 还是绕了很多路,包括到这里居然还使用了`java.security.AccessController` 权限验证...

评论和共享

1
2
3
4
5
6
7
8
9
10
package org.springframework.beans.factory;

public interface FactoryBean<T> {
//返回由FactoryBean创建的bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中
T getObject() throws Exception;
//返回FactoryBean创建的bean类型
Class<?> getObjectType();
//返回bean实例的作用域是singleton还是prototype
boolean isSingleton();
}

​ 当配置文件的class属性配置的实现类是FactoryBean 时,通过getBean() 方法返回的不是FactoryBean 本身,而是FactoryBean.getObject() 方法所返回的对象.

​ 相当于FactoryBean.getObject() 代理了getBean() 方法.

​ 例如,如果使用传统方式配置Car的bean,Car的每个属性对应一个 元素标签

1
2
3
4
5
6
public class Car {
private int maxSpeed;
private String brand;
private double price;
//get/set
}

​ 如果使用FactoryBean的方式就会灵活一些,下面通过逗号分隔符的方式一次性地为Car的所有属性指定配置值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class CarFactoryBean implements FactoryBean<Car> {
private String carInfo;

@Override
public Car getObject() throws Exception {
Car car = new Car();
String[] infos = carInfo.split(",");
car.setBrand(infos[0]);
car.setMaxSpeed(Integer.valueOf(infos[1]));
car.setPrice(Double.valueOf(infos[2]));
return car;
}

@Override
public Class<?> getObjectType() {
return Car.class;
}

@Override
public boolean isSingleton() {
return false;
}

public String getCarInfo() {
return carInfo;
}

public void setCarInfo(String carInfo) {
this.carInfo = carInfo;
}
}

有了这个CarFactoryBean之后,就可以在配置文件使用下面这种自定义的配置方式配置Car bean了

1
<bean id="car" class="com.test.factoryBean.CarFactoryBean" carInfo="超级跑车,400,3000" />

调用 getBean(“car”) , Spring并不会返回CarFactoryBean 而是返回car

如果希望返回CarFactoryBean 那就 getBean(“&car”)


书本作者讲到FactoryBean 接口对于Spring框架很关键, Spring自身就提供了70多个实现.

它们隐藏了实例化一些复杂的bean的细节,给上层应用带来了便利.

疑问: 具体哪些地方用FactoryBean 带来了真正的便利呢?

答:

评论和共享

作者的图片

heeexy

世上是不是就没有你不认识的字了?


JAVA


南京