Struts2 ValueStack & ActionContext & OGNL 关系

本文详细介绍了OGNL(Object-Graph Navigation Language)作为Web开发中View与Model之间的胶水语言的作用,包括其基本概念、使用方法以及在不同场景下的应用案例。文章通过代码示例展示了如何利用OGNL简化表达式语言来处理对象属性,实现数据绑定、数据映射和复杂表达式查询等功能,特别强调了OGNL在解决实际Web开发问题中的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 1.原生的OGNL

 1) Introduction

  OGNL stands for Object-Graph Navigation Language; it is an expression language for getting and setting properties of Java objects, plus other extras such as list projection and selection and lambda expressions. You use the same expression for both getting and setting the value of a property.

  We pronounce OGNL as a word, like the last syllables of a drunken pronunciation of "orthogonal".

  Many people have asked exactly what OGNL is good for. Several of the uses to which OGNL has been applied are:

  • A binding language between GUI elements (textfield, combobox, etc.) to model objects. Transformations are made easier by OGNL's TypeConverter mechanism to convert values from one type to another (String to numeric types, for example);
  • A data source language to map between table columns and a Swing TableModel;
  • A binding language between web components and the underlying model objects;
  • A more expressive replacement for the property-getting language used by the Apache Commons BeanUtils package or JSTL's EL (which only allow simple property navigation and rudimentary indexed properties).

  OGNL常被用来做为View与Model之间的胶水语言,在View中方便的使用Model对象,OGNL负责收集这些Model对象,在View中使用简单的OGNL表达式,就可以取到想要的对象。既消除了在View中直接使用getters 和 setters的重复代码的书写,还避免了在View代码中写的强制类型转换(Cast)。

  表达式语言(EL)本质上被设计为:帮助你使用简单的表达式来完成一些“常用”的工作。通常情况下,ELs 可以在一些框架中找到,它被是用来简化我们的工作。例如:大家熟知的 Hibernate,使用 HQL(Hibernate Query Language) 来完成数据库的操作,HQL 成了开发人员与复查的 SQL 表达式之间的一个桥梁。 在 web 框架下,表达式语言起到了相似的目的。它的存在消除了重复代码的书写。

 2) Use OGNL 

static ObjectgetValue(Object tree, Map context, Object root) 
          Evaluates the given OGNL expression tree to extract a value from the given root object.
static ObjectgetValue(Object tree, Map context, Object root, Class resultType) 
          Evaluates the given OGNL expression tree to extract a value from the given root object.
static ObjectgetValue(Object tree, Object root) 
          Evaluates the given OGNL expression tree to extract a value from the given root object.
static ObjectgetValue(Object tree, Object root, Class resultType) 
          Evaluates the given OGNL expression tree to extract a value from the given root object.
static ObjectgetValue(String expression, Map context, Object root) 
          Evaluates the given OGNL expression to extract a value from the given root object in a given context
static ObjectgetValue(String expression, Map context, Object root, Class resultType) 
          Evaluates the given OGNL expression to extract a value from the given root object in a given context
static ObjectgetValue(String expression, Object root) 
          Convenience method that combines calls to parseExpression and getValue.
static ObjectgetValue(String expression, Object root, Class resultType) 
          Convenience method that combines calls to parseExpression and getValue.

  使用OGNL非常简单,我们只用到了Ognl.jar文件,并且只使用了其中的一个类ognl.Ognl。这个类中的方法都是静态static方法。取值只用到了getValue方法,这个方法是个重载overload方法。

  OGNL三要素:

  1.expression 求值表达式——首先会被解析成对象树

  2.root object 根对象——默认的操作对象

  3.context OGNL执行环境——OGNL执行的上下文环境

  OGNL context是一个Map结构,ognl.OgnlContext类implements Map接口,root对象也在context里面,并且做这一个特殊的对象处理,具体表现为对root  对象的操作不需要加#指示符号(并且加上了#一定取不到root对象里面的值)。

  看一个例子:

 Person类:

