笔记:SpringMVC

215 阅读5分钟

1. 依赖

<!--JavaEE最基本的依赖-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency>

<!--SpringMVC-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

Tomcat自带javax.servlet-apijsp-api,所以设置scope=provided即可。

image.png

依赖变灰色(红框灰色),是因为外面有(红框白色)。

2. 利用反射获取参数名

从JDK8开始,可以通过java.lang.reflect.Parameter类获取参数名;

  • 前提:在编译*.java的时候保留参数名信息到*.class中,比如javac -parameters *.java
  • 可以通过javap -v *.class查看class文件的参数名信息
  • java * 执行class文件,比如有主类,且有main函数。

编写TestParam.java放在桌面。

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class TestParam {

    public void run(String name, int age) {}

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = TestParam.class.getMethod("run", String.class, int.class);
        for (Parameter parameter : method.getParameters()) {
            System.out.println(parameter.getName());
        }
    }
    
}

注意:package com.xxx 去掉

  • javac TestParam.java编译TestParam.java文件

    1. java TestParam 运行TestParam.class (注意不能是java TestParam.class)
      查看打印结果是:
      arg0
      arg1
      获取不到参数名。
    2. javap -v TestParam.class 查看这个class的参数名信息。
      看不到参数名信息。
  • javac -parameters TestParam.java 编译TestParam.java文件

    1. java TestParam 打印结果:name age。可以获取到参数名
    2. javap -v TestParam.class 查看这个class的参数名信息。

image.png

对比javac、javac -parameters 和Maven编译的差异

项目中通过Maven编译,会添加一些其他的信息。

package com.xxx;
import org.junit.Test;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class TestParam {
    public void run(String name, int age) {}
    @Test
    public void test() throws NoSuchMethodException {
        Method method = TestParam.class.getMethod("run", String.class, int.class);
        for (Parameter parameter : method.getParameters()) {
            System.out.println(parameter.getName());
        }
    }
}

运行下test方法,找到通过Maven编译后的TestParam.class

image.png

javap -v xxx.class 对比javac、javac -parameters 和Maven编译的差异

