Struts2之-OGNL和ActionContext原理详解

什么是OGNL?

OGNL(Object-Graph Navigation Language的简称),对象图导航语言,它是一门表达式语言,使用这种表达式语言,你可以通过某种表达式语法,存取Java对象树中的任意属性、调用Java对象树的方法、同时能够自动实现必要的类型转化。如果我们把表达式看做是一个带有语义的字符串,那么OGNL无疑成为了这个语义字符串与Java对象之间沟通的桥梁。

OGNL的API看起来就是两个简单的静态方法:

 public static Object getValue( Object tree, Map context, Object root ) throws OgnlException;  
 public static void setValue( Object tree, Map context, Object root, Object value ) throws OgnlException  

A) 针对根对象(Root Object)的操作,表达式是自根对象到被访问对象的某个链式操作的字符串表示。

B) 针对上下文环境(Context)的操作,表达式是自上下文环境(Context)到被访问对象的某个链式操作的字符串表示,但是必须在这个字符串的前面加上#符号,以表示与访问根对象的区别。

OGNL三要素:表达式,上下文,根对象

  1. 表达式(Expression)
      表达式是整个OGNL的核心,所有的OGNL操作都是针对表达式的解析后进行的。表达式会规定此次OGNL操作到底要干什么。我们可以看到,在上面的测试中,name、department.name等都是表达式,表示取name或者department中的name的值。OGNL支持很多类型的表达式,之后我们会看到更多。

  2. 根对象(Root Object)

  根对象可以理解为OGNL的操作对象。在表达式规定了“干什么”以后,你还需要指定到底“对谁干”。操作根对象的表达式不需要加’#’

  1. 上下文环境(Context)

  有了表达式和根对象,我们实际上已经可以使用OGNL的基本功能。例如,根据表达式对根对象进行取值或者设值工作。不过实际上,在OGNL的内部,所有的操作都会在一个特定的环境中运行,这个环境就是OGNL的上下文环境(Context)。说得再明白一些,就是这个上下文环境(Context),将规定OGNL的操作“在哪里干”。

  OGNL的上下文环境是一个Map结构,称之为OgnlContext。上面我们提到的根对象(Root Object),事实上也会被加入到上下文环境中去,并且这将作为一个特殊的变量进行处理,具体就表现为针对根对象(Root Object)的存取操作的表达式是不需要增加#符号进行区分的。

  OgnlContext不仅提供了OGNL的运行环境。在这其中,我们还能设置一些自定义的parameter到Context中,以便我们在进行OGNL操作的时候能够方便的使用这些parameter。不过我们在访问这些parameter时,需要使用#作为前缀才能进行。

OGNL的基本功能测试

  以上我们了解了OGNL的基本概念,下面我们通过代码来详细了解一下OGNL的功能和使用。

需要的两个JavaBean类

Users.java

/**
 * 
 */
package entity;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 *
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 6879304390797686316L;
    private String username;
    private String password;
}

Student.java

/**
 * 
 */
package entity;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Administrator
 *
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = -4248135347869749538L;
    private String name;
    private String sex;
    private int age;

}

OGNL测试代码

/**
 * 
 */
package ognl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.junit.Test;

import entity.Student;
import entity.Users;

/**
 * @author Administrator
 *
 */
public class OgnlTest {
    /**
     * 测试ognl的使用和方法的调用
     */
    @Test
    public void ognl1() {
    Student student = new Student("学生张三", "男", 17);
    Users user = new Users("用户李四", "123456");
    Users users = new Users("根用户", "123456");
    // 构建一个OgnlContext对象
    OgnlContext context = new OgnlContext();

    // 设置用户为根对象
    context.setRoot(users);
    context.put("user", user);
    context.put("student", student);

    // 构建Ognl表达式的树状表示,用来获取
    try {
        // 解析树状表达式,返回结果
        Object rootName = Ognl.getValue("username", context, context.getRoot());
        Object userName = Ognl.getValue("#user.username", context, context.getRoot());
        System.out.println(rootName + ":" + userName);

     // 从根对象中直接获取用户名称长度,非静态方法
        Object expression = Ognl.parseExpression("#user.username.length()");
        Object length = Ognl.getValue(expression, context, context.getRoot());
        System.out.println("非静态方法获取密码用户名长度"+length);

        // 在Ognl表达式中使用静态方法
         expression = Ognl.parseExpression("@java.lang.Integer@valueOf(#user.password)");
         length = Ognl.getValue("@java.lang.Integer@valueOf(#user.password)", context, context.getRoot());
        System.out.println("静态方法获取密码"+length);

    } catch (OgnlException e) {
        e.printStackTrace();
    }


    }