复制代码
 1 package com.cuillgln.ognl;
 2 
 3 public class Person {
 4     private String name;
 5     private int age;
 6     private String telphone;
 7 
 8     public Person() {
 9 
10     }
11 
12     public Person(String name, int age, String telphone) {
13         this.name = name;
14         this.age = age;
15         this.telphone = telphone;
16     }
17 
18     public String getName() {
19         return name;
20     }
21 
22     public void setName(String name) {
23         this.name = name;
24     }
25 
26     public int getAge() {
27         return age;
28     }
29 
30     public void setAge(int age) {
31         this.age = age;
32     }
33 
34     public String getTelphone() {
35         return telphone;
36     }
37 
38     public void setTelphone(String telphone) {
39         this.telphone = telphone;
40     }
41 
42 }
复制代码

 OgnlTest类:

复制代码
  1 package com.cuillgln.ognl;
  2 
  3 import ognl.Ognl;
  4 import ognl.OgnlContext;
  5 import ognl.OgnlException;
  6 
  7 public class OgnlTest {
  8 
  9     public static void main(String[] args) {
 10         try {
 11             OgnlContext context = new OgnlContext();
 12             context.put("name", "STRING1~~~");
 13             context.put("age", 100);
 14             context.put("telphone", "STRING3~~~");
 15             context.put("person.name", "STRING4~~~");
 16 
 17             Person p1 = new Person("Lee", 12, "134xxxxxxxx");
 18             Person p2 = new Person("Zhang", 15, "150xxxxxxxx");
 19             Person p3 = new Person("Wang", 18, "186xxxxxxxx");
 20             
 21             context.put("person1", p1);
 22             context.put("person2", p2);
 23             context.put("person3", p3);
 24             
 25             Person root = new Person("ROOT", 100, "xxxxxxxx");
 26 
 27             /*
 28              * the output is :
 29              * the value is: ROOT======
 30              * obviously, value是对应root object 的name属性的对象。
 31              * 输出的是这个对象的值
 32              */
 33             Object value = Ognl.getValue("name", context, root);
 34             System.out.println("the value is: " + value + "======");
 35             
 36             /*
 37              * the output is :
 38              * the value is: STRING1~~~======
 39              * obviously, value是context中,Key为name的对象
 40              * 输出的是这个对象的值
 41              */
 42             value = Ognl.getValue("#name", context, root);
 43             System.out.println("the value is: " + value + "======");
 44             
 45             /*
 46              * the output is :
 47              * ognl.NoSuchPropertyException: com.cuillgln.ognl.Person.person1
 48              * obviously, OGNL会试图去找root object中属性为person1的对象,
 49              * 结果没有找到,所以抛出ognl.NoSuchePropertyException
 50              */
 51             value = Ognl.getValue("person1", context, root);
 52             System.out.println("the value is: " + value + "======");
 53             
 54             /*
 55              * the output is :
 56              * the value is: com.cuillgln.ognl.Person@7d8483======
 57              * obviously, value是context中,Key为person1的对象。
 58              * 输出的结果是这个对象的toString的返回值
 59              */
 60             value = Ognl.getValue("#person1", context, root);
 61             System.out.println("the value is: " + value + "======");
 62             
 63             /*
 64              * the output is :
 65              * ognl.NoSuchPropertyException: com.cuillgln.ognl.Person.person1
 66              * obviously, OGNL会试图去找root object中属性为person1的对象,然后再找属性person1对应对象的name属性对应的对象
 67              * 结果没有找到root object中有person1属性对应的对象,
 68              * 所以抛出ognl.NoSuchePropertyException: com.cuillgln.ognl.Person.person1
 69              */
 70             value = Ognl.getValue("person1.name", context, root);
 71             System.out.println("the value is: " + value + "======");
 72             
 73             /*
 74              * the output is :
 75              * the value is: Lee======
 76              * obviously, OGNL会试图去找context中Key为person1对应的对象,再对这个对象取name属性对应的对象
 77              * 结果找到了正确输出了……。Lee
 78              */
 79             value = Ognl.getValue("#person1.name", context, root);
 80             System.out.println("the value is: " + value + "======");
 81             
 82             /*
 83              * the output is :
 84              * ognl.OgnlException: source is null for getProperty(null, "name")
 85              * obviously, OGNL会试图去找context中Key为person对应的对象,再对这个对象取name属性对应的对象
 86              * 结果在context中没有找到Key为person的属性,故返回null,在null上再取name属性,明显会出错。
 87              */
 88             value = Ognl.getValue("#person.name", context, root);
 89             System.out.println("the value is: " + value + "======");
 90             
 91             /*
 92              * the output is :
 93              * ognl.NoSuchPropertyException: java.lang.String.name
 94              * obviously, OGNL会试图在root object中查找name属性对应的对象,
 95              * 现在root object是一个String字符串,且没有name属性,故会抛出ognl.NoSuchPropertyException
 96              */
 97             value = Ognl.getValue("name", context, "abcdefg");
 98             System.out.println("the value is: " + value + "======");
 99             
100             /*
101              * the output is :
102              * the value is: STRING1~~~======
103              * obviously, value是context中,Key为name的对象,跟root object没有关系
104              * 输出的是这个对象的值
105              */
106             value = Ognl.getValue("#name", context, "abcdefg");
107             System.out.println("the value is: " + value + "======");
108             
109         } catch (OgnlException e) {
110             // TODO Auto-generated catch block
111             e.printStackTrace();
112         }
113     }
114
复制代码

   这些就是OGNL的基础知识,OGNL另外还有很强大的功能,包括投影、支持lambda表达式,暂不做介绍。

 2.XWork-specific OGNL features

  1)There can be many "root" objects. XWork中的表示根对象是CompoundRoot对象。CompoundRoot类extends ArrayList类。因为是一个List,里面可以放置多个对象,而这些对象经过XWork的改进对于OGNL表达式引擎来说都是root objects。XWork has a special OGNL PropertyAccessor that will automatically look at the all entries in the stack (in fact the CompoundRoot list) (from the top down) until it finds an object with the property you are looking for.

  2)Struts2 Named Objects

  Struts 2 places request parameters and request, session, and application attributes on the OGNL stack (in fact the OGNL context). They may be accessed as shown below.