javac

    public void run(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=3, args_size=3
         0: return
      LineNumberTable:
        line 7: 0

javac -parameters

   public void run(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=3, args_size=3
         0: return
      LineNumberTable:
        line 7: 0
    MethodParameters:
      Name                           Flags
      name
      age

Maven

public void run(java.lang.String, int);
  descriptor: (Ljava/lang/String;I)V
  flags: (0x0001) ACC_PUBLIC
  Code:
    stack=0, locals=3, args_size=3
       0: return
    LineNumberTable:
      line 10: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       1     0  this   Lcom/scc/TestParam;
          0       1     1  name   Ljava/lang/String;
          0       1     2   age   I

观察得知,通过Maven编译的,会多了局部变量表。

SpringMVC获取参数名的方法

SpringMVC内部通过PrioritizedParameterNameDiscoverer获取参数名。

@Override
@Nullable
public String[] getParameterNames(Method method) {
   for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) {
      String[] result = pnd.getParameterNames(method);
      if (result != null) {
         return result;
      }
   }
   return null;
}

SpringMVC通过多种方法获取参数名,一个parameterNameDiscoverer就是一个方案。 点击ParameterNameDiscoverer ctrl + H:

image.png

会优先使用StandardReflectionParameterNameDiscoverer反射的方式去获取。然后通过 LocalVariableTableParameterNameDiscoverer局部变量的方式去获取。

3. Tomcat内置的2个默认Servlet

可以再 TOMCAT_HOME/conf/web.xml中找到

  • org.apache.catalina.servlets.DefaultServlet url-pattern是/,可以处理静态资源
    <servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
  • org.apache.jasper.servlet.JspServlet url-pattern是*.jsp
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

不是用SPringMVC的Servlet,使用@WebServlet注解的接口,jsp、静态资源html等依然可以请求到,Tomcat内部有Servlet会拦截请求,处理。 比如请求一个html:http://localhost:8080/mvc01/test.html. Tomcat拦截请求后,会交给Tomcat内部的Servlet处理, 可能这样,内部会把静态资源转换为二进制流返回给客户端。

静态资源被拦截的解决方案

SpringMVC的Servlet的url匹配,*.do、/、/*的区别

  • *.do: 不会拦截动态资源(比如*.jsp)、静态资源(比如 .html、.js)

  • /: 会拦截静态资源(比如 .html、.js),不会拦截动态资源(比如*.jsp)

  • /*: 会拦截动态资源(比如*.jsp)、静态资源(比如 .html、.js)。 一般用于Filter中。

解决方案1

如果SpringMVC的DispatcherServlet的url-pattern设置为/,会导致静态资源拦截
解决方案:配置<mvc:default-servlet-handler/>,将静态资源交回给Tomcat的DefaultServlet去处理
原理:配置后会通过DefaultServletHttpRequestHandler对象将静态资源转发给Tomcat的DefaultServlet处理。

源码

@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {

   Assert.state(this.servletContext != null, "No ServletContext set");
   RequestDispatcher rd = this.servletContext.getNamedDispatcher(this.defaultServletName);
   if (rd == null) {
      throw new IllegalStateException("A RequestDispatcher could not be located for the default servlet '" +
            this.defaultServletName + "'");
   }
   rd.forward(request, response);
}

使用了mvc:default-servlet-handler/后会导致controller无法处理请求
加上<mvc:annotation-driven/>(注解驱动)即可保证controller正常使用

解决方案2

由SpringMVC框架来处理静态资源(内部通过ResourceHttpRequestHandler对象)

<!--**代表所有子路径-->
<!--mapping是请求路径-->
<!--location是静态资源的位置-->
<!--同样需要加上<mvc:annotation-driven/>-->
<mvc:resources mapping="/asset/**" location="/asset/" />

同样需要加上<mvc:annotation-driven/>,确保controller正常使用。

InternalResourceViewResolver

受InternalResourceViewResolver影响的有

  1. 通过返回值 ModelAndView 设置 viewName
  2. 通过返回值 String设置的 viewName
  3. 通过 <mvc:view-controller> 设置的 viewName

可以配置多个InternalResourceViewResolver order值越小,优先级越高

忽略受InternalResourceViewResolver影响

方法一: 在viewName前面加上 "forward:"、"redirect"
方法二: 通过ModelAndView的setView方法

实际上,之前通过返回值String、ModelAndView设置viewName之后 SpringMVC内部会根据具体情况创建对应的View对象 InternalResourceViewJstlViewRedirectView

  • InternalResourceViewResolvery影响的是没有带 "forward:"、"redirect" 的viewName

自定义InternalResourceViewResolver

InternalResourceViewResolver原理是会根据viewName创建响应的view(InternalResourceViewJstlViewRedirectView),然后view会调checkResource判断是否有相关文件,由于checkResource方法没有复写,调用的是父类的方法

public boolean checkResource(Locale locale) throws Exception {
   return true;
}

父类方法永远返回true,所以InternalResourceViewResolver的order属性是无效的,有资源文件会处理,没有就返回404(解析不了的返回505),不会调优先级较低的InternalResourceViewResolver。

我们可以自定义InternalResourceViewResolver,返回真实的checkResource。

public class MyResourceView extends InternalResourceView {

    @Override
    public boolean checkResource(Locale locale) throws Exception {
        String path = getServletContext().getRealPath(getUrl());
        File file = new File(path);
        return file.exists();
    }
}

如果checkResource返回false,会交给优先级次之的InternalResourceViewResolver处理,以此类推:
1.如果文件不存在,但优先级最低的返回true,返回404 2.如果文件不存在,但优先级最低的返回false,返回500

jsp7不存在
@RequestMapping("/jsp2")
public ModelAndView jsp2() {
    ModelAndView modelAndView = new ModelAndView("jsp2");
    modelAndView.setViewName("jsp7");
    return modelAndView;
}
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/page2/"/>
    <property name="suffix" value=".jsp"/>
    <property name="order" value="1"/>
</bean>

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/page/"/>
    <property name="suffix" value=".jsp"/>
    <property name="order" value="0"/>
    <property name="viewClass" value="com.xxx.view.MyResourceView"/>
</bean>

返回404错误

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/page2/"/>
    <property name="suffix" value=".jsp"/>
    <property name="order" value="1"/>
    <property name="viewClass" value="com.xxx.view.MyResourceView"/>
</bean>

返回500错误。

Java代码中路径问题总结

在Java代码中,路径问题总结

  1. 假设请求路径是:"http://IP地址:端口/context_path/path1/path2/path3"
  2. 假设转发路径是:"/page/test.jsp" 1> 以斜线(/)开头,参考路径是context_path 2> 所以最终转发路径是:"http://IP地址:端口/context_path" + "/page/test.jsp"
  3. 假设转发路径是:"page/test.jsp" 1> 不以斜线(/)开头,参考路径是当前请求路径的上一层路径 2> 所以最终转发路径是:"http://IP地址:端口/context_path/path1/path2/" + "page/test.jsp"

重定向同样适用
mv.setViewName("redirect:/page/jsp4.jsp?test=10");\ return "redirect:/page/jsp3.jsp";,

例外: modelAndView.setView(new RedirectView("/page/jsp3.jsp")); 和 return "redirect:/page/jsp3.jsp";的区别
modelAndView.setView(new RedirectView("/page/jsp3.jsp")); 为 http://IP地址:端口/page/jsp3.jsp context_path没了

在jsp、html代码中,路径问题总结

  1. 假设请求路径是:"http://IP地址:端口/context_path/path1/path2/path3"
  2. 假设跳转路径是:"/page/test.jsp"
    1> 以斜线(/)开头,参考路径是"http://IP地址:端口"
    2> 所以最终转发路径是:"http://IP地址:端口" + "/page/test.jsp"
  3. 假设转发路径是:"page/test.jsp"
    1> 不以斜线(/)开头,参考路径是当前请求路径的上一层路径
    2> 所以最终转发路径是:"http://IP地址:端口/context_path/path1/path2/" + "page/test.jsp"