简单模拟Spring MVC
在Spring MVC中,将一个普通的java类标注上Controller注解之后,再将类中的方法使用RequestMapping注解标注,那么这个普通的java类就够处理Web请求,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
@Controller public class LoginUI { @RequestMapping("LoginUI/Login2") public View forward1(){ return new View("/login2.jsp"); } @RequestMapping("LoginUI/Login3") public View forward2(){ return new View("/login3.jsp"); } }
|
spring通过java annotation就可以注释一个类为action ,在方法上添加上一个java annotation 就可以配置请求的路径了,那么这种机制是如何实现的呢,今天我们使用”自定义注解+Servlet”来简单模拟一下Spring MVC中的这种注解配置方式。
一、编写注解
1.1 Controller注解
开发Controller注解,这个注解只有一个value属性,默认值为空字符串,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| package me.gacl.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Controller {
public String value() default ""; }
|
1.2 RequestMapping注解
开发RequestMapping注解,用于定义请求路径,这个注解只有一个value属性,默认值为空字符串,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| package me.gacl.annotation;
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequestMapping {
public String value() default ""; }
|
以上就是我们自定义的两个注解,注解的开发工作就算是完成了,有了注解之后,那么就必须针对注解来编写处理器,否则我们开发的注解配置到类或者方法上面是不起作用的,这里我们使用Servlet来作为注解的处理器。
编写核心的注解处理器
2.1 开发AnnotationHandleServlet
这里使用一个Servlet来作为注解处理器,编写一个AnnotationHandleServlet,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| package me.gacl.web.controller;
import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Set; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import me.gacl.annotation.Controller; import me.gacl.annotation.RequestMapping; import me.gacl.util.BeanUtils; import me.gacl.util.RequestMapingMap; import me.gacl.util.ScanClassUtil; import me.gacl.web.context.WebContext; import me.gacl.web.view.DispatchActionConstant; import me.gacl.web.view.View;
public class AnnotationHandleServlet extends HttpServlet { private String pareRequestURI(HttpServletRequest request){ String path = request.getContextPath()+"/"; String requestUri = request.getRequestURI(); String midUrl = requestUri.replaceFirst(path, ""); String lasturl = midUrl.substring(0, midUrl.lastIndexOf(".")); return lasturl; } public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.excute(request, response); }
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { this.excute(request, response); } private void excute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ WebContext.requestHodler.set(request); WebContext.responseHodler.set(response); String lasturl = pareRequestURI(request); Class<?> clazz = RequestMapingMap.getRequesetMap().get(lasturl); Object classInstance = BeanUtils.instanceClass(clazz); Method [] methods = BeanUtils.findDeclaredMethods(clazz); Method method = null; for(Method m:methods){ if(m.isAnnotationPresent(RequestMapping.class)){ String anoPath = m.getAnnotation(RequestMapping.class).value(); if(anoPath!=null && !"".equals(anoPath.trim()) && lasturl.equals(anoPath.trim())){ method = m; break; } } } try { if(method!=null){ Object retObject = method.invoke(classInstance); if (retObject!=null) { View view = (View)retObject; if(view.getDispathAction().equals(DispatchActionConstant.FORWARD)){ request.getRequestDispatcher(view.getUrl()).forward(request, response); }else if(view.getDispathAction().equals(DispatchActionConstant.REDIRECT)){ response.sendRedirect(request.getContextPath()+view.getUrl()); }else{ request.getRequestDispatcher(view.getUrl()).forward(request, response); } } } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
@Override public void init(ServletConfig config) throws ServletException {
super.init(config); System.out.println("---初始化开始---"); String basePackage = config.getInitParameter("basePackage"); if (basePackage.indexOf(",")>0) { String[] packageNameArr = basePackage.split(","); for (String packageName : packageNameArr) { initRequestMapingMap(packageName); } }else { initRequestMapingMap(basePackage); } System.out.println("----初始化结束---"); }
private void initRequestMapingMap(String packageName){ Set<Class<?>> setClasses = ScanClassUtil.getClasses(packageName); for (Class<?> clazz :setClasses) { if (clazz.isAnnotationPresent(Controller.class)) { Method [] methods = BeanUtils.findDeclaredMethods(clazz); for(Method m:methods){ if(m.isAnnotationPresent(RequestMapping.class)){ String anoPath = m.getAnnotation(RequestMapping.class).value(); if(anoPath!=null && !"".equals(anoPath.trim())){ if (RequestMapingMap.getRequesetMap().containsKey(anoPath)) { throw new RuntimeException("RequestMapping映射的地址不允许重复!"); } RequestMapingMap.put(anoPath, clazz); } } } } } } }
|
简单说一下AnnotationHandleServlet的实现思路
- AnnotationHandleServlet初始化(init)时扫描指定的包下面使用了Controller注解的类,如下图所示:
- 遍历类中的方法,找到类中使用了RequestMapping注解标注的那些方法,获取RequestMapping注解的value属性值,value属性值指明了该方法的访问路径,以RequestMapping注解的value属性值作为key,Class类作为value将存储到一个静态Map集合中。如下图所示:
当用户请求时(无论是get还是post请求),会调用封装好的execute方法 ,execute会先获取请求的url,然后解析该URL,根据解析好的URL从Map集合中取出要调用的目标类 ,再遍历目标类中定义的所有方法,找到类中使用了RequestMapping注解的那些方法,判断方法上面的RequestMapping注解的value属性值是否和解析出来的URL路径一致,如果一致,说明了这个就是要调用的目标方法,此时就可以利用java反射机制先实例化目标类对象,然后再通过实例化对象调用要执行的方法处理用户请求。服务器将以下图的方式与客户端进行交互
另外,方法处理完成之后需要给客户端发送响应信息,比如告诉客户端要跳转到哪一个页面,采用的是服务器端跳转还是客户端方式跳转,或者发送一些数据到客户端显示,那么该如何发送响应信息给客户端呢,在此,我们可以设计一个View(视图)类,对这些操作属性进行封装,其中包括跳转的路径 、展现到页面的数据、跳转方式。这就是AnnotationHandleServlet的实现思路。
2.2 在Web.xml文件中注册AnnotationHandleServlet
在web.xml文件中配置AnnotationHandleServlet和需要扫描的包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <servlet> <servlet-name>AnnotationHandleServlet</servlet-name> <servlet-class>me.gacl.web.controller.AnnotationHandleServlet</servlet-class> <init-param> <description>配置要扫描包及其子包, 如果有多个包,以逗号分隔</description> <param-name>basePackage</param-name> <param-value>me.gacl.web.controller,me.gacl.web.UI</param-value> <!-- <param-value>me.gacl.web.controller</param-value> --> </init-param> <load-on-startup>1</load-on-startup> </servlet>
<servlet-mapping> <servlet-name>AnnotationHandleServlet</servlet-name> <!-- 拦截所有以.do后缀结尾的请求 --> <url-pattern>*.do</url-pattern> </servlet-mapping>
|
三、相关代码讲解
3.1 BeanUtils
BeanUtils工具类主要是用来处理一些反射的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
| package me.gacl.util;
import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier;
public class BeanUtils {
public static <T> T instanceClass(Class<T> clazz){ if(!clazz.isInterface()){ try { return clazz.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return null; }
public static <T> T instanceClass(Constructor<T> ctor, Object... args) throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException{ makeAccessible(ctor); return ctor.newInstance(args); }
public static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes){ try { return clazz.getMethod(methodName, paramTypes); } catch (NoSuchMethodException e) { return findDeclaredMethod(clazz, methodName, paramTypes); } } public static Method findDeclaredMethod(Class<?> clazz, String methodName, Class<?>[] paramTypes){ try { return clazz.getDeclaredMethod(methodName, paramTypes); } catch (NoSuchMethodException ex) { if (clazz.getSuperclass() != null) { return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes); } return null; } } public static Method [] findDeclaredMethods(Class<?> clazz){ return clazz.getDeclaredMethods(); } public static void makeAccessible(Constructor<?> ctor) { if ((!Modifier.isPublic(ctor.getModifiers()) || !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { ctor.setAccessible(true); } } public static Field[] findDeclaredFields(Class<?> clazz){ return clazz.getDeclaredFields(); } }
|
3.2 RequestMapingMap
该类是用于存储方法的访问路径,AnnotationHandleServlet初始化时会将类(使用Controller注解标注的那些类)中使用了RequestMapping注解标注的那些方法的访问路径存储到RequestMapingMap中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package me.gacl.util;
import java.util.HashMap; import java.util.Map;
public class RequestMapingMap {
private static Map<String, Class<?>> requesetMap = new HashMap<String, Class<?>>(); public static Class<?> getClassName(String path) { return requesetMap.get(path); }
public static void put(String path, Class<?> className) { requesetMap.put(path, className); }
public static Map<String, Class<?>> getRequesetMap() { return requesetMap; } }
|
3.3. ScanClassUtil
扫描某个包下面的类的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
| package me.gacl.util;
import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.net.JarURLConnection; import java.net.URL; import java.net.URLDecoder; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile;
public class ScanClassUtil {
public static Set<Class<?>> getClasses(String pack) {
Set<Class<?>> classes = new LinkedHashSet<Class<?>>(); boolean recursive = true; String packageName = pack; String packageDirName = packageName.replace('.', '/'); Enumeration<URL> dirs; try { dirs = Thread.currentThread().getContextClassLoader().getResources( packageDirName); while (dirs.hasMoreElements()) { URL url = dirs.nextElement(); String protocol = url.getProtocol(); if ("file".equals(protocol)) { System.err.println("file类型的扫描"); String filePath = URLDecoder.decode(url.getFile(), "UTF-8"); findAndAddClassesInPackageByFile(packageName, filePath, recursive, classes); } else if ("jar".equals(protocol)) { System.err.println("jar类型的扫描"); JarFile jar; try { jar = ((JarURLConnection) url.openConnection()) .getJarFile(); Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String name = entry.getName(); if (name.charAt(0) == '/') { name = name.substring(1); } if (name.startsWith(packageDirName)) { int idx = name.lastIndexOf('/'); if (idx != -1) { packageName = name.substring(0, idx) .replace('/', '.'); } if ((idx != -1) || recursive) { if (name.endsWith(".class") && !entry.isDirectory()) { String className = name.substring( packageName.length() + 1, name .length() - 6); try { classes.add(Class .forName(packageName + '.' + className)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } } } } catch (IOException e) { e.printStackTrace(); } } } } catch (IOException e) { e.printStackTrace(); }
return classes; }
public static void findAndAddClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) { File dir = new File(packagePath); if (!dir.exists() || !dir.isDirectory()) { return; } File[] dirfiles = dir.listFiles(new FileFilter() { public boolean accept(File file) { return (recursive && file.isDirectory()) || (file.getName().endsWith(".class")); } }); for (File file : dirfiles) { if (file.isDirectory()) { findAndAddClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes); } else { String className = file.getName().substring(0, file.getName().length() - 6); try { classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className)); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } } }
|
3.4 WebContext
WebContext主要是用来存储当前线程中的HttpServletRequest和HttpServletResponse,当别的地方需要使用HttpServletRequest和HttpServletResponse,就可以通过requestHodler和responseHodler获取,通过WebContext.java这个类 ,我们可以在作为Controller的普通java类中获取当前请求的request、response或者session相关请求类的实例变量,并且线程间互不干扰的,因为用到了ThreadLocal这个类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| package me.gacl.web.context;
import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession;
public class WebContext {
public static ThreadLocal<HttpServletRequest> requestHodler = new ThreadLocal<HttpServletRequest>(); public static ThreadLocal<HttpServletResponse> responseHodler = new ThreadLocal<HttpServletResponse>(); public HttpServletRequest getRequest(){ return requestHodler.get(); } public HttpSession getSession(){ return requestHodler.get().getSession(); } public ServletContext getServletContext(){ return requestHodler.get().getSession().getServletContext(); } public HttpServletResponse getResponse(){ return responseHodler.get(); } }
|
3.5 View
一个视图类,对一些客户端响应操作进行封装,其中包括跳转的路径 、展现到页面的数据、跳转方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| package me.gacl.web.view;
public class View {
private String url; private String dispathAction = DispatchActionConstant.FORWARD;
public View(String url) { this.url = url; } public View(String url,String name,Object value) { this.url = url; ViewData view = new ViewData(); view.put(name, value); } public View(String url,String name,String dispathAction ,Object value) { this.dispathAction = dispathAction; this.url = url; ViewData view = new ViewData(); view.put(name, value); } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; }
public String getDispathAction() { return dispathAction; }
public void setDispathAction(String dispathAction) { this.dispathAction = dispathAction; } }
|
3.6 ViewData
request范围的数据存储类,当需要发送数据到客户端显示时,就可以将要显示的数据存储到ViewData类中。使用ViewData.put(String name,Object value)方法往request对象中存数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package me.gacl.web.view;
import javax.servlet.http.HttpServletRequest;
import me.gacl.web.context.WebContext;
public class ViewData { private HttpServletRequest request; public ViewData() { initRequest(); }
private void initRequest(){ this.request = WebContext.requestHodler.get(); } public void put(String name,Object value){ this.request.setAttribute(name, value); } }
|
3.7 DispatchActionConstant
一个跳转方式的常量类
1 2 3 4 5 6 7 8 9 10 11
| package me.gacl.web.view;
public class DispatchActionConstant {
public static String FORWARD = "forward"; public static String REDIRECT = "redirect"; }
|
四、Controller注解和RequestMapping注解测试
4.1 简单测试
编写一个LoginUI类,用于跳转到具体的jsp页面,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package me.gacl.web.UI;
import me.gacl.annotation.Controller; import me.gacl.annotation.RequestMapping; import me.gacl.web.view.View;
@Controller public class LoginUI { @RequestMapping("LoginUI/Login2") public View forward1(){ return new View("/login2.jsp"); } @RequestMapping("LoginUI/Login3") public View forward2(){ return new View("/login3.jsp"); } }
|
运行结果如下
4.2 复杂测试
编写用于处理用户登录请求的Controller,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package me.gacl.web.controller;
import java.io.IOException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import me.gacl.annotation.Controller; import me.gacl.annotation.RequestMapping; import me.gacl.web.context.WebContext; import me.gacl.web.view.View; import me.gacl.web.view.ViewData;
@Controller public class LoginServlet2 {
@RequestMapping("login/handle") public View loginHandle(){ ViewData viewData = new ViewData(); HttpServletRequest request = WebContext.requestHodler.get(); String username =request.getParameter("usename"); String pwd = request.getParameter("pwd"); if (username.equals("gacl") && pwd.equals("xdp")) { request.getSession().setAttribute("usename", username); viewData.put("msg", "欢迎您!"+username); return new View("/index.jsp"); }else { viewData.put("msg", "登录失败,请检查用户名和密码是否正确!"); return new View("/login2.jsp"); } }
@RequestMapping("ajaxLogin/handle") public void ajaxLoginHandle() throws IOException{ HttpServletRequest request = WebContext.requestHodler.get(); String username =request.getParameter("usename"); String pwd = request.getParameter("pwd"); HttpServletResponse response = WebContext.responseHodler.get(); if (username.equals("gacl") && pwd.equals("xdp")) { request.getSession().setAttribute("usename", username); response.getWriter().write("success"); }else { response.getWriter().write("fail"); } } }
|
编写用于测试的jsp页面,代码如下所示:
Login2.jsp登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <%@ page language="java" pageEncoding="UTF-8"%> <!DOCTYPE HTML> <html> <head> <title>login2.jsp登录页面</title> </head> <body> <fieldset> <legend>用户登录</legend> <form action="${pageContext.request.contextPath}/login/handle.do" method="post"> 用户名:<input type="text" value="${param.usename}" name="usename"> <br/> 密码:<input type="text" value="${param.pwd}" name="pwd"> <br/> <input type="submit" value="登录"/> </form> </fieldset> <hr/> <label style="color: red;">${msg}</label> </body> </html>
|
login3.jsp登录页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <%@ page language="java" pageEncoding="UTF-8"%> <!DOCTYPE HTML> <html> <head> <title>login3登录页面</title> <script type="text/javascript" src="${pageContext.request.contextPath}/ajaxUtil.js"></script> <script type="text/javascript" src="${pageContext.request.contextPath}/js/Utils.js"></script> <script type="text/javascript"> function login(){ Ajax.request({ url : "${pageContext.request.contextPath}/ajaxLogin/handle.do", data : { "usename" : document.getElementById("usename").value, "pwd" : document.getElementById("pwd").value }, success : function(xhr) { onData(xhr.responseText); }, error : function(xhr) { } }); } function onData(responseText){ if(responseText=="success"){
location.replace(g_basePath+"/index.jsp"); }else{ alert("用户名和密码错误"); } } </script> </head> <body> <fieldset> <legend>用户登录</legend> <form> 用户名:<input type="text" name="usename" id="usename"> <br/> 密码:<input type="text" name="pwd" id="pwd"> <br/> <input type="button" value="登录" onclick="login()"/> </form> </fieldset> </body> </html>
|
index.jsp页面代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML> <html> <head> <title>首页</title> </head> <body> 登录的用户名:${usename} <br/> ${msg} </body> </html>
|
jsp页面中使用到的Utils.js代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| (function() { var contextPath = getContextPath(); var basePath = getBasePath(); window.g_contextPath = contextPath; window.g_basePath = basePath; })();
function getBasePath() { var curWwwPath = window.document.location.href; var pathName = window.document.location.pathname; var pos = curWwwPath.indexOf(pathName); var localhostPath = curWwwPath.substring(0, pos); var projectName = pathName.substring(0, pathName.substr(1).indexOf('/') + 1); return (localhostPath + projectName); }
function getContextPath() { return window.document.location.pathname.substring(0, window.document.location.pathname.indexOf('\/', 1)); };
|
测试结果如下:
以上就是对Spring MVC的简单模拟。