    /**
     * 测试Ognl表达式中如何处理数组、集合和Map对象
     * @throws OgnlException 
     */
    @Test
    public void ognl2() throws OgnlException {
    OgnlContext context = new OgnlContext();

    // 通过Ognl表达式构建一个LinkedList对象,这注意:一定是包名+类名的形式
    Object list = Ognl.parseExpression("new java.util.LinkedList()");
    Object obj = Ognl.getValue(list, context, context.getRoot());
    System.out.println(obj);
    System.out.println("----------------------------");

    // 在Ognl中提供了一种类似数组索引的方式访问集合指定位置的元素
    // 下述例子直接构建了一个包含aa, bb, cc, dd四个元素的集合,然后访问集合中的第三个元素
    Object object15 = Ognl.getValue("{'aa', 'bb', 'cc', 'dd'}[2]", context, context.getRoot());
    System.out.println(object15);
    System.out.println("----------------------------");

    // 处理数组类型
    String[] strs = new String[] { "aa", "bb", "cc" };
    context.put("strs", strs);
    System.out.println(Ognl.getValue("#strs[1]", context, context.getRoot()));
    System.out.println("----------------------------");

    // 处理集合类型
    List<String> words = new ArrayList<String>();
    words.add("hello");
    words.add("world");
    words.add("hello world");
    context.put("words", words);
    System.out.println(Ognl.getValue("#words[0]", context, context.getRoot()));
    System.out.println("----------------------------");

    // 处理Map类型,注意的是为了与集合区分开,在大括号前面加"#"
    System.out.println(
            Ognl.getValue("#{'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4'}['key3']",
                    context, context.getRoot()));

    }

    /**
     * 测试Ognl表达式的过滤和投影
     * @throws OgnlException 
     */
    @Test
    public void ognl3() throws OgnlException {
    Student s1 = new Student("Tom", "男", 18);
    Student s2 = new Student("Jack", "女", 17);
    Student s3 = new Student("Tomas", "男", 40);
    Student s4 = new Student("Lucy", "女", 16);
    List<Student> stus = new ArrayList<Student>();

    Collections.addAll(stus, s1, s2, s3, s4);
    // 新建OgnlContext对象
    OgnlContext context = new OgnlContext();
    context.put("stus", stus);

    // 过滤(filtering),collection.{? expression}
    // 利用过滤获取年龄在17以上的所有学生集合
    // 输出结果:[Student(name=Tom, sex=男, age=18), Student(name=Tomas, sex=男, age=40)]
    System.out.println(Ognl.getValue("#stus.{? #this.age > 17}", context, context.getRoot()));

    // 过滤(filtering),collection.{^ expression}
    // 利用过滤获取年龄在17以上的所有学生集合中第一个元素
    // 输出结果:[Student(name=Tom, sex=男, age=18)]
    System.out.println(Ognl.getValue("#stus.{^ #this.age > 17}", context, context.getRoot()));

    // 过滤(filtering),collection.{$ expression}
    // 利用过滤获取年龄在17以上的所有学生集合的最后一个元素
    // 输出结果:[Student(name=Tomas, sex=男, age=40)]
    System.out.println(Ognl.getValue("#stus.{$ #this.age > 17}", context, context.getRoot()));

    // 投影(projection), collection. {expression}
    // 获取集合中的所有学生的姓名
    // 输出结果:[Tom, Jack, Tomas, Lucy]
    System.out.println(Ognl.getValue("#stus.{name}", context, context.getRoot()));
    }
}

通过以上代码我们可以看到OGNL的功能非常强大,十分简洁的表达式就可以映射相应的java对象

需要注意的是OGNL的过滤和投影

  • 过滤指的是将原集合中不符合条件的对象过滤掉,然后将满足条件的对象,构建一个新的集合对象返回,Ognl过滤表达式的写法是:collection.{?|^|$ expression};

  • 投影指的是将原集合中所有对象的某个属性抽取出来,单独构成一个新的集合对象返回,基础语法为 :collection.{expression};

Struts2和OGNL

Struts 2中的OGNL Context实现者为ActionContext,它结构示意图如下:
《Struts2之-OGNL和ActionContext原理详解》

当Struts2接受一个请求时,会迅速创建ActionContext,ValueStack,action 。然后把action存放进ValueStack,所以action的实例变量可以被OGNL访问。访问上下文(Context)中的对象需要使用#符号标注命名空间,如#application、#session,另外OGNL会设定一个根对象(root对象),在Struts2中根对象就是ValueStack(值栈) 。如果要访问根对象(即ValueStack)中对象的属性,则可以省略#命名空间,直接访问该对象的属性即可。

