为了更好了Spring以及SpringMVC的核心原理,本文参考其他书籍实现了一个简易版的SpringMVC框架,框架的构建过程如下:
-
创建个Maven工程,并在pom.xml文件中引入Servlet;
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> </dependency>
-
工程结构如下:
-
在web.xml里配置前端控制器DispatcherServlet,用于拦截所有的请求,并在初始化参数里配置application.properties文件的路径
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.framework.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</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>
- application.properties文件中只填写包扫描路径即可
scanPackage=com.demo
- 创建各种注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
String value() default "";
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
String value() default "";
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Service {
String value() default "";
}
- 创建一组业务处理逻辑,用DemoController接收请求并返回DemoService的处理结果给浏览器
@Controller
@RequestMapping("/demo")
public class DemoController {
@Autowired
DemoService demoService;
@RequestMapping("/query")
public void query(HttpServletRequest request, HttpServletResponse response, @RequestParam("name") String name) throws IOException {
String result= demoService.get(name);
response.getWriter().write(result);
}
}
public interface DemoService {
String get(String name);
}
@Service
public class DemoServiceImpl implements DemoService {
@Override
public String get(String name) {
return "My name is " + name;
}
}
-
接下来是重中之重:构造DispatcherServlet类
我们将在DispatcherServlet的init方法中进行:
- 加载配置文件
- 扫描所有的类
- 实例化所有单例Bean并放到IOC容器中
- 完成依赖注入
- 初始化HandlerMapping容器
然后,因为本框架只对Get请求进行处理,所以只重写doGet方法。
具体过程如下:
- 创建DispatcherServlet成员变量:
//加载application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描到的所有类名称
private List<String> classNames = new ArrayList<>();
//IOC容器,保存所有Bean实例
private Map<String, Object> ioc = new HashMap<>();
//handlerMapping容器,保存URL和Method的映射关系
private Map<String, Method> handlerMapping = new HashMap<>();
- 在DispatcherServlet中重写init方法并实现以下方法:
@Override
public void init(ServletConfig config) {
//加载配置文件
loadConfig(config.getInitParameter("contextConfigLocation"));
//包扫描
initScanner(contextConfig.getProperty("scanPackage"));
//实例化扫描到的类,并放入到IOC容器中
initBeans();
//完成依赖注入
initAutowired();
//建立所有URL和Controller中方法的对应关系,并保存到HandlerMapping容器中
initHandlerMapping();
}
- 实现loadConfig方法
private void loadConfig(String contextConfigLocation) {
//找到application.properties文件路径
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
//加载application.properties
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 实现initScanner方法,扫描com.demo包以及子包的类,并将类的全限定名称保存起来,为initBean实例化这些类做准备;
private void initScanner(String scanPackage) {
//扫描com.demo包以及子包的类
URL url = getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));
File classPath = new File(url.getFile());
for (File file: classPath.listFiles()) {
if (file.isDirectory()) {
initScanner(scanPackage + "." +file.getName());
} else {
if (!file.getName().endsWith(".class")) continue;
String className = scanPackage + "." + file.getName().replace(".class", "");
classNames.add(className);
}
}
}
- 通过遍历classNames中的路径名称,并以反射的方法实例化带有@Controller和@Service注解的类,最后把实例加入到IOC容器中;
private void initBeans() throws Exception {
if (classNames.isEmpty()) return;
for (String className: classNames) {
Class<?> clazz = Class.forName(className);
//加了注解的类才能初始化
if (clazz.isAnnotationPresent(Controller.class)) {
Object instance = clazz.newInstance();
ioc.put(clazz.getSimpleName(), instance);
} else if (clazz.isAnnotationPresent(Service.class)) {
Object instance = clazz.newInstance();
for (Class<?> i : clazz.getInterfaces()) {
if (ioc.containsKey(i.getName())) {
throw new RuntimeException(i.getName() + "已存在");
}
ioc.put(i.getName(), instance);
}
} else {
continue;
}
}
}
- 实现initAutowired方法,遍历IOC容器中所有的实例,找到实例中带有@Autowire注解的成员变量,这些成员变量都是对象的引用,还没用进行赋值,所以用反射的方法将这些变量进行引用赋值。
private void initAutowired() throws IllegalAccessException {
if (ioc.isEmpty()) return;
for (Map.Entry<String, Object> entry: ioc.entrySet()) {
//获取被@Autowire注解的字段
Field[] declaredFields = entry.getValue().getClass().getDeclaredFields();
for (Field field: declaredFields) {
if (!field.isAnnotationPresent(Autowired.class)) continue;
//根据变量类型获取类路径
String beanName = field.getType().getName();
//设置暴力访问
field.setAccessible(true);
//将变量所注入的类和根据类路径查找到的实例设置给这个变量
field.set(entry.getValue(), ioc.get(beanName));
}
}
}
- 初始化HandlerMapping,它是SpringMVC中完成URL到Controller映射的组件,实质上是一个Map,容器初始化会建立所有URL和Controller中方法的对应关系,保存到HandlerMapping<URL, Method>中,用户请求时根据请求的URL快读定位到Controller中的某个方法。确定了方法后,就把请求参数绑定到方法的形参上。
private void initHandlerMapping() {
if (ioc.isEmpty()) return;
for (Map.Entry<String, Object> entry: ioc.entrySet()) {
Class<?> clazz = entry.getValue().getClass();
if (!clazz.isAnnotationPresent(Controller.class)) continue;
//保存类上面的@RequestMapping("/demo")
String baseUrl = "";
if (clazz.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
baseUrl = requestMapping.value();
}
//获取方法上的Url
for (Method method: clazz.getMethods()) {
if (!method.isAnnotationPresent(RequestMapping.class)) continue;
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
String fullUrl = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+", "/");
//把URL和对应的方法放到HandlerMapping容器中
handlerMapping.put(fullUrl, method);
}
}
}
- 重写doGet方法,将请求转发给doDispatch方法进行处理
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replaceAll(contextPath, "").replaceAll("/+", "/");
//handlerMapping中查到请求的URL,没有就返回404
if (!handlerMapping.containsKey(url)) {
resp.getWriter().write("404 Not Found");
return;
}
//通过url找到对应的方法,用反射的方式知道方法对应的实例对象,并执行这个方法
Method method = handlerMapping.get(url);
Map<String, String[]> parameterMap = req.getParameterMap();
String beanName = method.getDeclaringClass().getSimpleName();
method.invoke(ioc.get(beanName), req, resp, parameterMap.get("name")[0]);
}
通过以上步骤,我们完成了简易版SpringMVC框架的构建,下面看看运行结果:
- 访问路径为http://localhost:8080/mvc_write_war/时,返回404
- 访问路径为http://localhost:8080/mvc_write_war/demo/query?name=peter