namevalue
#parameters['foo'] or #parameters.foorequest parameter ['foo'] (request.getParameter())
#request['foo'] or #request.foorequest attribute ['foo'] (request.getAttribute())
#session['foo'] or #session.foosession attribute 'foo'
#application['foo'] or #application.fooServletContext attributes 'foo'
#attr['foo'] or #attr.fooAccess to PageContext if available, otherwise searches request/session/application respectively

 以OGNL表达式的观点来对上述两点做出解释:

  1)的意思是:OGNL中的root object,现在是CompoundRoot对象,CompoundRoot对象并不直接在里面放置属性(指JavaBean中的Field),而是一些对象列表。如果要用原生的OGNL表达式获取里面某个对象的值,会出现什么状况?你会发现根本取不到里面的对象。如下面的代码抛出的异常: ognl.NoSuchPropertyException: java.util.ArrayList.person。所以XWork在使用OGNL前做了一些特殊处理,具体就是使用了一个特殊的PropertyAccessor来访问CompoundRoot里面的对象。(具体没有研究……)

复制代码
 1             Person p1 = new Person("Lee", 12, "134xxxxxxxx");
 2             Person p2 = new Person("Zhang", 15, "150xxxxxxxx");
 3             Person p3 = new Person("Wang", 18, "186xxxxxxxx");
 4 
 5             context.put("person1", p1);
 6             context.put("person2", p2);
 7             context.put("person3", p3);
 8 
 9             List<Object> root = new ArrayList<Object>();
10             root.add(p3);
11             root.add(p2);
12             root.add(p1);
13             /*
14              * the output is : 
15              * ognl.NoSuchPropertyException: java.util.ArrayList.person
16              * 你会发现很难用表达式,表达出来你要查找的对象(导航到你要找的对象)————无解……
17              */
18             Object value = Ognl.getValue("person.name", context, root);
19             System.out.println("the value is: " + value + "======");
复制代码

   2)的意思是:Struts把跟Web相关的对象,具体就是request parameters, request attributes, session attributes, application attributes。放在OGNL的context中,分别对应parameters, request, session, application的Key。attributes都以Key/Value的形式存在的,所以在OGNL context里key等于parameters, request, session, application的Value对应的对象类型是一个Map类。

 3.OgnlValueStack的结构

  OgnlValueStack类implements ValueStack接口。里面有两个Field: context和root,分别是OGNL的两大要素OGNL context 和root object。

