一、创建Maven工程
创建出来的工程结构如下
java以及resources文件夹为后面新增
二、创建对应的目录结构
其中,cn.spectrumrpc.springmvc为手写的springmvc代码所在,cn.spectrumrpc.test为测试手写mvc的测试代码
annotation包下存放自定义注解,包括@Controller,@Service,@RequestMapping等
context包下存放自定义的一个简单的Spring容器,用来存储@Controller,@Service这些Bean
handler包下存放自定义的Handler,用来保存请求url与Controller的映射关系
servlet包下存放DispatcherServlet,类比于SpringMVC中的中央控制器
controller包下存放测试的Controller类
service包下存放测试的Service类
三、创建自定义注解/mvc配置文件/controller等
3.1 自定义注解
由于自定义注解相对较多,且没啥变化,这里不贴代码,详见最后的工程代码
3.2 mvc配置文件
配置扫描Controller&Service包
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<component-scan base-package="com.spectrumrpc.test.controller,com.spectrumrpc.test.service"/>
</beans>
3.3 Controller
@Controller
public class UserController {
@Autowired("userService")
private UserService userService;
@RequestMapping("/hello")
public String hello() {
return userService.hello();
}
}
3.4 Service
public interface UserService {
String hello();
}
@Service(value = "userService")
public class UserServiceImpl implements UserService {
@Override
public String hello() {
return "hello,my-mvc";
}
}
四、创建DispatcherServlet,将其注册到web.xml中
web.xml
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>cn.spectrumrpc.springmvc.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
五、创建Web容器,保存springmvc的配置文件路径
public class WebApplicationContext {
private String configLocation;
public WebApplicationContext() {
}
public WebApplicationContext(String configLocation) {
this.configLocation = configLocation;
}
}
六、刷新Web容器,将Controller&Service扫描进入到容器中
首先,需要读取springmvc的配置文件,此处通过Dom4J,读取XML配置,获取component-scan对应的节点中的base-package属性(即,这个方法返回的是[com.spectrumrpc.test.controller,com.spectrumrpc.test.service])
XML解析工具
public class XmlParser {
public static String getBasePackage(String xml){
try {
SAXReader saxReader=new SAXReader();
InputStream inputStream = XmlParser.class.getClassLoader().getResourceAsStream(xml);
//XML文档对象
Document document = saxReader.read(inputStream);
Element rootElement = document.getRootElement();
Element componentScan = rootElement.element("component-scan");
Attribute attribute = componentScan.attribute("base-package");
String basePackage = attribute.getText();
return basePackage;
} catch (DocumentException e) {
e.printStackTrace();
} finally {
}
return "";
}
}
将读取到的包路径,进行扫描,保存到容器中
其中,包含如下几步
- 循环所有的包路径,将所在包以及子包下的类,通过反射进行初始化,保存到集合中
- 进行Controller类的属性赋值,将Controller中包含@Autowired的属性,从ioc容器中取出赋值。
- 这几部操作完成之后,此时的beans集合中,即包含了所有的Controller以及Service。 ```java package cn.spectrumrpc.springmvc.context;
import cn.spectrumrpc.springmvc.annotation.Autowired; import cn.spectrumrpc.springmvc.annotation.Controller; import cn.spectrumrpc.springmvc.annotation.Service; import cn.spectrumrpc.springmvc.xml.XmlParser;
import java.io.File; import java.lang.reflect.Field; import java.net.URL; import java.util.HashMap; import java.util.Map; public class WebApplicationContext {
private String configLocation;
private Map<String, Object> beans = new HashMap<>();
public WebApplicationContext() {
}
public Map<String, Object> getBeans() {
return beans;
}
public void setBeans(Map<String, Object> beans) {
this.beans = beans;
}
public WebApplicationContext(String configLocation) {
this.configLocation = configLocation;
}
public void onRefresh() {
//
String basePackage = XmlParser.getBasePackage(configLocation.split(":")[1]);
String[] packages = basePackage.split(",");
for (String pack : packages) {
// 扫描包下的类,并通过反射进行初始化
scanAndInit(pack);
}
// 将Controller中的Service属性,从ioc中取出,设置进去
propertiesSet();
}
private void propertiesSet() {
try {
for (String beanName : beans.keySet()) {
Object o = beans.get(beanName);
Field[] declaredFields = o.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
if (declaredField.isAnnotationPresent(Autowired.class)) {
Autowired autowired = declaredField.getAnnotation(Autowired.class);
String value = autowired.value();
declaredField.setAccessible(true);
declaredField.set(o, beans.get(value));
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private void scanAndInit(String pack) {
System.out.println("pack = " + pack);
URL url = this.getClass().getClassLoader().getResource("/" + pack.replaceAll("\\.", "/"));
String path = url.getFile();
File dir = new File(path);
for (File f : dir.listFiles()) {
if (f.isDirectory()) {
//当前是一个文件目录,递归进行初始化
scanAndInit(pack + "." + f.getName());
} else {
//文件目录下文件 获取全路径
String className = pack + "." + f.getName().replaceAll(".class", "");
System.out.println("className = " + className);
try {
Class<?> clazz = Class.forName(className);
// Controller的名字,默认采用,类名首字母小写,放入容器。Service采用注解配置的value
if (clazz.isAnnotationPresent(Controller.class)) {
//控制层 bean
String beanName = clazz.getSimpleName().substring(0, 1).toLowerCase() + clazz.getSimpleName().substring(1);
beans.put(beanName, clazz.newInstance());
} else if (clazz.isAnnotationPresent(Service.class)) {
//Service层 bean
Service serviceAn = clazz.getAnnotation(Service.class);
String beanName = serviceAn.value();
beans.put(beanName, clazz.newInstance());
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}
}
}
<a name="8fOY4"></a>
# 七、在DispatcherServlet中进行容器的初始化,以及处理器映射器的初始化
在Servlet初始化的时候,进行ioc容器的创建,以及刷新,将Controller以及Service保存到容器中然后。
```java
@Override
public void init() throws ServletException {
// 1. 获取 web.xml中配置的init-param
String contextConfigLocation = this.getServletConfig().getInitParameter("contextConfigLocation");
// 2. 让ioc容器去加载这个配置文件
this.webApplicationContext = new WebApplicationContext(contextConfigLocation);
// 3. 刷新容器
webApplicationContext.onRefresh();
// 4. 保存url与Controller,Method的映射关系
initHandlerMappings();
}
对应的组件保存到容器中之后,将进行处理器关系的映射。
private void initHandlerMappings() {
// 从ioc容器中取出所有的组件
Map<String, Object> beans = webApplicationContext.getBeans();
for (String key : beans.keySet()) {
Object bean = beans.get(key);
Method[] declaredMethods = bean.getClass().getDeclaredMethods();
// 判断这些组件是否包含了@RequestMapping注解,如果包含了,说明对应请求
for (Method declaredMethod : declaredMethods) {
if (declaredMethod.isAnnotationPresent(RequestMapping.class)) {
// 然后通过Handler保存这些映射关系,让如集合中
RequestMapping requestMapping = declaredMethod.getAnnotation(RequestMapping.class);
String value = requestMapping.value();
MyHandler myHandler = new MyHandler(value, bean, declaredMethod);
handlers.add(myHandler);
}
}
}
}
当接收到请求时,通过请求路径与Handler中的路径进行比较,找出对应的Handler,如果找不到,则返回404,如果查找到了,则通过反射,调用对应的Method,完成一次请求。最后,通过函数的返回值,以及ResponseBody注解,来判断这个是一个跳转视图的请求,还是一次json前后端分离的请求,通过不同的格式,相应不同的数据。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
MyHandler handler = getHandler(req);
if (handler == null) {
resp.getWriter().print("404 NOT FOUNT");
} else {
try {
Method method = handler.getMethod();
Object invoke = method.invoke(handler.getController());
if (method.isAnnotationPresent(ResponseBody.class)) {
if (invoke instanceof String) {
resp.getWriter().write(invoke.toString());
} else {
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(JSONObject.toJSONString(invoke));
}
} else {
if (invoke.toString().startsWith("redirect:")) {
resp.sendRedirect(invoke.toString() + ".jsp");
} else {
req.getRequestDispatcher(invoke.toString().replaceFirst("forward:", "") + ".jsp").forward(req, resp);
}
}
} catch (IllegalAccessException | IOException | InvocationTargetException | ServletException e) {
e.printStackTrace();
}
}
}
private MyHandler getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
for (MyHandler handler : handlers) {
if (handler.getUrl().equals(requestURI)) {
return handler;
}
}
return null;
}
当然,此mvc源码,并未处理controller中的参数问题,
那么,接下来,我们就看看,参数如何进行处理
首先,都知道,通过method.getParameters(),可以获取到这个方法中的所有参数,在通过getName可以获取到参数的名称,但是,如果在默认情况下,获取到的是,arg0,arg1这种东西,根本没用。
在这种情况下,需要开启idea的一个功能,让其可以获取到真实的参数名称,详细步骤如下
在Settings下,添加 -parameters选项,即可获取到真实的参数名
然后,通过req.getParameter(name),根据真实的参数名称,从request中获取到请求参数,再通过反射调用即可。
for (int i = 0; i < parameters.length; i++) {
String name = parameters[i].getName();
System.out.println("name = " + name);
String param = req.getParameter(name);
params[i] = param;
}
Object invoke = method.invoke(handler.getController(), params);