Java Servlet 教程-21-自己手写 spring mvc 简单实现
2018年9月27日大约 6 分钟
整体代码结构
├─java
│ └─com
│ └─github
│ └─houbb
│ └─mvc
│ │ package-info.java
│ │
│ ├─annotation
│ │ Controller.java
│ │ RequestMapping.java
│ │ RequestParam.java
│ │
│ ├─controller
│ │ IndexController.java
│ │
│ ├─exception
│ │ MvcRuntimeException.java
│ │
│ └─servlet
│ DispatchServlet.java
│
├─resources
│ application.properties
│
└─webapp
└─WEB-INF
web.xml
pom.xml 依赖
引入 servlet-api 相关的包,用户开发。
javax.servlet
javax.servlet-api
3.0.1
provided
注解
因为本次主要实现 dispatch 分发这个功能,所有的 ioc 并不是我们实现的重点。
关于 ioc,可以参见 spring ioc 实现
所以只实现了以下几个注解:
功能和 spring 保持一致,此处不再赘述。
@Controller
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
/**
* 对象的别名
* @return 路径
* @since 0.0.1
*/
String value() default "";
}
@RequestMapping
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequestMapping {
/**
* 映射 url 路徑
* @return 路径
* @since 0.0.1
*/
String value();
}
@RequestParam
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
/**
* 参数别称
* @return 路径
* @since 0.0.1
*/
String value();
}
属性配置文件
web.xml
首先看一下 web 程序最核心的文件 web.xml
SpringMvc
com.github.houbb.mvc.servlet.DispatchServlet
contextConfigLocation
application.properties
1
SpringMvc
/*
配置文件
这里主要有一个配置文件 application.properties
basePackage=com.github.houbb.mvc
spring-mvc 中一般是一个 app.xml,其他指定的基本也是扫描包等基础信息。
分发 Servlet
还有一个 DispatchServlet,用于处理各种请求。
也是本次实现的核心内容。
DispatchServlet
继承自 HttpServlet
public class DispatchServlet extends HttpServlet {
基本属性
后面实现会用到。
/**
* 实例 Map
*
* @since 0.0.1
*/
private Map controllerInstanceMap = new HashMap<>();
/**
* 请求方法 map
*
* @since 0.0.1
*/
private Map requestMethodMap = new HashMap<>();
/**
* 配置文件
*
* @since 0.0.1
*/
private Properties properties = new Properties();
重载父类方法
@Override
public void init(ServletConfig config) throws ServletException {}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {}
实现 init 方法
init 方法主要负责解析 web.xml 中的配置,然后进行相关的初始化。
@Override
public void init(ServletConfig config) throws ServletException {
super.init();
//1. 加载配置文件信息
initConfig(config);
//2. 根据配置信息进行相关处理
String basePackage = properties.getProperty("basePackage");
initInstance(basePackage);
//3. 初始化映射关系
initRequestMappingMap();
}
我们分开一个个看。
1. 加载配置文件信息
这个就是根据 web.xml 中的配置,初始化一下配置文件信息。
/**
* 初始化配置信息
* (1)spring-mvc 一般是指定一个 xml 文件。
* 至于各种 classpath,我们也可以对其进行特殊处理,暂时简单化。
*
* @param config 配置信息
* @since 0.0.1
*/
private void initConfig(final ServletConfig config) {
final String configPath = config.getInitParameter("contextConfigLocation");
//把web.xml中的contextConfigLocation对应value值的文件加载到流里面
try (InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(configPath)) {
properties.load(resourceAsStream);
} catch (IOException e) {
throw new MvcRuntimeException(e);
}
}
- MvcRuntimeException 异常类
一个简单的自定义运行时异常类:
public class MvcRuntimeException extends RuntimeException {
//....
}
2. 根据配置信息进行初始化
根据指定的扫描包,我们初始化所有指定 @Controller
注解的类。
当然这里最简单的场景,还有很多复杂的情况,比如 jar 包中引用等等,可以参考 spring ioc 实现
/**
* 初始化对象实例
*
* @param basePackage 基本包
* @since 0.0.1
*/
private void initInstance(final String basePackage) {
String path = basePackage.replaceAll("\\.", "/");
URL url = this.getClass().getClassLoader().getResource(path);
if (null == url) {
throw new MvcRuntimeException("base package can't loaded!");
}
File dir = new File(url.getFile());
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
//递归读取包
initInstance(basePackage + "." + file.getName());
} else {
String className = basePackage + "." + file.getName().replace(".class", "");
//实例化处理
try {
Class clazz = Class.forName(className);
if (clazz.isAnnotationPresent(Controller.class)) {
Object instance = clazz.newInstance();
controllerInstanceMap.put(className, instance);
}
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throw new MvcRuntimeException(e);
}
}
}
}
}
3. 初始化映射关系
这个主要是解析 @RequestMapping
对应的 url 信息。
/**
* 初始化 {@link com.github.houbb.mvc.annotation.RequestMapping} 的方法映射
*
* @since 0.0.1
*/
private void initRequestMappingMap() {
for (Map.Entry entry : controllerInstanceMap.entrySet()) {
Object instance = entry.getValue();
String prefix = "/";
final Class controllerClass = instance.getClass();
if (controllerClass.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = (RequestMapping) controllerClass.getAnnotation(RequestMapping.class);
prefix = requestMapping.value();
}
// 暂时只处理当前类的方法
Method[] methods = controllerClass.getDeclaredMethods();
// 为了简单,只有注解处理的方法才被作为映射。
// 当然这里可以加一些限制,比如只处理 public 方法等。
// 可以加一些严格的判重,暂不处理。
for (Method method : methods) {
if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String methodUrl = requestMapping.value();
String fullUrl = prefix + methodUrl;
requestMethodMap.put(fullUrl, method);
}
}
}
}
请求分发
请求主要分为 get/post 两种:
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doDispatch(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doDispatch(req, resp);
}
这里我们统一调用了消息分发接口。
doDispatch() 核心实现
/**
* 执行消息的分发
*
* @param req 请求
* @param resp 响应
* @since 0.0.1
*/
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException {
try {
if (requestMethodMap.isEmpty()) {
return;
}
// 请求信息 url 的处理
String requestUrl = req.getRequestURI();
System.out.println("requestUrl====" + requestUrl);
String contextPath = req.getContextPath();
// 直接替换掉 contextPath,感觉这样不太好。可以选择去掉这种开头。
requestUrl = requestUrl.replace(contextPath, "");
//404
// 这里应该有各种响应码的处理,暂时简单点。
if (!requestMethodMap.keySet().contains(requestUrl)) {
resp.getWriter().write("404 for request " + requestUrl);
return;
}
Method method = requestMethodMap.get(requestUrl);
// 参数,这里其实要处理各种基本类型等
// 还有各种信息的类型转换
Class[] paramTypes = method.getParameterTypes();
// 参数为空的情况
final Object instance = controllerInstanceMap.get(method.getDeclaringClass().getName());
if (paramTypes.length paramNames = getParamNames(method);
Map requestParamMap = req.getParameterMap();
for (int i = 0; i getParamNames(final Method method) {
Annotation[][] paramAnnos = method.getParameterAnnotations();
List paramNames = new ArrayList<>(paramAnnos.length);
final int paramSize = paramAnnos.length;
for(int i = 0; i
org.apache.tomcat.maven
tomcat7-maven-plugin
2.2
8080
/
${project.build.sourceEncoding}
页面访问
- 404
tomcat7 启动以后,直接页面访问 http://localhost:8080/
默认页面显示
404 for request /
因为我们没有处理默认的 index 页面。
页面访问 http://localhost:8080/index/print?param=hello
会在控台打印
hello
- echo
页面访问 http://localhost:8080/index/echo?param=hello
会在页面打印
Echo :hello
开源地址
完整代码地址 mvc
贡献者
binbin.hou