由于ValueStack(值栈)是Struts 2中OGNL的根对象,如果用户需要访问值栈中的对象,在JSP页面可以直接通过下面的EL表达式访问ValueStack(值栈)中对象的属性: ${foo} //获得值栈中某个对象的foo属性。如果访问其他Context中的对象,由于他们不是根对象,所以在访问时,需要添加#前缀。

  • application对象:用于访问ServletContext,例如#application.userName或者#application[‘userName’],相当于调用ServletContext的getAttribute(“username”)。
  • session对象:用来访问HttpSession,例如#session.userName或者#session[‘userName’],相当于调用session.getAttribute(“userName”)。
  • request对象:用来访问HttpServletRequest属性(attribute)的Map,例如#request.userName或者#request[‘userName’],相当于调用request.getAttribute(“userName”)。
  • parameters对象:用于访问HTTP的请求参数,例如#parameters.userName或者#parameters[‘userName’],相当于调用request.getParameter(“username”)。
  • attr对象:用于按page->request->session->application顺序访问其属性。

ActionContext和OGNL关系

Strut2的Action类通过属性可以获得所有相关的值,如请求参数属性值等。要获得这些参数值,我们要做的唯一一件事就是在Action类中声明与参数同名的属性。在Struts2调用Action类的Action方法(默认是execute方法)之前,就会为相应的Action属性赋值。要完成这个功能,有很大程度上,Struts2要依赖于ValueStack对象。这个对象贯穿整个Action的生命周期,每个Action类的对象实例会拥有一个ValueStack对象。

当Struts2接收到一个.action的请求后,会先建立Action类的对象实例,但并不会调用Action方法,而是先将Action类的相应属性放到ValueStack对象的顶层节点(ValueStack对象相当于一个栈)。只是所有的属性值都是默认的值,如String类型的属性值为null,int类型的属性值为0等。在处理完上述工作后,Struts 2就会调用拦截器链中的拦截器,这些拦截器会根据用户请求参数值去更新ValueStack对象顶层节点的相应属性的值,最后会传到Action对象,并将ValueStack对象中的属性值,赋给Action类的相应属性。当调用完所有的拦截器后,才会调用Action类的Action方法。ValueStack会在请求开始时被创建,请求结束时消亡。

也就是说valuestack 是与Action类一一对应的

《Struts2之-OGNL和ActionContext原理详解》

1. Struts2中将ActionContext作为OGNL的上下文环境(ActionContext内部含有一个Map对象)
2. Struts2中的OGNL表达式语言的根对象是一个ValueStack,ValueStack中的每一个对象都被视为根对象。

Struts2框架将实例化的Action对象放入ValueStack中,如果是Action链,则多个Action都存在于ValueStack中。而ValueStack中除了Action外,Struts2框架还将parameters,request,response,session,application,attr等对象放到ActionContext中,访问这些对象需要加前缀#。

Struts2中EL的查找域为:page(PageContext)–>request–>actionContext(contextMap)–>ValueStack–>session–>application

OGNL的查找域为:page(PageContext)–>ValueStack–>actionContext(contextMap)–>request–>session–>application

如何获得ActionContext?

1.在自定义的拦截器中:使用ActionInvocation.getInvocationContext()或者使用ActionContext.getContext()。
  
2.在Action类中:让拦截器注入或者使用ActionContext.getContext()。
  
3.在非Action类中:让Action类传递参数、使用注入机制注入或者使用ActionContext.getContext()。
  
注意:只有运行在request线程中的代码才能调用ActionContext.getContext(),否则返回的是null。

获取值栈的方法

方法1:

ValueStack valueStack1 = (ValueStack) ServletActionContext.getRequest().getAttribute("struts.valueStack");

方法2:

//获取值栈的第二种方式
 ValueStack valueStack2 = ActionContext.getContext().getValueStack();

分析源码:

ActionContext源码:

public static final String VALUE_STACK = ValueStack.VALUE_STACK;
public ActionContext(Map<String, Object> context) {
        this.context = context;
    }
 public void setValueStack(ValueStack stack) {
        put(VALUE_STACK, stack);
    }
 public ValueStack getValueStack() {
        return (ValueStack) get(VALUE_STACK);
    }
public void setApplication(Map<String, Object> application) {
        put(APPLICATION, application);
    }
public Map<String, Object> getApplication() {
        return (Map<String, Object>) get(APPLICATION);
    }
public void put(String key, Object value) {
        context.put(key, value);
}
...
其他都是类似的

org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter中的doFilter方法创建ActionContext并且创造了ValueStack

public ActionContext createActionContext(HttpServletRequest request, HttpServletResponse response) {
        ActionContext ctx;
        Integer counter = 1;
        Integer oldCounter = (Integer) request.getAttribute(CLEANUP_RECURSION_COUNTER);
        if (oldCounter != null) {
            counter = oldCounter + 1;
        }

        ActionContext oldContext = ActionContext.getContext();
        旧的ActionContext存在时,直接获取赋值
        if (oldContext != null) {
            // detected existing context, so we are probably in a forward
            ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
        } else {
        否则通过ValueStack的getContext方法获取Map来构造新的ActionContext
            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
            stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));
            ctx = new ActionContext(stack.getContext());
        }
        request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
        ActionContext.setContext(ctx);
        return ctx;
    }

参考

细谈struts2(十)ognl概念和原理详解

OGNL表达式语言详解

点赞

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注