• Struts2


    Struts2对应MVC的C。控制层

    一、Struts1  VS  Struts2

      1.控制器:Struts2使用一个Filter作为控制器,StrutsPrepareAndExecuteFilter

           Struts1使用ActionServlet作为控制器

      2.表单映射:Struts2表单直接被映射到POJO

            Struts1每一个表单都对应一个ActionForm实例

      3.Action类:Struts2任何一个POJO都可以是一个Action类

            Struts1中Action类必须继承org.apache.struts.action.Action

      4.前端页面:Struts2可以使用OGNL表达式

      5.验证逻辑:Struts2的验证逻辑卸载Action类中

            Struts1的验证逻辑必须写在ActionForm的实现类中

    二、HelloWorld

    1.web.xml

    配置Filter,StrutsPrepareAndExecuteFilter,拦截所有请求

        <filter>
            <filter-name>struts2</filter-name>
            <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
        </filter>
    
        <filter-mapping>
            <filter-name>struts2</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>

     

    2.struts.xml(classpath下)

    <struts>
      <constant name="struts.action.extension" value="action, ,do"></constant> 可以处理的action请求的扩展名 <package name="helloword" extends="struts-default"> <action name="information-save" class="com.helloworld.Information" method="save"> <result name="details">/jsp/details.jsp</result> </action> </package> </struts>

    3.Action类

    public class Information {
        private Integer id;
        private String infoName;
        private String infoDesc;
        
        @Override
        public String toString() {
            return "Information [id=" + id + ", infoName=" + infoName
                    + ", infoDesc=" + infoDesc + "]";
        }
        public Information() {
            super();
        }
        public Information(String infoName, String infoDesc) {
            super();
            this.infoName = infoName;
            this.infoDesc = infoDesc;
        }
        
        public String save() {
            System.out.println("save():"+this);return "details";
        }
        
    
    }

    4.jsp

    <form action="information-save.action" method="post">
      informationName:<input type="text" name="infoName"/><br>
      informationDesc:<input type="text" name="infoDesc"/><br>
    <input type="submit" value="提交" />

    三、Action类

    (一)Action类的特点

    Action类可以是任何一个POJO

    必须有一个空参的构造器

    Form表单提交数据之后,表单的属性映射到Action类的相应属性上,因此,属性必须一一对应,可以是任意类型,基本类型和字符串之间可以自动类型转换,其它要使用类型转换器(后面讲)

    必须有一个可以供Struts2 在执行这和action时可调用的方法(这个方法在struts.xml中配置),用于应答action请求,这个方法必须返回一个String,在struts.xml的配置中,根据该返回值返回对应的jsp

    一个Action类中,可以都多个action方法

    Struts2为每一个action请求创建一个Action实例,因此,Action在多线程下是安全的(由于Action不是单例的,所以,在SSH整合的时候,将Action实例交由Spring IOC容器创建,在Spring配置文件中配置bean时,bean的作用范围必须指定为原型的,默认是单例的)

    (二)在Action中访问web资源(Struts2访问Servlet)

    HttpSession   HttpServletRequest HttpServletResponse 

    1.与Servlet API 耦合的方式

    能访问到更多的原生方法,通过ServletActionContext对象 或者 实现XxxAware接口(servletRequestAware ServletResponseAware ServletContextAware)

     (1)ServletActionContext对象

    如何获取session:

    在获取request之后,调用request.getSession()

        public static ServletContext getServletContext() {
            return (ServletContext) ActionContext.getContext().get(SERVLET_CONTEXT);
        }
        public static HttpServletResponse getResponse() {
            return (HttpServletResponse) ActionContext.getContext().get(HTTP_RESPONSE);
        }
        public static HttpServletRequest getRequest() {
            return (HttpServletRequest) ActionContext.getContext().get(HTTP_REQUEST);
        }

    2.与Servlet API 解耦的方式

    只能访问到较少的方法,通过ActionContext对象 或者 实现XxxAware接口(ApplicationAware  RequestAware  SessionAware  ParameterAware)

    (1)ActionContext对象

    Action的上下文对象,类似于PageContext ServletContext

    ActionContext  context = ActionContext.getContext();

    调用ActionContext对象的getXxx方法,获取对应的Map。Struts2构造了三个Map对象,用于封装Application(ServletContext)、session、parameter

      如何获取Request,调用get("request")

    public Object get(String key) {
           return context.get(key);
    }

        public Map<String, Object> getApplication() {
            return (Map<String, Object>) get(APPLICATION);
        }
        public Map<String, Object> getParameters() {
            return (Map<String, Object>) get(PARAMETERS);
        }
        public Map<String, Object> getSession() {
            return (Map<String, Object>) get(SESSION);
        }

     (2)XxxAware接口

    public class TestAwareAction implements ApplicationAware{
    
        public String execute(){
            applicationMap.put("Aware_application", "Aware_application_value");
            System.out.println(applicationMap.get("dateJsp"));
            return "success";
        }
    
        private Map<String, Object> applicationMap;
        @Override
        public void setApplication(Map<String, Object> applicationMap) {
            this.applicationMap = applicationMap;
        }
    }

    四、ActionContext类

    Action的上下文对象,保存了一个Action在执行过程中所必须的对象,session, parameters, locale等,Struts2会根据每个执行HTTP请求的下城创建一个对应的ActionContext对象,每一个线程对应一个ActionContext对象

    获取每一个线程对应的ActionContext对象-----------ActionContext context = ActionContext.getContext();(ActionContext对象有一个静态成员变量ThreadLocal,在getContext方法中调用ThreadLocal对象的get方法,获取当前线程的ActionContext对象,不用担心线程安全问题)

    Action是线程安全的,原因---------ActionContext对象的成员变量:static ThreadLocal actionContext = new ThreadLocal();

    ActionContext是用来存放Action在执行过程中所必须的对象,有struts2本身存放的数据,也可以在程序中向其中存放数据

    实际是一个Map<String, Object>

    创建ActionContext对象的时机-----------发送请求,请求被StrutsPrepareAndExecuteFilter拦截,调用拦截器的doFilter方法,在该方法中创建ActionContext对象

    如何创建-----------调用prepare.createActionContext(request, response);     看是否已经有与当前线程绑定的ActionContext对象。有,根据之前的ActionContext对象创建一个新的ActionContext对象;没有,获取ValueStack对象,根据值栈中保存的键值对创建一个ActionContext对象。将此次创建的ActionContext对象放入ThreadLocal中。

        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();
            if (oldContext != null) { 
                // detected existing context, so we are probably in a forward
                ctx = new ActionContext(new HashMap<String, Object>(oldContext.getContextMap()));
            } else {
                ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); //创建值栈
        public ValueStack createValueStack() {
            ValueStack stack = new OgnlValueStack(xworkConverter, compoundRootAccessor, textProvider, allowStaticMethodAccess);
            container.inject(stack);
            stack.getContext().put(ActionContext.CONTAINER, container);
            return stack;
        }
                stack.getContext().putAll(dispatcher.createContextMap(request, response, null, servletContext));
                ctx = new ActionContext(stack.getContext());
            }
            request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
            ActionContext.setContext(ctx);
        public static void setContext(ActionContext context) {
            actionContext.set(context); //static ThreadLocal actionContext = new ThreadLocal();  调用ThreadLocal的set方法,将ActionContext对象放在当前线程的ThreadLocal中
        }
    
    
    return ctx;
        }

     

     五、ActionSupport类

      <default-class-ref class="com.opensymphony.xwork2.ActionSupport" />

      如果在配置<action>时,没有指定class属性的值,ActionSupport的全类名就是该属性的值(默认值),此时,execute方法即为默认要执行的action方法

    六、Result接口

     public void execute(ActionInvocation invocation) throws Exception;

    每一个action方法都有一个String返回值,根据String返回值决定响应什么结果

    <result >是<action >的子节点,可以包含多个<result>

    <result>的属性 name type

    name:结果的名字,必须和action方法的String返回值对应,默认success

    type:结果的响应类型,默认dispatcher

    <result-types>
    <result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/>    构成一个chain链,前一个action将控制权交给后一个action,通过dispatcher不能实现chain
    <result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/>  将控制权转发给当前应用程序内部的一个资源
    <result-type name="freemarker" class="org.apache.struts2.views.freemarker.FreemarkerResult"/>
    <result-type name="httpheader" class="org.apache.struts2.dispatcher.HttpHeaderResult"/>
    <result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/>    重定向 可以是外部资源
    <result-type name="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult"/>    重定向到另一个Action,通过redirect可以实现redirectChain
    <result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/>
    <result-type name="velocity" class="org.apache.struts2.dispatcher.VelocityResult"/>
    <result-type name="xslt" class="org.apache.struts2.views.xslt.XSLTResult"/>
    <result-type name="plainText" class="org.apache.struts2.dispatcher.PlainTextResult" />
    <result-type name="postback" class="org.apache.struts2.dispatcher.PostbackResult" />
    </result-types>

    七、通配符

      一个<action>可以处理多个action请求,减少<action>的配置

    如果有多个相匹配的,则没有通配符的那个胜出;如果匹配到的多个都有通配符,则写在最前面的胜出

    匹配到的URI字符串的子串可以用{1}{2}{3}……进行引用,{1}对应第一个通配符,以此类推

    *  匹配0到多个字符,不包含“/”

    若要匹配“/” 使用**

    转义使用“/”

            <action name="employee-*" class="com.duan.action.InterceptionEmployeeAction" method="{1}">
                <result name="{1}">/interception/employee-{1}.jsp</result>
         </action>

     

    八、值栈

      ValueStack接口   OgnlValueStack

    创建时机-----------在Filter的doFilter方法中,调用prepare.createActionContext(request, response);创建ActionContext对象,在该方法中,先判断是否已经存在与当前线程绑定ActionContext对象,不存在时,先创建调用ValueStackFactory的createValueStack方法创建值栈,然后根据值栈创建ActionContext对象

        public ValueStack createValueStack() {
            ValueStack stack = new OgnlValueStack(xworkConverter, compoundRootAccessor, textProvider, allowStaticMethodAccess);
            container.inject(stack);
            stack.getContext().put(ActionContext.CONTAINER, container);
            return stack;
        }

    Struts2中,在jsp页面的隐含对象request不是tomcat创建的原生的HttpServletRequest,而是Struts2包装之后的StrutsRequestWrapper,重写了getAttribute方法,先获取ActionContext对象,从ActionContext对象中获取ValueStack对象,从ValueStack对象中获取属性值

        public Object getAttribute(String s) {
            if (s != null && s.startsWith("javax.servlet")) {
                // don't bother with the standard javax.servlet attributes, we can short-circuit this
                // see WW-953 and the forums post linked in that issue for more info
                return super.getAttribute(s);
            }
    
            ActionContext ctx = ActionContext.getContext();
            Object attribute = super.getAttribute(s);
            if (ctx != null) {
                if (attribute == null) {
                    boolean alreadyIn = false;
                    Boolean b = (Boolean) ctx.get("__requestWrapper.getAttribute");
                    if (b != null) {
                        alreadyIn = b.booleanValue();
                    }
        
                    // note: we don't let # come through or else a request for
                    // #attr.foo or #request.foo could cause an endless loop
                    if (!alreadyIn && s.indexOf("#") == -1) {
                        try {
                            // If not found, then try the ValueStack
                            ctx.put("__requestWrapper.getAttribute", Boolean.TRUE);
                            ValueStack stack = ctx.getValueStack();
                            if (stack != null) {
                                attribute = stack.findValue(s);
                            }
                        } finally {
                            ctx.put("__requestWrapper.getAttribute", Boolean.FALSE);
                        }
                    }
                }
            }
            return attribute;
        }
    }

    每个Action对象都拥有一个ValueStack

    OgnlValueStack

    CompoundRoot root;   public class CompoundRoot extends ArrayList,虽然root是一个ArrayList,但是内部的pop push方法实现了栈的先进先出的功能
    transient Map<String, Object> context;

    root:栈。存储属性值。Struts2默认将Action及其相关对象放在栈顶,在调用ActionProxyFactory的createActionProxy方法时,会创建ActionInvocation对象,在调用该对象的init方法时,将Action对象放入值栈栈顶

    stack.push(action);
    contextMap.put("action", action);

    context:键值对。存储各种键值对,request、session、application、attr、parameters

     

     

    九、OGNL表达式

    在jsp页面,使用ognl表达式获取值栈中存放的数据

    1.读入root中的对象

    (1)某个对象的某个属性值

    Object.propertyName

    Object["propertyName"]

    Object['propertyName']

    (2)可以使用下标

    [0].propertyName  返回栈顶对象的属性值

    使用下标[i].propertyName,如果在下标为i的对象中没有找到指定的属性,则从i开始,往后找

    若从栈顶元素往后找某个属性的值,可以省略下标,直接指定属性值即可

    <s:property value="message"> 从栈顶元素开始找message属性的值,这是最常用的方法

    2.读取context(Map)中的属性

    加上前缀字符“#”

    (1)某个对象的某个属性值

    #Object.propertyName

    #Object["propertyName"]

    #Object['propertyName']

    (2)使用Map中键的引用

    #request  #application #session #attr

    eg:#request.code  返回request域中code属性的值

    3.调用字段和方法

    可以使用OGNL表达式调用任何一个类的静态的字段和方法,或者一个压入root中的对象的公共字段和方法

    需要修改默认配置,使得Struts2支持该功能  allowStaticMethodAccess

    eg:

    <s:property  value="@java.lang.Math@PI" >

    <s:property  value="@java.lang.Math@cos(0)" >

    <s:property value="[0].infoName">

    <s:property value="[0].setInfoName("sss")">

     

    十、exception拦截器-----ExceptionMappingInterceptor------默认拦截器栈defaultStack

      程序发生异常时,返回异常页面

    全局(全局的result属性必须引用全局<result>)

    局部

    <s:property value="exception">

    <s:property value="exception.message">

        <global-results>
            <result name="error">/error.jsp</result>
        </global-results>
        <global-exception-mappings>
            <exception-mapping result="error" exception="java.lang.ArithmeticException"></exception-mapping>
        </global-exception-mappings>
            <action name="information-save" class="com.helloworld.Information" method="save">
                <exception-mapping result="input" exception="java.lang.ArithmeticException"></exception-mapping>
                <result name="input">/jsp/input.jsp</result>
                <result name="details">/jsp/details.jsp</result>
            </action>

    十一、params拦截器-----ParametersInterceptor------默认拦截器栈defaultStack

    将表单字段映射到值栈栈顶对象的同名属性中,如果栈顶对象不存在同名属性,尝试栈中的下一个对象

    例如,删除一条记录,传递记录的id值,在Action类中创建一个同名的属性,setXxx(),即可

    通常情况下,值栈的栈顶对象都是Action类的对象,因此,会将表单字段映射到Action类的同名属性中,如果表单字段过多,就会导致Action类庞大

    例如,插入一条记录,要将记录的每一个属性值通过表单传递,此时,要在Action类中创建记录的每一个属性对应的属性及其setXxx(),将Action类和Model耦合在一起

    需要将Action和Model分开

    十二、modelDriven拦截器----ModelDrivenInterceptor------默认拦截器栈defaultStack

    使Action和Model分开

    Action类实现ModelDriven接口,modelDriven拦截器就会将getModel方法返回的那个对象置于值栈栈顶,接下来params拦截器就会将表单字段映射到值栈栈顶元素的同名属性中,如果栈顶元素不存在同名属性,则去栈中下一个对象中找

    例如,插入一个记录,创建一个记录对应的实体类,在getModel方法中返回该实体类的一个对象即可

    public interface ModelDriven<T> {
    
        /**
         * Gets the model to be pushed onto the ValueStack instead of the Action itself.
         *
         * @return the model
         */
        T getModel();
    
    }
        @Override
        public String intercept(ActionInvocation invocation) throws Exception {
            Object action = invocation.getAction();
    
            if (action instanceof ModelDriven) {    //判断当前Action类有没有实现ModelDriven接口,如果实现了,就会获取重写的抽象方法getModel的返回值,将其置于值栈栈顶
                ModelDriven modelDriven = (ModelDriven) action;
                ValueStack stack = invocation.getStack();
                Object model = modelDriven.getModel();
                if (model !=  null) {
                    stack.push(model);
                }
                if (refreshModelBeforeResult) {
                    invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
                }
            }
            return invocation.invoke();
        }

      表单回显

        在值栈栈顶有一个对象,如果该对象的属性与表单的name属性是同名属性,就会自动回显

      

    十三、paramsPrepareParamsStack拦截器栈的使用

    更新一条记录时,首先要传递记录的id,到达编辑页面,需要将记录的原始信息显示在页面上,更新之后,提交

    params拦截器 - modelDriven拦截器 - params拦截器          paramsPrepareParamsStack拦截器栈可以实现

    更换拦截器栈,struts.xml    <default-interceptor-ref name="paramsPrepareParamsStack"></default-interceptor-ref>

     

    十四、prepare拦截器----PrepareInterceptor------paramsPrepareParamsStack拦截器栈

    Action类必须实现Preparable接口,重写prepare方法

    modelDriven拦截器将一个Action类以外的一个对象压入值栈的栈顶,而prepare拦截器负责为getModel方法提供model

    通过分析prepare拦截器的执行过程,会调用两个方法 prepareXxx 和 prepareDoXxx,如果Action类中存在这两个方法,会调用,然后根据配置决定是否调用重写的prepare方法

    流程:如果Action类实现了Preparable接口,就会触发prepare拦截器的doIntercept方法,默认情况下,尝试执行prepareXxx方法,若该方法存在,执行,尝试执行prepareDoXxx方法,若该方法存在,执行;之后,根据配置决定是否执行重写的prepare方法

    为需要的action方法创建一个配对的prepareXxx方法,并设置不执行prepare方法

    如何设置拦截器的参数? 

            <interceptors>
                <interceptor-stack name="duanStack">
                    <interceptor-ref name="paramsPrepareParamsStack">
                        <param name="prepare.alwaysInvokePrepare">false</param>
                    </interceptor-ref>
                </interceptor-stack>
            </interceptors>
    
            <default-interceptor-ref name="duanStack"></default-interceptor-ref>
    public interface Preparable {
    
        /**
         * This method is called to allow the action to prepare itself.
         *
         * @throws Exception thrown if a system level exception occurs.
         */
        void prepare() throws Exception;
        
    }
        public String doIntercept(ActionInvocation invocation) throws Exception {
            Object action = invocation.getAction();
    
            if (action instanceof Preparable) {  //判断Action类是否实现了Preparable接口
                try {
                    String[] prefixes;
                    if (firstCallPrepareDo) {  //根据拦截器的firstCallPrepareDo属性的值(默认false),确定prefix(方法前缀)
      private final static String PREPARE_PREFIX = "prepare";
        private final static String ALT_PREPARE_PREFIX = "prepareDo";
                        prefixes = new String[] {ALT_PREPARE_PREFIX, PREPARE_PREFIX};
                    } else {
                        prefixes = new String[] {PREPARE_PREFIX, ALT_PREPARE_PREFIX};
                    }
                    PrefixMethodInvocationUtil.invokePrefixMethod(invocation, prefixes);    //调用前缀方法

        public static void invokePrefixMethod(ActionInvocation actionInvocation, String[] prefixes) throws InvocationTargetException, IllegalAccessException {
            Object action = actionInvocation.getAction();  //获取Action类的实例
            
            String methodName = actionInvocation.getProxy().getMethod();  //获取对应的action方法的名字
            
            if (methodName == null) {
                // if null returns (possible according to the docs), use the default execute 
                methodName = DEFAULT_INVOCATION_METHODNAME;    //如果action方法的名字是空,证明没有配置,使用默认的execute方法
            }
            
            Method method = getPrefixedMethod(prefixes, methodName, action);  //获取前缀方法

        public static Method getPrefixedMethod(String[] prefixes, String methodName, Object action) {
            assert(prefixes != null);
            String capitalizedMethodName = capitalizeMethodName(methodName);  //将目标action方法的方法名的首字母大写
            for (String prefixe : prefixes) {
                String prefixedMethodName = prefixe + capitalizedMethodName;  //这个的值会是prepareXXX   prepareDoXXX
                try {
                    return action.getClass().getMethod(prefixedMethodName, EMPTY_CLASS_ARRAY);
                }
                catch (NoSuchMethodException e) {
                    // hmm -- OK, try next prefix
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("cannot find method [" + prefixedMethodName + "] in action [" + action + "]");
                    }
                }
            }
            return null;
        }
    
    
    if (method != null) {
                method.invoke(action, new Object[0]);  //通过反射调用前缀方法
            }
        }
    
    
    
                }
                catch (InvocationTargetException e) {
                    /*
                     * The invoked method threw an exception and reflection wrapped it
                     * in an InvocationTargetException.
                     * If possible re-throw the original exception so that normal
                     * exception handling will take place.
                     */
                    Throwable cause = e.getCause();
                    if (cause instanceof Exception) {
                        throw (Exception) cause;
                    } else if(cause instanceof Error) {
                        throw (Error) cause;
                    } else {
                        /*
                         * The cause is not an Exception or Error (must be Throwable) so
                         * just re-throw the wrapped exception.
                         */
                        throw e;
                    }
                }
    
                if (alwaysInvokePrepare) {  //根据alwaysInvokePrepare属性决定是否调用Action类中重写的Preparable接口的prepare方法
                    ((Preparable) action).prepare();
                }
            }
    
            return invocation.invoke();
        }

    十五、类型转换

    (一)类型转化错误时,如何处理?
      若Action类没有实现ValidationAware接口,在类型转化错误时,struts2会继续调用action方法,将该类型转化错误的属性的属性值置为默认值,不报错。
      若Action类实现了ValidationAware接口,在类型转化错误时,struts2会检查当前<action>是否配置了<result name="input">……</result>,若配置了,将控制权交给该<result>;若没有配置,报错:No result defined for action …… and result input。

    (二)如何显示类型转化失败时的错误消息?
      若form标签使用的是默认的主题(xhtml),则自动显示错误消息,默认的错误消息是:Invalid field value for field ……
      若form标签使用的是simple主题,使用<s:fielderror>标签显示。例如:<s:fielderror fieldName="name"/>

    (三)默认的错误消息是如何显示的?
      如果当前Action类实现了ValidationAware接口,conversionError拦截器(默认拦截器栈的一员)负责添加与类型转化相关的错误消息。

    (四)如何覆盖、定制默认的错误消息
      在当前字段的model所在的包下新建一个文件,文件名:字段所在类的类名.properties
      在该新建的文件中输入键值对,如下
        invalid.fieldvalue.表单中相应字段的name属性的值=定制的错误消息

    (五)自定义类型转化器

      1.为什么要自定义?
        params拦截器只能完成基本数据类型和字符串之间的类型转化,不能完成字符串和引用类型之间的转换。
        例如:字符串和日期之间的转化
      2.如何自定义?
       (1)开发一个类,继承StrutsTypeConverter
       (2)配置(两种方式)
        ①基于字段的配置(只能处理当前字段的类型装换异常)
          在字段所在的model所在的包下新建一个文件,文件名:字段所在类的类名-conversion.properties
          在该新建文件中输入键值对,如下
            待转换的字段名=自定义类型转换器的全类名
          基于字段配置的自定义类型转化器在第一次使用时创建实例,并且仅创建一次(单例)
        ②基于类型的配置(可以处理当前类型的所有字段的转换异常)
          在类路径下新建一个文件,文件名:xwork-conversion.properties
          在该新建文件中输入键值对,如下
            带转换类型的全类名=自定义类型转换器的全类名
          基于类型配置的自定义类型转化器在当前web应用被加载时创建。

  • 相关阅读:
    node.js 安装
    spring mvc 表单标签
    配置文件 .properties 的使用。
    angular 参考文档
    bootStrap 教程 文档
    idea 安装 破解方法
    restful 注解 总结 (比较完整的):http://www.xuetimes.com/archives/388 , https://www.cnblogs.com/chen-lhx/p/5599806.html
    apo 简单参考
    jsonUtils&&Json、Xml转换工具Jackson使用
    restful 分风格
  • 原文地址:https://www.cnblogs.com/duanjiapingjy/p/7747160.html
Copyright © 2020-2023  润新知