Marco's Java【SpringMVC进阶(三) 之 手写SpringMVC终章】

终结篇前言

终于到了我们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底层是很有帮助的,还是一句话,多看底层,多研究底层,然后按照自己的思路写一遍,不管对与错,都是对自己有极大的提升!

后语

我们的框架之旅目前已经跨过三个阶段,接下来还有很多很好用的框架,我会给大家再一一介绍啦~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值