复制代码
public class OgnlValueStack implements Serializable, ValueStack, ClearableValueStack, MemberAccessValueStack {
    CompoundRoot root;
    transient Map<String, Object> context;
    Class defaultType;
   transient OgnlUtil ognlUtil;
   protected void setRoot(XWorkConverter xworkConverter, CompoundRootAccessor accessor, CompoundRoot compoundRoot,
                           boolean allowStaticMethodAccess) {
        this.root = compoundRoot;
        this.securityMemberAccess = new SecurityMemberAccess(allowStaticMethodAccess);
        this.context = Ognl.createDefaultContext(this.root, accessor, new OgnlTypeConverterWrapper(xworkConverter), securityMemberAccess);
        context.put(VALUE_STACK, this);
        Ognl.setClassResolver(context, accessor);
        ((OgnlContext) context).setTraceEvaluations(false);
        ((OgnlContext) context).setKeepLastEvaluation(false);
    }
/**
     * @see com.opensymphony.xwork2.util.ValueStack#getContext()
     */
    public Map<String, Object> getContext() {
        return context;
    }
/**
     * @see com.opensymphony.xwork2.util.ValueStack#getRoot()
     */
    public CompoundRoot getRoot() {
        return root;
    }
/**
     * @see com.opensymphony.xwork2.util.ValueStack#findString(java.lang.String)
     */
    public String findString(String expr) {
        return (String) findValue(expr, String.class);
    }

    public String findString(String expr, boolean throwExceptionOnFailure) {
        return (String) findValue(expr, String.class, throwExceptionOnFailure);
    }

    /**
     * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String)
     */
    public Object findValue(String expr, boolean throwExceptionOnFailure) {
        try {
            setupExceptionOnFailure(throwExceptionOnFailure);
            return tryFindValueWhenExpressionIsNotNull(expr);
        } catch (OgnlException e) {
            return handleOgnlException(expr, throwExceptionOnFailure, e);
        } catch (Exception e) {
            return handleOtherException(expr, throwExceptionOnFailure, e);
        } finally {
            ReflectionContextState.clear(context);
        }
    }

private Object tryFindValueWhenExpressionIsNotNull(String expr) throws OgnlException {
        if (expr == null) {
            return null;
        }
        return tryFindValue(expr);
    }

private Object tryFindValue(String expr) throws OgnlException {
        Object value;
        expr = lookupForOverrides(expr);
        if (defaultType != null) {
            value = findValue(expr, defaultType);
        } else {
            value = getValueUsingOgnl(expr);
            if (value == null) {
                value = findInContext(expr);
            }
        }
        return value;
    }

private Object getValueUsingOgnl(String expr) throws OgnlException {
        try {
            return ognlUtil.getValue(expr, context, root);
        } finally {
            context.remove(THROW_EXCEPTION_ON_FAILURE);
        }
    }

    public Object findValue(String expr) {
        return findValue(expr, false);
    }

    /**
     * @see com.opensymphony.xwork2.util.ValueStack#findValue(java.lang.String, java.lang.Class)
     */
    public Object findValue(String expr, Class asType, boolean throwExceptionOnFailure) {
        try {
            setupExceptionOnFailure(throwExceptionOnFailure);
            return tryFindValueWhenExpressionIsNotNull(expr, asType);
        } catch (OgnlException e) {
            return handleOgnlException(expr, throwExceptionOnFailure, e);
        } catch (Exception e) {
            return handleOtherException(expr, throwExceptionOnFailure, e);
        } finally {
            ReflectionContextState.clear(context);
        }
    }

    private Object tryFindValueWhenExpressionIsNotNull(String expr, Class asType) throws OgnlException {
        if (expr == null) {
            return null;
        }
        return tryFindValue(expr, asType);
    }

    private Object handleOgnlException(String expr, boolean throwExceptionOnFailure, OgnlException e) {
        Object ret = findInContext(expr);
        if (ret == null) {
            if (shouldLogNoSuchPropertyWarning(e)) {
                LOG.warn("Could not find property [" + ((NoSuchPropertyException) e).getName() + "]");
            }
            if (throwExceptionOnFailure) {
                throw new XWorkException(e);
            }
        }
        return ret;
    }
private Object tryFindValue(String expr, Class asType) throws OgnlException {
        Object value = null;
        try {
            expr = lookupForOverrides(expr);
            value = getValue(expr, asType);
            if (value == null) {
                value = findInContext(expr);
            }
        } finally {
            context.remove(THROW_EXCEPTION_ON_FAILURE);
        }
        return value;
    }

private Object findInContext(String name) {
        return getContext().get(name);
    }

