0x00 数据流转的困境
再看一下Struts2中MVC各层
Model: User.java
public class User {
private String name;
private String password;
public User() {}
// setter,getter方法
}
View: login.jsp
<form method="post" action="/login">
username: <input type="text" name="user.name"/>
password: <input type="text" name="user.password"/>
<input type="submit" name="submit"/>
</form>
Control: LoginServlet.java
public class UserAction implements Action {
private User user;
private String execute() {
// 可以直接在这里使用user对象,因为它已经被作为参数传入了
return "success";
}
// setter,getter
}
View层的数据模型将遵循HTTP协议,它没有数据类型的概念,多为字符串,数据是弱类型的
Controller层的数据模型遵循Java的语法和数据结构,所有数据载体在Java世界中可以表现为丰富的数据结构和数据类型,可以自定义喜欢的类,在类与类之间进行继承、嵌套,通常把这种模型叫做对象树,数据在传递时,将以对象树形式进行,数据是强类型的
数据在不同的MVC层次上,扮演的角色和表现形式不同,是由于HTTP协议与Java面向对象之间的不匹配造成的
从View层到Controller层,这个方向流转的数据期望可以将一个扁平而分散的数据能以一定的规则设置到Java世界的对象树中去,同时,能够聪明地进行由字符串类型到Java中各个类型的转化
从Controller层到View层,这个方向流转的数据期望保证View才能能够以某些简单的规则对Java的对象树进行访问,同时,在一定程度上控制对象树中的数据的显示格式
数据访问的困境,来源于数据模型在某些层次的展现缺乏足够的表现力
为了处理这个数据流转的问题,比如Java世界用Hibernate或者MyBatis这样的持久层框架来处理Java对象与关系型数据库的匹配,在Struts2中,引入来OGNL表达式引擎来处理View层和Controller层的数据匹配关系
OGNL表达式引擎会帮助我们在Web开发中完成定义了一定规则的字符串表达式和Java对象之间相互转化
0x01 从OGNL的API看OGNL三要素和基本操作
OGNL的API
public static Object getValue(String expression, Map context, Object root) throws OgnlException {
return getValue((String)expression, (Map)context, root, (Class)null);
}
public static void setValue(String expression, Map context, Object root, Object value) throws OgnlException {
setValue(parseExpression(expression), context, root, value);
}
上面两个方法分别针对对象的取值和写值操作,OGNL的基本操作就是通过上述这两个方法的3个参数实现,OGNL同时编写了很多其他方法,但大同小异,上面两个最具代表性
我们来试用以下这两个方法
public class OGNLTest {
public static void main(String[] args) throws Exception {
User user = new User();
user.setId(1);
user.setUsername("good");
HashMap context = new HashMap();
context.put("introduction", "My name is ");
Object name = Ognl.getValue(Ognl.parseExpression("name"), user);
System.out.println(name);
Object contextValue = Ognl.getValue(Ognl.parseExpression("#introduction"), context, user);
System.out.println(contextValue);
Object hello = Ognl.getValue(Ognl.parseExpression("#introduction + name"), context, user);
System.out.println(hello);
Ognl.setValue("age", user, "18");
System.out.println(user.getAge());
}
}
输出
good
My name is
My name is good
18
OGNL三要素
从上面的例子中看出,OGNL的所有操作都是围绕3个参数进行的,这三个参数被称为OGNL的三要素
表达式
表达式是整个OGNL的核心,表达式是一个带有语法含义的字符串,规定操作的类型和操作的内容
Root对象
OGNL的Root对象可以理解为OGNL的操作对象,表达式规定干什么,Root对象规定对谁干,Root对象实际上是一个Java对象,是所有OGNL操作的实际载体
上下文环境
这个上下文环境规定OGNL的操作”在哪里干”,OGNL内部所有操作会在一个特定的数据环境中进行,OGNL的上下文是一个Map结构,称为OGNLContext,上面的Root也会添加到上下文环境中,作为一个特殊变量处理
OGNL基本操作
name // 获取Root对象中name的值
department.name // 获取Root对象department属性的name属性的实际值
#introduction // 获取上下文环境中introduction的对象的值
#parameters.user.name // 获取上下文环境中parameters对象中user属性的name的属性的值
@com.example.core.Resource@ENABLE // 访问com.example.core.Resource类中名为ENABLE的值
@com.example.core.Resource@get() // 调用com.example.core.Resource类中get方法
group.users.size() // 调用Root对象中group属性的属性users的size方法
group.containsUser(#requestUser) // 调用Root对象中属性group的containsUser方法,并将上下文环境中的requestUser作为参数传入
加减乘除、取模、字符串叠加等操作
foo++ // 递增
foo == bar // 等于判断
foo in list // 是否在容器中
group.users[0].name // 访问Root对象的group属性中users的第一个对象的name属性值
#sessionMap['currentLoginUser'] // 访问OGNL上下文中名为sessionMap的Map对象中key为currentLoginUser的值
group.users.{name} // 返回Root对象中group属性中users这个集合中所有元素的name构成的集合
group.users.{code + '-' + name} // 将group中users这个集合中的元素的code和name用-链接符拼起来构成的字符串集合
group.users.{? #this.name != null} // 返回Root对象的group中users这个集合所有元素中name不为null的元素构成的集合
{"green", "red", "blue"} // 构造一个List
#{"key1": "value1", "key2": "value2", "key3": "value3"} // 构造一个Map
new java.net.URL("http://localhost/") // 构造一个java.net.URL对象
users.{? #this.age > 3} // 返回group中users这个集合中所有age比3大的元素构成的集合
group.users.size().(#this+1) // 返回group中users这个集合的大小+1的值
group.users.{? #this.name != null} // 返回Root对象的gourp中users这个集合所有元素中name不为null的元素构成的集合
0x02 深入OGNL
OgnlContext
OGNL三要素中上下文环境,在OGNL内部有一个实际的对象与之对应,即OgnlContext,是一个Map结构
OGNL的上下文环境实际上包涵来很多参数设置,这些参数指定了OGNL在进行计算时使用的一些默认行为和默认值,这些参数会参与到OGNL计算中
public static final ClassResolver DEFAULT_CLASS_RESOLVER = new DefaultClassResolver();
public static final TypeConverter DEFAULT_TYPE_CONVERTER = new DefaultTypeConverter();
public static final MemberAccess DEFAULT_MEMBER_ACCESS = new DefaultMemberAccess(false);
private static Map RESERVED_KEYS = new HashMap(11);
// Root对象的引用
private Object _root;
// 通过一个Map来维护Ognl的上下文环境
private final Map _values;
private ClassResolver _classResolver;
private TypeConverter _typeConverter;
private MemberAccess _memberAccess;
我看查看以下OgnlContext内部的数据结构
- _root:在OgnlContext内部维护着Root对象,它是OGNL主要的操作对象
- _values:在OGNL计算时候使用传入的Map作为上下文环境,OGNL依旧会创建一个OgnlContext,将传入的Map中所有键值对维护在_values变量中,从这里可以看出OgnlContext是装饰器模式
- ClassResolver:指定处理class loading的处理类,实际上这个处理类是用于指定OGNL在根据Class名称来构建对象时,寻找Class名称与对应Class类之间对应关系的处理方式,默认情况下用JVM的class.forName实现
- TypeConverter:指定处理类型转化的处理类,这个处理类非常关键,它会指定一个对象属性转化成字符串以及字符串转化成Java对象时的处理方式
- MemberAccess:指定处理属性访问策略的处理方式
OGNL计算规则
public static final ClassResolver DEFAULT_CLASS_RESOLVER = new DefaultClassResolver();
从上面这行代码中,我们可以发现,OGNL在设计的时候就已经充分考虑了扩展性,将计算原则定义成接口,并且允许运行时替换它的实现
ClassResolver——类的寻址方式
ClassResolver的接口定义
public interface ClassResolver {
Class classForName(String var1, Map var2) throws ClassNotFoundException;
}
从定义中可以看出,ClassResolver通过接收一个字符串作为Java类寻址的依据,返回值是经过寻址后得到的与之对应的Java类
OGNL的默认实现是ognl.DefaultClassResolver,使用了JVM的class.forName机制来实现,在Struts2中,为了处理一些特殊支持的class名称,例如vs,vs2,vs3等,采用了com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor
作为实现方式
public Class classForName(String className, Map context) throws ClassNotFoundException {
Object root = Ognl.getRoot(context);
try {
if (root instanceof CompoundRoot) {
if (className.startsWith("vs")) {
CompoundRoot compoundRoot = (CompoundRoot) root;
if ("vs".equals(className)) {
return compoundRoot.peek().getClass();
}
int index = Integer.parseInt(className.substring(2));
return compoundRoot.get(index - 1).getClass();
}
}
} catch (Exception e) {
LOG.debug("Got exception when tried to get class for name [{}]", className, e);
}
return Thread.currentThread().getContextClassLoader().loadClass(className);
}
对vs开头的类进行的特殊处理,最后使用ClassLoader进行处理
TypeConverter——类型转化方式
TypeConverter的接口定义
public interface TypeConverter {
Object convertValue(Map<String, Object> var1, Object var2, Member var3, String var4, Object var5, Class var6);
}
Struts2中TypeConverter的默认实现类是OgnlTypeConverterWrapper
public class OgnlTypeConverterWrapper implements ognl.TypeConverter {
private final TypeConverter typeConverter;
public OgnlTypeConverterWrapper(TypeConverter converter) {
if (converter == null) {
throw new IllegalArgumentException("Wrapped type converter cannot be null");
}
this.typeConverter = converter;
}
public Object convertValue(Map context, Object target, Member member,
String propertyName, Object value, Class toType) {
return typeConverter.convertValue(context, target, member, propertyName, value, toType);
}
public TypeConverter getTarget() {
return typeConverter;
}
}
从源码中可以看到,被OgnlTypeConverterWrapper所装饰的实现类是Struts2中的TypeConverter,这个TypeConverter接口可以被我们自由扩展,在Struts2中默认实现是DefaultTypeConverter
public abstract class DefaultTypeConverter implements TypeConverter {
protected static String MILLISECOND_FORMAT = ".SSS";
private static final String NULL_STRING = "null";
private static final Map<Class, Object> primitiveDefaults;
private Container container;
static {
// 注册默认支持的基本类型
Map<Class, Object> map = new HashMap<>();
map.put(Boolean.TYPE, Boolean.FALSE);
map.put(Byte.TYPE, Byte.valueOf((byte) 0));
map.put(Short.TYPE, Short.valueOf((short) 0));
map.put(Character.TYPE, new Character((char) 0));
map.put(Integer.TYPE, Integer.valueOf(0));
map.put(Long.TYPE, Long.valueOf(0L));
map.put(Float.TYPE, new Float(0.0f));
map.put(Double.TYPE, new Double(0.0));
map.put(BigInteger.class, new BigInteger("0"));
map.put(BigDecimal.class, new BigDecimal(0.0));
primitiveDefaults = Collections.unmodifiableMap(map);
}
public Object convertValue(Object value, Class toType) {
// 根据Class类型,返回类型转换后的值
Object result = null;
if (value != null) {
/* If array -> array then convert components of array individually */
if (value.getClass().isArray() && toType.isArray()) {
Class componentType = toType.getComponentType();
result = Array.newInstance(componentType, Array
.getLength(value));
for (int i = 0, icount = Array.getLength(value); i < icount; i++) {
Array.set(result, i, convertValue(Array.get(value, i),
componentType));
}
} else {
if ((toType == Integer.class) || (toType == Integer.TYPE))
result = (int) longValue(value);
if ((toType == Double.class) || (toType == Double.TYPE))
result = doubleValue(value);
if ((toType == Boolean.class) || (toType == Boolean.TYPE))
result = booleanValue(value) ? Boolean.TRUE : Boolean.FALSE;
if ((toType == Byte.class) || (toType == Byte.TYPE))
result = (byte) longValue(value);
if ((toType == Character.class) || (toType == Character.TYPE))
result = (char) longValue(value);
if ((toType == Short.class) || (toType == Short.TYPE))
result = (short) longValue(value);
if ((toType == Long.class) || (toType == Long.TYPE))
result = longValue(value);
if ((toType == Float.class) || (toType == Float.TYPE))
result = new Float(doubleValue(value));
if (toType == BigInteger.class)
result = bigIntValue(value);
if (toType == BigDecimal.class)
result = bigDecValue(value);
if (toType == String.class)
result = stringValue(value);
if (Enum.class.isAssignableFrom(toType))
result = enumValue(toType, value);
}
} else {
if (toType.isPrimitive()) {
result = primitiveDefaults.get(toType);
}
}
return result;
}
}
上面这段代码默认处理类绝大多数我们日常编程中最常用的Java类型,但有一堆if/else,虽然大而全,但不优雅,扩展性也不好(据说SpringEL实现上扩展性更好,没有去看)
MemberAccess——方法/属性访问策略
MemberAccess的接口
public interface MemberAccess {
// 设置访问策略
Object setup(Map var1, Object var2, Member var3, String var4);
// 恢复原始的Member访问策略
void restore(Map var1, Object var2, Member var3, String var4, Object var5);
// 判断当前Member是否具备访问权限
boolean isAccessible(Map var1, Object var2, Member var3, String var4);
}
在Struts2中,SecurityMemberAccess实现类MemberAccess
public class SecurityMemberAccess extends DefaultMemberAccess {
private final boolean allowStaticMethodAccess;
private Set<Pattern> excludeProperties = Collections.emptySet();
private Set<Pattern> acceptProperties = Collections.emptySet();
private Set<Class<?>> excludedClasses = Collections.emptySet();
private Set<Pattern> excludedPackageNamePatterns = Collections.emptySet();
private Set<String> excludedPackageNames = Collections.emptySet();
private boolean disallowProxyMemberAccess;
/**
* SecurityMemberAccess
* - access decisions based on whether member is static (or not)
* - block or allow access to properties (configurable-after-construction)
*
* @param allowStaticMethodAccess
*/
public SecurityMemberAccess(boolean allowStaticMethodAccess) {
super(false);
this.allowStaticMethodAccess = allowStaticMethodAccess;
}
Struts2在对象属性的访问策略上进行扩展,指定是否支持访问静态方法以及通过正则表达式来规定某些属性是否能够被访问
MethodAccessor和PropertyAccessor——方法/属性访问机制
MethodAccessor和PropertyAccessor规定了OGNL在访问方法和属性时的实现方式,这里我么你不放代码看了
我们只需要知道在struts-default.xml中,可以找到很多MethodAccessor和PropertyAccessor作为type的Bean,Struts2采用的策略是将访问机制的实现根据不同的Java类型派发到对应的实现类中去