终结篇前言
终于到了我们SpringMVC的终结篇啦!看完前面的章节的朋友应该已经对SpringMVC的执行流程有了较为清晰的认知,那么本节呢为了提升对SpringMVC执行流程和原理的理解呢,我们将它的"骨架"给拆散重组,将核心的部分手写出来,如果能够完全的理解接下来的代码,那么对你理解SpringMVC是非常有帮助的,那我么就开始吧~
实现原理图
老规矩啦,为了方便大家的理解,我还是把之前的图给搬过来了,为了突出重点呢我将原理图作了一点点简化修改,对比我们下面的代码更通俗易懂,大家可以参照这个流程图一步步的进行阅读,加深理解。
准备工作
本次我们手写代码就不用导入任何包啦,不过有几个文件还是需要提前准备下,在此之前,还是先将我们的目录结构给大家参考下吧
业务代码包结构
核心代码包结构
那么此次为了突出重点,我就没有写XML解析的代码,用properties文件暂时替代啦,Dom4j也不难,有兴趣的朋友可以参考我另一篇手写Mybatis的文章 Marco’s Java【Mybatis进阶(五) 漫谈Mybatis动态代理及源码解析】,其中有也有对XML解析代码的讲解。
那么我们在bean.properties
中只用配置如下信息,模拟springmvc.xml中的<context:conponent-scan: base-package="com.marco.business"></context:component-scan>
包扫描的这一部分
basePackage=com.marco.business
接下来就是我们的DispatcherServlet的初始化配置啦,注意了这里的DispatcherServlet是需要我们手写的哦,因此<servlet-class>
中的完全限定名应该是com.marco.spring.core.DispatcherServlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
id="WebApp_ID" version="3.1">
<display-name>10_marspring</display-name>
<!-- 配置前端控制器开始 -->
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.marco.spring.core.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>bean.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
<!-- 配置前端控制器结束 -->
</web-app>
还有个注意点就是<load-on-startup>1</load-on-startup>
提前初始化配置一定一定不要忘记了,否则是运行不了的
自定义注解的创建
那准备工作就到这里啦,在本次模拟SpringMVC运行中有5个必备的注解需要我们手动创建
分别是@Controller、@Service、@AutoWired、@RequestMapping和@Param,之前已经对注解系统的讲解过,比较简单,那么我就直接上代码啦
@Controller
/*该注解只能作用在类上*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
@Service
/*该注解只能作用在类上*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
@AutoWired
/*该注解只能作用在属性上*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoWired {
}
@RequestMapping
/*该注解可以作用在类和方法上*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
String value();
}
@Param
/*该注解只能作用在参数上*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Param {
String value();
}
前端控制器DispatcherServlet的创建
为了让大家能够跟着我们的执行流程走,DispatcherServlet的这一部分我会划分的很细,里面的代码会拆分成多个部分来讲解,大家不要走丢哦~
首先是我们DispatcherServlet的成员变量这一部分,具体的作用我们下面会一一讲解到,需要注意的一点是,这里我们没有额外的添加父类和接口去继承HttpServelt,直接让DispatcherServlet去继承它,另外我们使用重写doGet()和doPost()的方法替代重写service(),因为service()还有一些doPut()、doDelete()方法我们暂时还用不上。
DispatcherServlet继承了HttpServelt之后需要重写它的init()初始化方法,以便于我们获取web.xml配置文件中的信息,我这里将DispatcherServlet的创建及初始化的执行流程总结为下面5个步骤
/**
* 初始化DispatcherServlet
*/
@Override
public void init(ServletConfig config) throws ServletException {
//1,得到并加载配置文件
doLoadConfig(config.getInitParameter(CONTEXT_CONFIGLOCATION));
//2,扫描用户设定的包下面所有的类
doScanner(properties.getProperty(BASE_PACKAGE));
//3,实例化
doInstance();
//4,自动装配置
doAutoWired();
//5,初始化HandlerMapping 解析Controller 里面的@RequestMaping
initHandlerMapping();
//测试代码
Set<Entry<String,HandlerMethod>> entrySet = handlerMapping.entrySet();
System.out.println("##@##################################");
for (Entry<String, HandlerMethod> entry : entrySet) {
System.out.println(entry.getKey()+" "+entry.getValue().getMethod().getName());
}
}
接下来我们一步步的来看,并补充相关类和代码,这样思路会更加清晰
第一步:得到并加载配置文件
当tomcat启动时会加载web.xml配置文件,那么我们的DispatcherServlet继承了HttpServelt,同样可以获取到配置文件中的内容,那么我们第一步需要获取到web.xml配置文件中的contextConfigLocation的值
通过config.getInitParameter(CONTEXT_CONFIGLOCATION)
我们可以获取到contextConfigLocation的value,然后调用
doLoadConfig(config.getInitParameter(CONTEXT_CONFIGLOCATION))
这个方法
/**
* 加载配置文件,这里是模拟加载springmvc.xml,有兴趣的朋友可以自行解析xml来获取
* @param configLocation
*/
private void doLoadConfig(String configLocation) {
//加载流获取configLocation的值,也就是配置文件的路径
InputStream inStream = DispatcherServlet.class.getClassLoader().getResourceAsStream(configLocation);
try {
properties.load(inStream);//加载配置文件
} catch (IOException e) {
e.printStackTrace();
}
}
获取到contextConfigLocation的值也就是bean.properties文件的路径之后,我们通过之前定义的Properties加载配并存储在Properties对象中
第二步:扫描用户设定的包下面所有的类
接下来我们通过propertis获取到配置文件的路径properties.getProperty(BASE_PACKAGE)
并调用扫描方法doScanner(properties.getProperty(BASE_PACKAGE))
/**
* 扫描com/marco/business文件夹下所有的.class文件
* @param path
*/
private void doScanner(String path) {
String realPath = path.replaceAll("\\.", "/");//将com.marco.business 转成 com/marco/business
URL resource = DispatcherServlet.class.getClassLoader().getResource(realPath);
String filePath = resource.getFile();//获取com.marco.business下classes(编译为.class的文件)文件夹的路径
File file = new File(filePath);
//判断文件是否存在
if(file.exists()) {
//解析存放编译后的.class文件的文件夹中的com/marco/business这一部分的文件
File[] files = file.listFiles();
if(null != files && files.length > 0) {
for (File f : files) {
if(f.isDirectory()) {
doScanner(path + "." + f.getName());
} else {
//将com.marco.business.controller.UserController.class 转成com.marco.business.controller.UserController
classNames.add(path + "." + f.getName().replaceAll(".class", ""));
}
}
}
}
}
获取到文件的路径之后我们通过解析源路径(如将com.marco.business) 转成 com/marco/business这种File能够支持的形式,接着通过File类的解析获取到所有在com.marco.business下的类的完全限定名,并且存放在我们之前定义好的容器List<String> classNames
中
第三步:实例化className名单中的对象
接下来我们需要通过反射初始化我们className名单中的对象,并判断该对象上是否有@Controller或@Service的注解,如果有,则一并获取它的所有超类的名称(首字符小写,例如UserService写成userService,方便后期我们通过userService能够找到对应的类的映射执行对象MappedHandler),为此我们封装了一个方法获取当前对象所有的超类名称存放在Set<String>
容器中。
/**
* 把字符串的首字母小写
*
* @param name
* @return
*/
private String toLowerFirstWord(String name) {
char[] charArray = name.toCharArray();
charArray[0] += 32;
return String.valueOf(charArray);
}
/**
* 获取当前实例对象的所有父类的名称以及接口的Set集合,为了做自动装配时,能够自适应多态的情形
* @param clz
* @return
*/
private Set<String> getAllSuperClassName(Class<? extends Object> clz) {
Set<String> superClassNames = new HashSet<>();
Class<?> superclass = clz.getSuperclass();
Class<?>[] interfaces = clz.getInterfaces();
while(null != superclass) {
superClassNames.add(toLowerFirstWord(superclass.getSimpleName()));
superclass = superclass.getSuperclass();
}
if(null != interfaces && interfaces.length > 0) {
for (Class<?> interfaceClass : interfaces) {
superClassNames.add(toLowerFirstWord(interfaceClass.getSimpleName()));
}
}
return superClassNames;
}}
那什么需要专门定义一个存放超类名称的容器呢?原因是我们在一个对象中引用另一个对象的时候,一般会存在父类引用指向子类对象的情况,打个简单的比方,UserController中会引用UserServiceImpl对象,我们一般会这么写UserService userService = new UserServiceImpl()
,因此如果我们存放userServiceImpl
为key的话,可能找不到userService对象,因此需要Set集合存放该对象的名称以及它的超类的名称,以便我们可以通过它的超类的名称直接获取到这个对象。
我们再定义一个MappedHandler类,用于存放该花名册中映射类的实例,以及该类的类型,这里的类型指的是MVC模型中所属的位置,比如说Controller、Service、Dao,这里我们用枚举来定义
MappedHandler
package com.marco.spring.handler;
public class MappedHandler {
/**
* 扫描的包下的类的实例
*/
private Object instance;
/**
* 类的Web类型(如Controller,Service)
*/
private String type;
public MappedHandler(Object instance, String type) {
super();
this.instance = instance;
this.type = type;
}
public Object getInstance() {
return instance;
}
public void setInstance(Object instance) {
this.instance = instance;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
ContentType
public enum ContentType {
CONTROLLER,SERVICE;
}
接下来我们将获取到的MappedHandler作为value,存放对象名称的Set集合作为key存放在IoC容器WebServletContext中,做为储备资源库。
/**
* 实例化className名册中的对象,并存入webServletContext容器中
*/
private void doInstance() {
if(!classNames.isEmpty()) {
for (String className : classNames) {
//初始化对象,并加载进内存中
try {
Class<?> clz = Class.forName(className);
//判断ins里面有没有@Controller或@Service的注解
if(clz.isAnnotationPresent(Controller.class)) {
String name = clz.newInstance().getClass().getSimpleName();
Set<String> names = getAllSuperClassName(clz.newInstance().getClass());//获取当前实例对象的所有父类的名称的Set集合
names.add(toLowerFirstWord(name));//将当前类也加入该集合
MappedHandler mappedHandler = new MappedHandler(clz.newInstance(), ContentType.CONTROLLER.name());
webServletContext.put(names, mappedHandler);
} else if(clz.isAnnotationPresent(Service.class)) {
String name = clz.newInstance().getClass().getSimpleName();
Set<String> names = getAllSuperClassName(clz.newInstance().getClass());
names.add(toLowerFirstWord(name));//将当前类也加入该集合
MappedHandler mappedHandler = new MappedHandler(clz.newInstance(), ContentType.SERVICE.name());
webServletContext.put(names, mappedHandler);
}
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
System.out.println("The class【" + className + "】 was initialized with error");
e.printStackTrace();
}
}
}
}
第四步:自动装配置
完成上面的对象实例化操作之后,接下来就是通过注解@AutoWired模拟实现IoC注入了,在SpringMVC中可以通过方法和属性上配置@AutoWired注解实现IoC注入,我们这边就只是用其中一种,属性注入的方式啦。
/**
* 扫描IOC容器中的对象,如果有@AutoWired注解,则使用注入的方式完成自动装配(这里采取的是Field赋值方式)
*/
private void doAutoWired() {
//判断IoC容器是否为空
if(!webServletContext.isEmpty()) {
Set<Entry<Set<String>, MappedHandler>> entrySet = webServletContext.entrySet();
Iterator<Entry<Set<String>, MappedHandler>> iterator = entrySet.iterator();
while (iterator.hasNext()) {
Map.Entry<Set<String>,MappedHandler> entry = (Map.Entry<Set<String>,MappedHandler>) iterator.next();
Object instance = entry.getValue().getInstance();
Field[] fields = instance.getClass().getDeclaredFields();
if(null != fields && fields.length > 0) {
for (Field field : fields) {
//遍历这个类是否有被@AutoWired注解的属性
if(field.isAnnotationPresent(AutoWired.class)) {
String name = field.getName();//获取类中的属性名称
Object obj = doScannerForAutoWired(name);//获取IOC容器中扫描到的对象的实例
if(null != obj) {
//如果有,就给属性赋值
field.setAccessible(true);
try {
field.set(instance, obj);
} catch (IllegalArgumentException | IllegalAccessException e) {
System.err.println("webServletContext doesn't contains the instance of :"+name);
e.printStackTrace();
}
}
}
}
}
}
}
}
整个过程都是使用反射机制来完成的,关于反射是如何操作我就不细细讲啦,如果觉得反射的知识需要回顾一下的话,请戳 Marco’s Java【反射】
需要注意的一点是,通过反射对属性赋值的时候,需要开启可赋值权限,如下所示,否则赋值会不成功
field.setAccessible(true);
原因是我们的JavaBean中属性一般都是private修饰的,如果不设置权限,当isAccessible()的结果是false时是不允许通过反射访问该字段的。
第五步:初始化映射控制器
到我们熟悉的一步操作啦,初始化"花名册"(映射控制器),那么我们这里重点关注的对象就是Controller控制器了,通过context容器我们可以根据Type筛选CONTROLLER对象,接着呢,我们来解析@RequestMapping中的值(如果存在),依然还是通过反射获取注解中的值,因为@RequestMapping既可以存在于类上,也可以存在于方法上,因此,我么需要判断当前的CONTROLLER对象的类上和方法上是否有这个注解,并获取里面的值,做拼接。最终获取到这种效果/user/addUser
/**
* 初始化映射控制器
*/
public void initHandlerMapping() {
if(!webServletContext.isEmpty()) {
Set<Entry<Set<String>, MappedHandler>> entrySet = webServletContext.entrySet();
for (Entry<Set<String>, MappedHandler> entry : entrySet) {
//筛选CONTROLLER对象
if(entry.getValue().getType().equals(ContentType.CONTROLLER.name())) {
MappedHandler handler = entry.getValue();
Object instance = handler.getInstance();//获取CONTROLLER对象实例
StringBuilder rootPath = new StringBuilder("/");
if(instance.getClass().isAnnotationPresent(RequestMapping.class)) {//如果类上有@RequestMapping注解
//获取注解
RequestMapping requestMapping = instance.getClass().getDeclaredAnnotation(RequestMapping.class);
//获取类上注解的值并拼接
rootPath.append(requestMapping.value());
}
Method[] methods = instance.getClass().getDeclaredMethods();
if(null != methods && methods.length > 0) {
for (Method method : methods) {
//判断方法上是否有@RequestMapping注解
if(method.isAnnotationPresent(RequestMapping.class)) {
StringBuilder childrenPath = new StringBuilder(rootPath.toString() + "/");
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
//获取方法上注解的值并拼接
childrenPath.append(requestMapping.value());
//将拼接好的路径作为key HandlerMethod作为value存放进handlerMapping控制器映射器中
handlerMapping.put(childrenPath.toString(), new HandlerMethod(instance, method));
}
}
}
}
}
}
}
方法的最后,我们将拼接好的路径作为key HandlerMethod作为value存放进handlerMapping控制器映射器中
等待客户端请求的时候,再被扫描并调用,注意了,这里的HandlerMethod存放的是类的实例对象和被执行的犯法,方便后期直接通过method.invoke(obj,method,args)执行该对象对应的方法,下面是HandlerMethod的代码
package com.marco.spring.handler;
import java.lang.reflect.Method;
public class HandlerMethod {
private Object target;
private Method method;
public HandlerMethod(Object target, Method method) {
super();
this.target = target;
this.method = method;
}
public Object getTarget() {
return target;
}
public void setTarget(Object target) {
this.target = target;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
}
第六步:初始化映射控制器
还没完!上面只列出了DispatcherServlet初始化的5个步骤,但是接下来的几个正真涉及到和页面交互的步骤也是不可或缺的,还记得我们之前说过我们的前端控制器继承了HttpServlet并重写了它的doGet()和doPost()方法吗?我们接着来看。
@Override
/**
* get请求,统统调用post方法
*/
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
/**
* 处理页面上的请求,并回给页面响应的内容
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object object = doDispatch(request, response);
if(null != object) {
//判断控制器返回的值是String类型,还是ModelAndView
if(object instanceof String) {
//判断是否是请求重定向
if(object.toString().startsWith("redirect:")) {
response.sendRedirect(object.toString().replaceAll("redirect:", ""));
} else {//如果不是请求重定向就是请求转发
request.getRequestDispatcher(object.toString()).forward(request, response);
}
} else if(object instanceof ModelAndView) {
ModelAndView modelAndView = (ModelAndView) object;
String viewName = modelAndView.getViewName();
//判断是否是请求重定向
if(viewName.startsWith("redirect:")) {
response.sendRedirect(viewName.replaceAll("redirect:", ""));
} else {
//如果是请求转发,则获取ModelAndView中的Model中的数据
Map<String, Object> model = modelAndView.getModel();
if(!model.isEmpty()) {
Set<Entry<String, Object>> entrySet = model.entrySet();
for (Entry<String, Object> entry : entrySet) {
request.setAttribute(entry.getKey(), entry.getValue());
}
}
request.getRequestDispatcher(viewName).forward(request, response);
}
}
} else {
System.err.println("Param doesn't match with annotation's value!");
}
}
首先,我们需要通过分发请求,执行Controller里面的方法并获取到该方法中最终返回的值,有可能是ModelAndView,也有可能是字符串,我们需要对此加以区分,我们先来看doDispatch()分发之后的操作,再来看doDispatch()是如何执行的。
按照我们的思路,我们首先分析Controller执行完成后返回给我们的究竟是什么值?
如果是String,我们需要将他再封装成ModelAndView模型视图对象,SpringMVC的底层也是这么去实现的,接着我们需要分析控制器返回给我们的路径前面是不是有"redirect",如果有,则证明是请求重定向,如果没有则按照请求转发处理,因为请求转发可能伴随着对象在request作用域中传输,因此我们还需要判断ModelAndView中是否存有值。讲了半天,我们来看看ModelAndView的结构。
我们发现ModelAndView就是存放页面跳转路径以及Model对象的一个类,那么Model实际上就是一个Map,在SpringMVC中,Model是继承了LinkedHashMap的,总而言之,就是用于存放请求转发对象的容器。
因此,当控制器返回的结果是ModelAndView时,我们就不用再次封装啦,执行的方式和刚才String的处理方式差不太多,唯一需要注意的就是判断ModelAndView中有没有转发对象,并取出来,添加到request域对象中进行传输。那我们在这里先停一下,回过头来看看doDispatch(request, response)
方法是如何分配请求的。
第七步:分发请求
实质上这一步骤才是第六步,但是为了将前面的内容一气呵成,就将这个重要的部分单独拿到最后讲,这个步骤呢就模拟了我们下面这一部分的操作,这里我就将查找适配器和拦截器省掉了,直接获取handler,有兴趣的朋友也可以自己加上这一步骤啦
我们来分析下一下的代码,首先呢,我们的前端控制需要通过request获取界面上请求的URI,通过解析URI,例如marspring/user/addUser.action
,我们解析之后需要把URL卸成/user/addUser
,那么通过这个key,我们很容易可以从handlerMapping取出对应的HandlerMethod方法执行对象,接着呢,我们需要解析控制器的方法中的参数上是否有@Param注解,如果有,则获取@Param注解中的值,也就是参数名,接着按序从parameterMap(页面上获取的所有参数和值的Map集合)取出对应的值,存放在数组中Object[] paramValues
,注意了,这个数组的长度就是我们页面上获取的参数个数,最后拿到所有参数的值的数组之后,通过反射调用method.invoke()方法
获取到Controller执行完成后返回的值。
/**
* 分发请求,并执行控制器中的方法,将方法执行的返回值返回
* @param request
* @param response
* @return
*/
private Object doDispatch(HttpServletRequest request, HttpServletResponse response) {
//解析前台发送过来的URI请求,例如/marspring/user/addUser.action
String uri = request.getRequestURI();
//获取contextPath
String contextPath = request.getContextPath();
//截取前缀,得到/user/addUser.action
uri = uri.replaceAll(contextPath, "");
//截取后缀,得到/user/addUser
uri = uri.substring(0, uri.lastIndexOf("."));
System.out.println(uri);
//根据解析后的uri,通过handlerMapping花名册获取到对应的HandlerMethod
if(handlerMapping.containsKey(uri)) {
HandlerMethod handlerMethod = handlerMapping.get(uri);
try {
Object target = handlerMethod.getTarget();//获取对象
Method method = handlerMethod.getMethod();//获取方法的对象
Map<String, String[]> parameterMap = request.getParameterMap();//获取页面上所有参数和对应值的Map集合
//获取被执行方法上所有参数的Class
method.setAccessible(true);
Parameter[] parameters = method.getParameters();
int index = 0;
Object[] paramValues = new Object[parameters.length];
for (Parameter parameter : parameters) {
//String typeName = parameter.getType().getSimpleName();
Annotation[] annotations = parameter.getDeclaredAnnotations();
for (Annotation annotation : annotations) {
//判断该参数上标注的注解是否是@Param
if(annotation instanceof Param) {
Param param = (Param) annotation;
//如果是@Param注解,则获取注解中的值
String key = param.value();
//通过注解中的值,我们可以获取到,这方法中的参数的名称,通过参数的名称
//我们可以去parameterMap中获取该参数对应的值
String[] params = parameterMap.get(key);
if(null == params) {
return null;
}
if(params.length == 1) {
String value = params[0];
paramValues[index] = value;
}
}
}
index ++;
}
Object invoke = method.invoke(target, paramValues);
return invoke;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
e.printStackTrace();
}
}
return null;
}
那么到现在为止,就已经和之前的代码串联起来了,在第六步的最后,我们通过判断返回值的跳转路径,来确定是否跳转还是重定向,并执行相应的分发方法,最后跳转到到相应的jsp页面,如果request作用域中有值,那么我们就将数据渲染到页面上。到此为止,我们的核心代码部分就差不多编写完成了,接下来我们编写业务代码测试一下是否能正常执行吧~
第八步:编写业务代码测试
业务代码就比较简单啦,我就不过多讲解了,直接上代码吧
domain包: User
service包: UserService
package com.marco.business.service;
public interface UserService {
void addUser(String name);
}
service.impl包: UserServiceImpl
package com.marco.business.service.impl;
import com.marco.business.service.UserService;
import com.marco.spring.annotation.Service;
@Service
public class UserServiceImpl implements UserService{
@Override
public void addUser(String name) {
System.out.println("添加用户" + name + "成功");
}
}
controller包: UserController和DeptController
package com.marco.business.controller;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.marco.business.domain.User;
import com.marco.business.service.impl.UserServiceImpl;
import com.marco.spring.annotation.AutoWired;
import com.marco.spring.annotation.Controller;
import com.marco.spring.annotation.Param;
import com.marco.spring.annotation.RequestMapping;
import com.marco.spring.handler.ModelAndView;
@Controller
@RequestMapping("user")
public class UserController {
@AutoWired
private UserServiceImpl userService;
@RequestMapping("addUser")
public String addUser(HttpServletRequest request,HttpServletResponse response,@Param(value="name")String name) {
System.out.println(userService);
userService.addUser(name);
return "../add.jsp";
}
@RequestMapping("listUser")
public ModelAndView listUser(HttpServletRequest request,HttpServletResponse response) {
List<User> users = new ArrayList<>();
for (int i = 0; i < 5; i++) {
users.add(new User("marco" + i, 18));
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("../list.jsp");
modelAndView.addAttribute("users", users);
return modelAndView;
}
}
package com.marco.business.controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.marco.spring.annotation.Controller;
import com.marco.spring.annotation.RequestMapping;
/*这个包仅是为了多添加一条包扫描数据,因此不会去调用它*/
@Controller
@RequestMapping("dept")
public class DeptController {
@RequestMapping("addDept")
public String addDept(HttpServletRequest request,HttpServletResponse response) {
System.out.println("addDept");
return "list.jsp";
}
}
好啦,我们的业务代码就这么多,接下来就测试看看UserController中的两个方法是不是能够正常执行啦~
我们先来测试看看listUser()方法,是否能够正常执行
发现没有问题,那接下来测试addUser()方法,这里我们将请求参数写在url地址上来进行测试
页面貌似跳转成成功了,我们来看看页面上传递的参数有没有被打印出来
也没有问题!那本节的手写SpringMVC终结篇到这里就告一段落啦,可能本次手写的代码并不是最齐全的,但是我相信,通过这次手写并梳理思路会比直接看源码更通俗易懂,如果能够吧上面的步骤彻底弄清楚,那么对你再深度挖掘SpringMVC底层是很有帮助的,还是一句话,多看底层,多研究底层,然后按照自己的思路写一遍,不管对与错,都是对自己有极大的提升!
后语
我们的框架之旅目前已经跨过三个阶段,接下来还有很多很好用的框架,我会给大家再一一介绍啦~