    public Object findValue(String expr, Class asType) {
        return findValue(expr, asType, false);
    }

  private Object getValue(String expr, Class asType) throws OgnlException {
    return ognlUtil.getValue(expr, context, root, asType);
  }

/**
     * @see com.opensymphony.xwork2.util.ValueStack#peek()
     */
    public Object peek() {
        return root.peek();
    }

    /**
     * @see com.opensymphony.xwork2.util.ValueStack#pop()
     */
    public Object pop() {
        return root.pop();
    }

    /**
     * @see com.opensymphony.xwork2.util.ValueStack#push(java.lang.Object)
     */
    public void push(Object o) {
        root.push(o);
    }

  public void set(String key, Object o) {
    //set basically is backed by a Map pushed on the stack with a key being put on the map and the Object being the value
    Map setMap = retrieveSetMap();
    setMap.put(key, o);
  }

 
   

  private Map retrieveSetMap() {     Map setMap;     Object topObj = peek();     if (shouldUseOldMap(topObj)) {       setMap = (Map) topObj;     } else {       setMap = new HashMap();       setMap.put(MAP_IDENTIFIER_KEY, "");       push(setMap);//放在了CompoundRoot的顶部     }     return setMap;   }

  /**    * check if this is a Map put on the stack for setting if so just use the old map (reduces waste)    */   private boolean shouldUseOldMap(Object topObj) {     return topObj instanceof Map && ((Map) topObj).get(MAP_IDENTIFIER_KEY) != null;   }

}
复制代码

  从源码中可以看出:

  1)对ValueStack的栈操作(即peek、pop、push)实际上是对root上的操作。————这就是为什么说有时候ValueStack特指Object Stack。这里的Object Stack即是CompoundRoot对象,它提供了对栈的操作接口。

  2)findValue的过程:经过一系列的方法,最后调用tryFindValue方法,tryFindValue首先调用getValue,getValue调用OgnlUtil.getValue,OgnlUtil.getValue调用原生OGNL的Ognl.getValue方法。如果得到的value是null,会再调用findInContext方法,findInContext返回getContext().get(name),这是试图从context里获得Key为name的Value值。这是的name可能很复杂,如"person.name",此时不会再被解释成“先取得属性名为person的对象,再取这个对象的name属性对应的对象”,而是直接做为Map.get()里的一个Key。

  3)ValueStack.set(String key, Object value);确实是在CompoundRoot栈顶压入了一个HashMap,里面放入了set的Key/value pairs。

4.ActionContext类结构及初始化过程

  ActionContext里只有一个Filed,即context。从FilterDispacher的doFilter方法中,可以看出,ActionContext中的context被初始化为ValueStack中context的内容。并且在后面又在context中添加了一些key/value pairs。主要是把从ValueStack的context中的request parameters, request attributes, session attributes, application attributes,又以不同的Key放在了context中。并且还把整个ValueStack对象也放到了context中,Key是 "com.opensymphony.xwork2.util.ValueStack.ValueStack"。并且提供了这些对象的setters/getters方法。

