为了能够深入理解SpringMVC,自己实现一个简单的"SpringMVC"。所有代码的实现都是为了快速构建SpringMVC的流程,理解要完成SpringMVC这样一个框架大概是什么样子的原理,而不是一比一的复刻。文中涉及到的源码有兴趣可以在评论联系我。
SpringMVC其实是在将Servlet中繁琐的web.xml简化成注解,以达到提高开发效率的目的。要实现这一目的,SpringMVC使用的方式其实是将web.xml的任务放到DispatcherServlet中去处理,这样就能避免用户配置web.xml。
功能分析
- 能够将@Controller标记的类中的方法作为具体请求的处理器
- 能够处理@RequestMapping的url映射
- 能够为@Controller标记的类中的请求处理器注入HttpServletRequest等必要的上下文信息
- 能够正确的返回数据给用户
流程分析
- 解析配置文件以读取basePackage等关键信息
- 递归扫描basePackage下的所有被@Controller标记的类
- 处理@RequestMapping的请求路径映射
- 注入处理器的参数
- 返回结果
代码实现
SpringMVC是建立在Servlet功能基础之上的,属于是Servlet的增强版本。所以项目也应该建立在基本的Servlet项目之上,只引入少数几个必须的包。
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
注解开发
这里模拟几个SpringMVC中至关重要的注解,比较简单就只把代码贴出来不做解释了。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
出了这几个SpringMVC中有的注解,再加入一个@Security来实现自定义MVC框架的权限控制功能。@Security中包含一个String[],用来存放用户名。我们通过在URL上加上?username=xxx
的方式来判断xxx是否在当前被@Security标记的方法上有访问权限。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Security {
String[] value() default "";
}
解析配置文件
MVC框架作为一个web框架,不难想到其初始化过程可以通过ServletListener监听启动事件触发,还可以在关键的Servlet的初始化中完成。这里使用的是在DispathcerServlet中完成初始化。
//新建一个DispatcherServlet类,并注册给web.xml
public class DispatcherServlet extends HttpServlet {
private final Properties properties = new Properties();
private final Set<String> annotatedClass = new HashSet<>();
private final Map<String, Object> beanContainer = new HashMap<>();
private final Map<String, Handler> urlMapping = new HashMap<>();
@Override
public void init(ServletConfig config) throws ServletException {
//init方法中可以从ServletConfig中读取web.xml配置的init-param参数
String configPath = config.getInitParameter("configLocation");
try {
//读取配置文件
loadConfig(configPath);
String basePath = properties.getProperty("servlet.dispatcher.scan.basepackage");
//扫描被@Controller标记的类
doScan(basePath);
//实例化被@Controller标记的类
doInitialize();
//处理依赖
resolveDependencies();
//处理@RequestMapping的url和Method的关系
doUrlMapping();
} catch (Exception e) {
e.printStackTrace();
}
}
private void doUrlMapping() throws Exception {
}
private void resolveDependencies() throws Exception {
}
private void doInitialize() throws Exception {
}
private void doScan(String basePath) throws Exception {
}
private void loadConfig(String configPath) throws Exception {
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
}
<web-app>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>pers.supermaskv.mvc.servlet.DispatcherServlet</servlet-class>
<!--将配置文件的路径作为初始化参数提供给DispatcherServlet-->
<init-param>
<param-name>configLocation</param-name>
<param-value>setup.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
setup.properties中的内容只有我们将要扫描的包
servlet.dispatcher.scan.basepackage=pers.supermaskv.mvc
到这里为止,我们所需要的启动信息就都处理完成了。接下来就是将配置文件的内容载入,然后读取其中的信息。在上面的DispatcherServlet中,我们已经创建了整个启动流程的框架,我们只需要依次的将逻辑填充的方法中即可。
//读取配置文件
private void loadConfig(String configPath) throws Exception {
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(configPath);
properties.load(inputStream);
}
//当配置文件被解析为Properties对象之后,我们就可以通过getProperty获取到刚刚配置在setup.properties中的base package。
String basePath = properties.getProperty("servlet.dispatcher.scan.basepackage");
扫描基础包下的所有类
我们已经在DispatcherServlet类中声明了一个HashSet<String>
作为的所有被@Controller标记的类的全限定名的集合,我们需要将扫描到符合条件的类名加入进去。
private void doScan(String basePath) throws Exception {
//getResource("").getPath()可以直接获得*包所在的目录,本项目的*包为pers。
//和要扫描的包的相对路径拼接起来就得到了要扫描的目录在文件系统中的绝对路径。
String packagePath = Thread.currentThread().getContextClassLoader().getResource("").getPath()
+ basePath.replaceAll("\\.", "/");
File path = new File(packagePath);
//获取所有的子目录和以.class为扩展名的文件
File[] files = path.listFiles(f -> f.isDirectory() || f.getName().endsWith(".class"));
for (File file : files) {
if (file.isDirectory()) {
//如果是子目录就递归调用doScan方法,故技重施
doScan(basePath + "." + file.getName());
} else {
String className = basePath + "." + file.getName().replaceAll(".class", "");
if (Class.forName(className).isAnnotationPresent(Controller.class)) {
//将被@Controller标记的类的全限定名加入到集合中,等待接下来的使用
annotatedClass.add(className);
}
}
}
}
实例化
使用反射技术将上面我们得到的全限定类名实例化为对象。beanContainer
也是在DispatcherServlet中声明的用来存放对象集合,整个逻辑比较简单就不解释了。
private void doInitialize() throws Exception {
for (String className : annotatedClass) {
Class<?> aClass = Class.forName(className);
Object o = aClass.getConstructor().newInstance();
beanContainer.put(className, o);
for (Class<?> anInterface : aClass.getInterfaces()) {
beanContainer.put(anInterface.getName(), o);
}
}
}
处理依赖对象
也就是被@Autowired标记的字段。这一步也不是这篇文章的重点,简要带过。
private void resolveDependencies() throws Exception {
for (String className : annotatedClass) {
Class<?> aClass = Class.forName(className);
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
String fieldName = field.getType().getName();
field.set(beanContainer.get(className), beanContainer.get(fieldName));
}
}
}
}
映射URL和Method的关系
既然DispatcherServlet要替代web.xml,那么也不可避免的要完成处理器和请求路径的映射关系。因为使用反射来调用方法比较啰嗦,所以用一个实体类Handler来辅助完成,最后将封装好的Handler和URL路径放到DispatcherServlet中声明的urlMapping集合中去。
//getter/setter方法已经省略
public class Handler {
private Object handlerInvokeObject;
private Method method;
private final List<Class<?>> parameterMapping = new ArrayList<>();
private Set<String> username = new HashSet<>();
public Object invoke(Object[] params) {
try {
return method.invoke(handlerInvokeObject, params);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
private void doUrlMapping() throws Exception {
for (String className : annotatedClass) {
Class<?> aClass = Class.forName(className);
String classRequestPath = "";
//处理标记在类上的@RequestMapping中的value()
if (aClass.isAnnotationPresent(RequestMapping.class)) {
classRequestPath = aClass.getAnnotation(RequestMapping.class).value();
}
Method[] methods = aClass.getDeclaredMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
String[] username = new String[0];
//顺便将@Security中的信息保存到handler中
if (method.isAnnotationPresent(Security.class)) {
username = method.getAnnotation(Security.class).value();
}
//处理标记在方法上的@RequestMapping中的value()
String methodRequestPath = method.getAnnotation(RequestMapping.class).value();
//将类上的路径和方法上的路径拼接在一起就完成了请求路径的构建
String url = classRequestPath + methodRequestPath;
//构建Handler对象
Handler handler = new Handler();
handler.setMethod(method);
handler.getParameterMapping().addAll(Arrays.asList(method.getParameterTypes()));
handler.getUsername().addAll(Arrays.asList(username));
handler.setHandlerInvokeObject(beanContainer.get(className));
//将请求路径和Handler对象的映射关系加入到urlMapping中
urlMapping.put(url, handler);
}
}
}
处理请求
上面所有的步骤都是在为我们处理请求做准备,真正的处理请求还是要在DispatcherServlet中的doPost()方法里完成。
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=utf-8");
// 获取请求url
String url = req.getRequestURI();
String username = req.getParameter("username");
// 根据url获取Handler对象
Handler handler = urlMapping.get(url);
if (handler == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 判断username是否在合法用户列表中
if (!handler.getUsername().contains(username) && handler.getUsername().size() > 0) {
resp.getWriter().write("无权限");
return;
}
// 根据ParameterMapping准备参数
List<Class<?>> parameterMapping = handler.getParameterMapping();
int len = parameterMapping.size();
Object[] objects = new Object[len];
for (int i = 0; i < len; i++) {
//这里只简单完成对request和response对象的注入
if (parameterMapping.get(i).equals(HttpServletRequest.class)) {
objects[i] = req;
} else if (parameterMapping.get(i).equals(HttpServletResponse.class)) {
objects[i] = resp;
} else {
objects = null;
}
}
// 注入参数
Object result = handler.invoke(objects);
// 将结果写入到resp中
resp.getOutputStream().write(result.toString().getBytes(StandardCharsets.UTF_8));
}
测试
为了简化测试就不使用@Autowired注解了,只编写一个最最简单的Controller来试试。
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/hello")
//只有用户名为"supermaskv"可以访问这个方法
@Security("supermaskv")
public String hello(HttpServletRequest request) {
System.out.println(request.getRequestURI());
return "hello";
}
}
总结
整个小demo完成了对SpringMVC中DispatcherServlet和HandlerMapping两大组件的简单实现,并实现了@Security作为方法级的用户权限控制的注解。