在手些SpringMVC之前,先明确SpringMVC的内容有那些:
- DispatcherServlet的初始化
- 请求
在SpringMVC里面扫描是在xml文件中配置了包的地址
在这里使用properties代替xml
前端访问后端的过程:
首先解析web.xml文件,找到前端控制器(DispatcherServlet,前端控制器得到了前端的url地址,并通过HandlerMapping分配到不同的控制器(Controller),控制器处理请求并返回ModelAndView(包括模型和视图)给前端控制器,前端控制器将渲染结果发送到前端;前后端完成一次交互:
在实现SpringMVC的过程中,需要实例化业务类(如Controller, Service ),此处通过注解进行实例化
所以需要创建一些注解:
首先:
实例化对象的注解有:
@Controller
@Service
自动装配置的注解有
@AutoWired
为方法配置访问名的注解:
@ResultMapping
获取参数名的注解:
@Param:
- @Controller是注解在类上面的,被他注解的类是一个总控制类,它在运行时起作用;如何实现它的作用呢?扫描所有的类,如果发现这个类上有这个注解,就为这个类创建对象,并且把这个对象放入IOC容器中
@Target(value={ElementType.TYPE}) // 只能作用在类上
@Retention(RetentionPolicy.RUNTIME) // 运行时起作用
@Documented // 生成文档
public @interface Controller {
}
- @Service和@Controller作用都是实例化对象,只是标记不同作用的类而已, 是注解在类上面的,被他注解的类是一个服务类,它在运行时起作用。实现它的作用的方法和@Controller一样,即扫描所有的类,如果发现这个类上有这个注解,就为这个类创建对象,并且把这个对象放入IOC容器中
@Target(value={ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
}
- @AutoWired自动装配置,只能注解在属性上,这个属性必须是对象,不能是基本数据类型,它的作用是为这个属性赋值。即在Ioc容器里拿相应的对象为其赋值。IOC容器是通过一个Map集合进行存放对象的;此处为简化,为实现这个注解的作用,可以通过以下方法来实现,即当扫描到一个属性有这个注解时,通过这个属性的名字,在IOC的Map集合中获取值,这样就得到 了这个属性的对象,并对其赋值。
@Target(value= {ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoWired {
}
- @ResultMapping这个注解既可以注释在类上也可以注解在方法方法上,这个注解中存放了这个方法或类的地址名,可以通过这个注解的值拼接一个url,并把这个方法作为值,url作为键存放在一个集合中,有了这个集合,所有的请求都可以通过地址来访问对应的方法,这个方法再处理这个请求,并作出一些相应。从而就完成了请求响应的实现;
@Target(value= {ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value(); // 存放方法或类的地址名
}
- @Param,这个注解只能注解在参数上。这个注解的值是参数的名称;在进行请求的过程中,前端会传来一些参数,那么后端就需要接收这些参数,我们可以通过参数名来获取参数的值,如何获取参数的值,后面会进行详解;这就要求方法的参数必须要加这个注解,不然无法接收参数
@Target(value={ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
String value(); // 存放参数的名称
}
以上准备做好了,下面开始手写SpringMVC;以下分两步进行手写:
- IOC容器的实现;
- 请求响应数据的实现;
先写第一步:IOC容器的实现
写IOC容器之前,先明白三个问题
- 什么是IOC容器?
IOC容器就是使用IoC/DI容器反过来控制应用程序所需要的外部资源,是程序开发思想。 - 为什么要写IOC容器?
IOC容器的底层是一个Map集合,集合中的键是对象名,值是一个对象,这个对象的属性有对象名对应的对象以及对象的类型。在请求之前就已经把所有配置了的对象创建好了,要用的话直接从里面拿就行了。这个类型是表示这个对象是@Controller注解的对象,还是@Service注解的对象。区分好了@Controller,就能得到处理前端请求的对象,从而可以通过这个对象处理请求。 - 怎么写IOC容器?
接下来,我们来详细地看看实现IOC容器的过程
前端访问后端,首先要解析Web.xml文件,所以Web.xml文件中必须配置好servlet的信息,以及包名的信息。这个包名是存放业务类的包,后期可以通过包名得到所有的业务类,并对这些类进行筛选;
所以我们先配置这个web.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>MySpringmvc</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- 配置前端控制器开始 -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>com.young.spring.core.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>source.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name><u>springmvc</u></servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
<!-- 配置前端控制器结束 -->
</web-app>
标签中的标签中的内容是配置文件名,通过解析文件获取这个文件中的内容,这个配置文件中的内容是业务类所在的包。创建配置文件:source.properties
basePackage=com.young.business
标签中的内容是前端控制器的完全限定名,前端控制器是Spring的核心类;它的作用是解析配置文件,得到配置文件中的包的信息,再扫描这个包,得到所有的类,把这些加了@Controller或@Servlet注解的类全部实力化,并放在一个Map集合中,从而实现了IOC容器;
当然实现了IOC容器还有很多其他的功能,获得了IOC容器,就要创建HandlerMapping对象,即访问控制器的花名册,这个对象是一个Map集合,可通过url地址获得值,这个值是url访问的目标方法。以上功能是这一步需要实现的功能;
首先我们来实现这一步需要实现的功能
作为一个Servlet,DispatcherServlet必须先继承HttpServlet类;
请求发送过来后,首先访问的是它的init方法,所以先重写HttpServlet类的init方法;
@Override
private static final String BASE_PACKAGE = "basePackage";
public void init(ServletConfig config) throws ServletException {
// 1. 扫描并读取配置文件
doFile(config.getInitParameter(CONTEXT_CONFIGLOCATION));
// 2. 扫描用户设定的包下面的所有的类
doScanner(prop.getProperty(BASE_PACKAGE));
// 3. 根据className去实例化
try {
doInstance();
} catch (Exception e) {
e.printStackTrace();
}
// 4. 自动装配
try {
doAutoWired();
} catch (Exception e) {
e.printStackTrace();
}
// 5. 初始化HandlerMapping,解析controller里面的@RequestMapping注解
initHandlerMapping();
}
init方法需要扫描并读取配置文件,这个配置文件存放的是业务类所在的包,可通过web.xml中获取,通过标签中的值获取,获取方式:下面写init方法的,扫描并读取配置文件的方法 doFile(config.getInitParameter(CONTEXT_CONFIGLOCATION));即解析source.properties,并获取到他的内容,放入Properties集合中,properties集合中放的就是业务类所在的包名,通过包名可以获取到它里面所有的类
// 全局定义<param-name>标签中的值
private static final String CONTEXT_CONFIGLOCATION = "contextConfigLocation";
// 使用properties存放配置文件的内容
private Properties prop = new Properties();
private void doFile(String configLocation) {
InputStream in = DispatcherServlet.class.getClassLoader().getResourceAsStream(configLocation);
try {
prop.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
业务类所在的包存在properties中了,接下来我们要找到这个包下所有的类,创建一个List集合
- 首先获取文件中的内容
- 格式化这个包,把它变成地址
- 通过这个地址创建File对象;
- 扫描File,遇到文件,就将它的完全限定名放入集合中,
- 遇到文件夹就进入这个文件,重复步骤4
// 存放完全限定名的集合
private List<String> paths = new ArrayList<>();
private void doScanner(String basePackage) {
String newPath = basePackage.replaceAll("\\.", "/");
// resource 的值为file:/F:/WH_JAVA_190328/MySQL/apache-tomcat-8.5.41/webapps/MySpringmvc/WEB-INF/classes/com/young/business/
URL resource =DispatcherServlet.class.getClassLoader().getResource(newPath);
String filePath = resource.getFile();
File file = new File(filePath);
if(file.exists()){
File[] listFiles = file.listFiles();
for (File file2 : listFiles) {
if(file2.isDirectory()) {
doScanner(basePackage + "." + file2.getName());
}else {
// clazz是类名
String clazz = file2.getName().replace(".class", "");
// 包名+类名就组成了这个类的完全限定名
paths.add(basePackage +"." + clazz);
}
}
}
}
获取到所有的类完全限定名后,我们要扫描里面所有的被@Controller和@Service注释的类,并为他们创建对象,并把这些对象以及对象的注解类型放入一个HandlerObject对象中,再以对象名为键,HandlerObject对象为值放入Map集合中,一个IOC容器就形成了
- 创建一个HandlerObject类
public class HandlerObject {
private Object instance;
private String instanceType;
public HandlerObject() {
super();
// TODO Auto-generated constructor stub
}
public HandlerObject(Object instance, String instanceType) {
super();
this.instance = instance;
this.instanceType = instanceType;
}
public Object getInstance() {
return instance;
}
public void setInstance(Object instance) {
this.instance = instance;
}
public String getInstanceType() {
return instanceType;
}
public void setInstanceType(String instanceType) {
this.instanceType = instanceType;
}
@Override
public String toString() {
return "HandlerObject [instance=" + instance + ", instanceType=" + instanceType + "]";
}
}
- 先判断这个存放完全限定名的集合是否为空
- 如果不为空则执行如下操作
- 通过反射实例化这个注解
- 判断这个类的注解是否为@Controller或者@Service,如果是则创建对象,处理这个队形的完全限定名成一个对象名,并以对象名为键,对象为值存在集合中
- 如果为空则不进行任何操作
private Map<String,HandlerObject> ioc = new HashMap<>();
private void doInstance() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
if(!paths.isEmpty()) {
for (String className : paths) {
Class<?> clazz = Class.forName(className);
if(clazz.isAnnotationPresent(Controller.class)) {
Object newInstance = clazz.newInstance();
// 获取这个对象的类名
String simpleName = newInstance.getClass().getSimpleName();
// 将这个类的类名进行首字母小写处理成对象名
ioc.put(toLowerFirstWord(simpleName), new HandlerObject(newInstance, BeanType.CONTROLLER.name()));
}else if(clazz.isAnnotationPresent(Service.class)) {
Object newInstance = clazz.newInstance();
String simpleName = newInstance.getClass().getSimpleName();
ioc.put(toLowerFirstWord(simpleName), new HandlerObject(newInstance, BeanType.CONTROLLER.name()));
}
}
}
}
/**
* 把字符串的首字母小写
*
* @param name
* @return
*/
private String toLowerFirstWord(String name) {
char[] charArray = name.toCharArray();
charArray[0] += 32;
return String.valueOf(charArray);
}
创建完对象后,扫描这些对象,找出所有的被@AutoWired注释的对象,并为其赋值
- 判断IOC容器是否为空
2.如果不为空则进行如下操作- 扫描IOC容器,找到里面的所有对象
- 判断对象的属性中是否有@AutoWired,如果有则通过属性名从Ioc容器拿对象,给自己赋值
- 如果为空则啥也不做
private void doAutoWired() throws InstantiationException, IllegalAccessException {
if(!ioc.isEmpty()) {
for (HandlerObject handler : ioc.values()) {
Object instance = handler.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();
// 判断IOC容器中是否有这个对象名对应的对象
if(ioc.containsKey(name)) {
// 有,则给自己赋值
field.setAccessible(true);
field.set(instance, ioc.get(name).getInstance());
}else {
// 没有,则打印异常
System.err.println("IOC容器里面没有"+name+"对应的象");
}
}
}
}
}
}
}
创建HandlerMapping对象,将url值为键,HandlerMapping对象为值存入一个Map集合中,这个集合即花名册,往后可以通过url获取HandlerMapping对象,实现对目标方法的调用和赋值。以下我们来创建这个花名册:
- 应为要调用对应的方法,所有HandlerMapping对象应有一个Object型的属性以及一个Method属性,便于调用方法。创建HandlerMapping类
public class HandlerMapping {
private Object instance;
private Method method;
public HandlerMapping() {
super();
// TODO Auto-generated constructor stub
}
public HandlerMapping(Object instance, Method method) {
super();
this.instance = instance;
this.method = method;
}
public Object getInstance() {
return instance;
}
public void setInstance(Object instance) {
this.instance = instance;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
@Override
public String toString() {
return "HandlerMapping [instance=" + instance + ", method=" + method + "]";
}
}
- 扫描IOC容器,获取里面的所有的被@Controller注释的类,
- 每次扫描到@Controller注释的类时,判断这个类是否有@RequestMapping注释,如果有则获取它的值,拼接到path变量上
- 扫描这个类的所有被@RequestMapping注释的方法,获取它的值并拼接到path变量上,得到了访问这个方法的路径path,将这个方法和他的实例放入一个HandlerMapping对象中,并以path为键,HandlerMapping对象为值放入Map集合中
private Map<String,HandlerMapping> handlerMapping = new HashMap<>();
private void initHandlerMapping() {
if(!ioc.isEmpty()) {
for (HandlerObject handler : ioc.values()) {
Object instance = handler.getInstance();
String path = "";
// 判断类上是否有@RequestMapping注解,有则获取它的值
if(instance.getClass().isAnnotationPresent(RequestMapping.class)) {
RequestMapping declaredAnnotation = instance.getClass().getDeclaredAnnotation(RequestMapping.class);
path += "/" + declaredAnnotation.value();
}
// 获取这个类上的所有方法
Method[] declaredMethods =instance.getClass().getDeclaredMethods();
if(null != declaredMethods && declaredMethods.length > 0) {
for (Method method : declaredMethods) {
// 判断这个方法是否有@RequestMapping注解,有则获取它的值
if(method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping declaredAnnotation2 = method.getDeclaredAnnotation(RequestMapping.class);
// 拼接路径
path += "/" + declaredAnnotation2.value();
HandlerMapping handlerMapping2 = new HandlerMapping();
handlerMapping2.setInstance(instance);
handlerMapping2.setMethod(method);
handlerMapping.put(path, handlerMapping2);
}
}
}
}
}
}
以上第一步就完成了,接下来就是接收请求,并处理请求
2. 请求响应数据的实现
前端向后端发送请求,会先访问它的service方法,service方法在通过解析请求把它发送给doGet方法或doPost方法,这里为简化,把所有的请求都使用doPost方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
如何接收请求并处理请求呢?可分为以下几个步骤:
- 从handlerMapping集合中获取HandlerMapping对象
- 获取HandlerMapping对象中的对象,以及方法
- 获取前端传过来中的所有参数值,得到参数值的数组;
- 获取参数列表的所有注解
- 遍历注解,找到@param注解,并获取它的值
- 用这个值给从参数值的数组中获得参数值
- 创建数组存放这些参数值
- 调用对象,将返回值返回出去
private Object doDispatch(HttpServletRequest req,
HttpServletResponse resp) {
String requestURI = req.getRequestURI();
// 处理URI
String fileName = req.getServletContext().getContextPath();
requestURI = requestURI.replaceAll(fileName, "");
requestURI = requestURI.substring(0, requestURI.lastIndexOf("."));
// 判断是否存在这个方法
if(handlerMapping.containsKey(requestURI)) {
HandlerMapping handler = handlerMapping.get(requestURI);
Method method = handler.getMethod();
Object instance = handler.getInstance();
// 获取传递的参数值
Map<String, String[]> parameterMap =
req.getParameterMap();
// 获取参数列表
Parameter[] parameters = method.getParameters();
// 获取参数的所有注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// 创建存放参数值的数组
Object[] param = new Object[parameters.length];
int index = 0;
// 解析注解,通过注解的值获取传过来的参数值,并放入参数值的数组中
for (Annotation[] annotations : parameterAnnotations) {
for (Annotation annotation : annotations) {
if(annotation instanceof Param) {
Param anno = (Param)annotation;
String value = anno.value();
// 判断value值
if(value.equals("request")) {
param[index] = req;
}else if(value.equals("response")) {
param[index] = resp;
}else {
String[] strings = parameterMap.get(value);
param[index] = strings[0];
}
}
}
index++;
}
// 给method赋值
try {
Object invoke = method.invoke(instance, param);
return invoke;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return null;
}
处理请求后要给前端发送响应,接收方法的返回值,判断他返回的是字符串还是ModelAndView。再判断它是内部转发还是重定向,再执行相应的操作
首先创建ModelAndView类
public class ModelAndView {
private String viewName;
// data中存放的是放在作用域中的值
private static Map<String,Object> data = new HashMap<>();
// 实现ModelAndView的addAttribute方法
public void addAttribute(String name, Object obj) {
data.put(name, obj);
}
public ModelAndView() {
super();
}
public ModelAndView(String viewName) {
super();
this.viewName = viewName;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getData() {
return data;
}
public void setData(Map<String, Object> data) {
this.data = data;
}
}
处理响应
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Object obj = doDispatch(req,resp);
if(null != obj) {
if(obj instanceof String ) {
// 判断是否是重定向
if(obj.toString().startsWith("redirect:")) {
resp.sendRedirect(obj.toString());
}else {
req.getRequestDispatcher(obj.toString()).forward(req, resp);
}
}else if(obj instanceof ModelAndView) {
ModelAndView model = (ModelAndView) obj;
String viewName = model.getViewName();
// 判断是否是重定向
if(viewName.toString().startsWith("redirect:")) {
resp.sendRedirect(viewName.toString());
}else {
Set<Entry<String, Object>> entrySet = model.getData().entrySet();
for (Entry<String, Object> entry : entrySet) {
req.setAttribute(entry.getKey(), entry.getValue());
}
req.getRequestDispatcher(viewName.toString()).forward(req, resp);
}
}
}
}
把以上所有的方法组合在一个DispatcherServlet类中,这样就创建好了SpringMVC的一个核心类
总结一下:
- IOC容器在加载文件的时候就已经把所有的配置好的类创建好了对象
- 对象是通过对象名从IOC容器中获取的;
- IOC容器的底层是一个Map集合
- Servlet在首次载入时,执行复杂的初始化任务,但不想每个请求都重复使用这些任务时,用init方法,它在servlet初次创建的时候被调用,之后处理每个用户的请求时,则不再调用这个方法。
- 前端传过来的请求要先经过前端控制器,前端控制器再通过URL来获取HandlerMapping对象,即花名册,使用花名册找到对应的方法