复制代码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
                 throws IOException, ServletException { 
     HttpServletRequest request = (HttpServletRequest) req; 
     HttpServletResponse response = (HttpServletResponse) res; 
     ServletContext servletContext = getServletContext(); 

     String timerKey = "FilterDispatcher_doFilter: "; 
     try { 
          //1 处理前的准备
          //1.1 创建值栈对象,值栈包含 object stack 和 context map 两个部分。
      ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); 
          //1.2 创建 actionContext。
         ActionContext ctx = new ActionContext(stack.getContext()); 
         ActionContext.setContext(ctx); 

// 省略剩余代码
复制代码

5.use Struts tags with OGNL expression

  自定义标签(Customer Tags)一般都继承自javax.servlet.jsp.tagext.BodyTagSupport类,重写(overriding)doStartTag()和doEndTag()方法。

  对于像<s:set var="test" value="%{'abc'}" scope="scope"/> assigns a value to a variable in a specified scope。根据源代码可以看出来实际上是把var定义的key放到了context中相应的scope下的Map中或context中。调用的是ValueStack.setValue(expr, Object)方法,而不是ValueStac.set(key, value)方法。两方法的区别:

  1)ValueStac.set(key, value)是在CompoundRoot栈顶压入了一个HashMap,里面放入了set的Key/value pairs。

  2)ValueStack.setValue(expr, object)是给根据expr查找到的对象赋与object值。

  这就可以理解为什么在用<s:set/>绑定一个变量后,一般还是要加#才能取到值的原因了。因为他们的key/value pairs放置在context中,而不是CompoundRoot中。

  还要理解为什么有一部分变量就不用加#也可以取到。不加#可以取到条件有(三个条件,缺一不可):

    1) <s:set scope="action"/>中scope的值是默认值,即action作用域。

    2)<s:set value=""/>中value是个OGNL constants. 即String, 各种Number, Boolean, null等。

    3)在CompoundRoot对象中没有名为此key的属性。

  为什么能取到:

    1)scope="action"是把key/value pairs直接ValueStack.getContext().put(key, value)的。是存放在OGNL context中的。

    2)按原生的OGNL,不加#号是不可能取到的。

    3)Struts中的OgnlValueStack的tryFindValue()在用原生的OGNL getValue()得到的是null后(说明在CompoundRoot中没有与此key同名的属性),会调用findInContext(key)方法return getContext.get(key)。如此,如果在context中有此key,便可不用#也可能取到想要的值。

复制代码
 1 public boolean end(Writer writer, String body) {
 2         ValueStack stack = getStack();
 3 
 4         Object o;
 5         if (value == null) {
 6             if (body != null && !body.equals("")) {
 7                 o = body;
 8             } else {
 9                 o = findValue("top");
10             }
11         } else {
12             o = findValue(value);
13         }
14 
15         body="";
16 
17         if ("application".equalsIgnoreCase(scope)) {
18             stack.setValue("#application['" + getVar() + "']", o);
19         } else if ("session".equalsIgnoreCase(scope)) {
20             stack.setValue("#session['" + getVar() + "']", o);
21         } else if ("request".equalsIgnoreCase(scope)) {
22             stack.setValue("#request['" + getVar() + "']", o);
23         } else if ("page".equalsIgnoreCase(scope)) {
24             stack.setValue("#attr['" + getVar() + "']", o, false);
25         } else {
26             stack.getContext().put(getVar(), o);
27             stack.setValue("#attr['" + getVar() + "']", o, false);
28         }
29 
30         return super.end(writer, body);
31     }
标题基于SpringBoot+Vue的学生交流互助平台研究AI更换标题第1章引言介绍学生交流互助平台的研究背景、意义、现状、方法与创新点。1.1研究背景与意义分析学生交流互助平台在当前教育环境下的需求及其重要性。1.2国内外研究现状综述国内外在学生交流互助平台方面的研究进展与实践应用。1.3研究方法与创新点概述本研究采用的方法论、技术路线及预期的创新成果。第2章相关理论阐述SpringBoot与Vue框架的理论基础及在学生交流互助平台中的应用。2.1SpringBoot框架概述介绍SpringBoot框架的核心思想、特点及优势。2.2Vue框架概述阐述Vue框架的基本原理、组件化开发思想及与前端的交互机制。2.3SpringBoot与Vue的整合应用探讨SpringBoot与Vue在学生交流互助平台中的整合方式及优势。第3章平台需求分析深入分析学生交流互助平台的功能需求、非功能需求及用户体验要求。3.1功能需求分析详细阐述平台的各项功能需求,如用户管理、信息交流、互助学习等。3.2非功能需求分析对平台的性能、安全性、可扩展性等非功能需求进行分析。3.3用户体验要求从用户角度出发,提出平台在易用性、美观性等方面的要求。第4章平台设计与实现具体描述学生交流互助平台的架构设计、功能实现及前后端交互细节。4.1平台架构设计给出平台的整体架构设计,包括前后端分离、微服务架构等思想的应用。4.2功能模块实现详细阐述各个功能模块的实现过程,如用户登录注册、信息发布与查看、在线交流等。4.3前后端交互细节介绍前后端数据交互的方式、接口设计及数据传输过程中的安全问题。第5章平台测试与优化对平台进行全面的测试,发现并解决潜在问题,同时进行优化以提高性能。5.1测试环境与方案介绍测试环境的搭建及所采用的测试方案,包括单元测试、集成测试等。5.2测试结果分析对测试结果进行详细分析,找出问题的根源并
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值