项目地址
SpringMVC_03
觉得博主还可以给个Star
项目目录
pom.xml
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.0</version>
</dependency>
动手写代码
首先,看过前面的文章(SpringMVC源码分析------关键源码分析),都应该知道SpringMVC的容器注册,还有前端访问请求的处理了。
那下面我们开始来一步一步的实现SpringMVC的逻辑
- 容器注册
前面可能没有提及到容器的注册,在这里补充一下吧。
SpringMVC的核心分发器DispatcherServlet继承自HttpServletBean,HttpServletBean重写了Servlet的init方法
那么我们就使用HttpServletBean继承HttpServlet来重写init()方法。
创建HttpServletBean.java并继承HttpServlet,我们再看到源码中的HttpServletBean的init()方法。
调用了initServletBean()方法,那么我们也调用这个方法。并且可以发现在HttpServletBean中initServletBean()只是个模板方法,我们一样模仿
HttpServletBean.java
package com.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
/**
* @author 龙小虬
* @date 2021/3/15 10:20
*/
public class HttpServletBean extends HttpServlet {
@Override
public void init() throws ServletException {
initServletBean();
}
protected void initServletBean() {
}
}
再来寻找,谁重写了initServletBean()方法,
这里可以看到是FrameworkServlet重写了此方法,我们进去看看,他重写的方法主要做的什么。
进去看initWebApplicationContext()方法
他调用了onRefresh()方法,并且这个方法在本类中是个抽象方法
那么我们创建FrameworkServlet,java并加入如下代码
package com.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 龙小虬
* @date 2021/3/15 10:28
*/
public class FrameworkServlet extends HttpServletBean{
@Override
protected void initServletBean() {
onRefresh();
}
protected void onRefresh() {
}
}
我们可以发现,这个方法被DispatcherServlet重写。
那么我们进入去看看
可以看到onRefresh()调用了initStrategies(),而initStrategies()就在下面,并且有一大堆的initxxx(),但是我们之前在上一篇文章提到过RequestMappingHandlerMapping,这个和下面的HandlerMappings很相似吧,其实这个就是信息注册。
那么我们就可以自己创建DispatcherServlet.java
public class DispatcherServlet extends FrameworkServlet{
@Override
protected void onRefresh() {
initStrategies();
}
protected void initStrategies() {
initHandlerMappings();
}
private void initHandlerMappings() {
// 初始化容器
System.out.println(">>>初始化initHandlerMappings对象<<<");
}
}
好了,容器创建的步骤就出来了,我们再去看看容器是利用什么方法进行创建的。
我们利用SpringMVC源码分析------关键源码分析的代码来测试查看,在DispatcherServlet的initHandlerMappings()方法之中打开断点调试。
debug可以看到。在handlerMappings中有注册我们的url数据。并且数据来自RequestMappingHandlerMapping类。
那么我们就利用RequestMappingHandlerMapping这个类来进行数据注入,将url,method存放(因为项目只是demo,所以拦截器什么的,在此处已忽略)
那么我们怎么去识别他是否为接口类呢?当然是使用注解去识别。我们一般使用的注解有:@ComponentScan、@RequestMapping、@Controller。我们先创建这三个注解。
package com.annotation;
import java.lang.annotation.*;
/**
* @author 龙小虬
* @date 2021/3/15 10:41
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE})
@Documented
public @interface ComponentScan {
String value();
}
package com.annotation;
import java.lang.annotation.*;
/**
* @author 龙小虬
* @date 2021/3/15 10:43
*/
@Target({
ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value();
}
package com.annotation;
import java.lang.annotation.*;
/**
* @author 龙小虬
* @date 2021/3/15 10:39
*/
@Target({
ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Controller {
}
我们现在创建RequestMappingHandlerMapping.java,并进行逻辑处理,将url,method注入。并且实现查询功能,以url为key在map集合中查询。
package com.web;
import com.annotation.ComponentScan;
import com.annotation.Controller;
import com.annotation.RequestMapping;
import com.config.SpringMVCConfig;
import com.method.HandlerMethod;
import com.utils.ReflexUtils;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author 龙小虬
* @date 2021/3/15 11:30
*/
public class RequestMappingHandlerMapping {
private final Map<String, HandlerMethod> registryMapping = new HashMap<String, HandlerMethod>();
// 初始化mvc容器
public void registryMapping(){
// 1.获取@ComponentScan注解的类名
ComponentScan declaredAnnotation = SpringMVCConfig.class.getDeclaredAnnotation(ComponentScan.class);
// 若没有就跳出
if(declaredAnnotation == null){
return;
}
String springmvcPackage = declaredAnnotation.value();
// 有注解但没有value跳出
if(StringUtils.isEmpty(springmvcPackage)){
return;
}
// 2.使用java反射机制获取类上加有@Controller注解的类
Set<Class<?>> classes = ReflexUtils.getClasses(springmvcPackage);
// 3.遍历每一个类 查找类上是否加有RequestMapping注解
for (Class<?> c: classes) {
// 查找类上的注解
Controller controller = c.getDeclaredAnnotation(Controller.class);
// 若没有就查看下一个类
if(controller == null){
continue;
}
// 获取含有@Controller的类的所有方法
Method[] declaredMethods = c.getDeclaredMethods();
for (Method m : declaredMethods) {
RequestMapping requestMapping = m.getDeclaredAnnotation(RequestMapping.class);
// 若方法上含有@RequestMapping就获取其value
if(requestMapping != null){
String url = requestMapping.value();
// 获取value并且将value和对象 put到registryMapping 对象必须实例化
registryMapping.put(url,new HandlerMethod(m,newInstance(c)));
}
}
}
}
/**
* 在registryMapping中通过url查找相应的对象和方法
* @param url
* @return
*/
public HandlerMethod getHandler(String url) {
return registryMapping.get(url);
}
private Object newInstance(Class classInfo) {
try {
Object value = classInfo.newInstance();
return value;
} catch (Exception e) {
return null;
}
}
}
在上面的类中有使用一个工具类,java反射工具类ReflexUtils。
ReflexUtils.java
package com.utils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author 龙小虬
* @date 2021/3/15 10:45
*/
public class ReflexUtils {
/**
* 从包package中获取所有的Class
*
* @param pack
* @return
*/
public static Set<Class<?>> getClasses(String pack) {
// 第一个class类的集合
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
// 是否循环迭代
boolean recursive = true;
// 获取包的名字 并进行替换
String packageName = pack;
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(
packageDirName);
// 循环迭代下去
while (dirs.hasMoreElements()) {
// 获取下一个元素
URL url = dirs.nextElement();
// 得到协议的名称
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
System.err.println("file类型的扫描");
// 获取包的物理路径
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件 并添加到集合中
findAndAddClassesInPackageByFile(packageName, filePath,
recursive, classes);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
// 定义一个JarFile
System.err.println("jar类型的扫描");
JarFile jar;
try {
// 获取jar
jar = ((JarURLConnection) url.openConnection())
.getJarFile();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
// 同样的进行循环迭代
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 如果前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 如果以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx)
.replace('/', '.');
}
// 如果可以迭代下去 并且是一个包
if ((idx != -1) || recursive) {
// 如果是一个.class文件 而且不是目录
if (name.endsWith(".class")
&& !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(
packageName.length() + 1, name
.length() - 6);
try {
// 添加到classes
classes.add(Class
.forName(packageName + '.'
+ className));
} catch (ClassNotFoundException e) {
// log
// .error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
} catch (IOException e) {
// log.error("在扫描用户定义视图时从jar包获取文件出错");
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
/**
* 以文件的形式来获取包下的所有Class
*
* @param packageName
* @param packagePath
* @param recursive
* @param classes
*/
public static void findAndAddClassesInPackageByFile(String packageName,
String packagePath, final boolean recursive, Set<Class<?>> classes) {
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
// log.warn("用户定义包名 " + packageName + " 下没有任何文件");
return;
}
// 如果存在 就获取包下的所有文件 包括目录
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
public boolean accept(File file) {
return (recursive && file.isDirectory())
|| (file.getName().endsWith(".class"));
}
});
// 循环所有文件
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findAndAddClassesInPackageByFile(packageName + "."
+ file.getName(), file.getAbsolutePath(), recursive,
classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0,
file.getName().length() - 6);
try {
// 添加到集合中去
//classes.add(Class.forName(packageName + '.' + className));
//这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
现在我们写完了扫描的逻辑处理,但是扫包范围、Controller都没有。我们先创建一个扫包管理的config。
SpringMVCConfig.java
package com.config;
import com.annotation.ComponentScan;
/**
* @author 龙小虬
* @date 2021/3/15 10:47
*/
@ComponentScan("com.controller")
public class SpringMVCConfig {
}
MyController.java
package com.controller;
import com.annotation.Controller;
import com.annotation.RequestMapping;
/**
* @author 龙小虬
* @date 2021/3/15 10:42
*/
@Controller
public class MyController {
@RequestMapping("/pay")
public String test(){
return "test";
}
}
好了,现在我们来进行前端访问的逻辑处理。
我们都知道DispatcherServlet是前端控制器。所有逻辑都在这里。而且在SpringMVC源码分析------关键源码分析我们也提到过。他是先经过FrameworkServlet重写了HttpServlet的service(),然后在调用doService()。DispatcherServlet再重写doService(),之后调用doDispatch()。那么我们需要先完善FrameworkServlet。加入代码之后:
FrameworkServlet.java
package com.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 龙小虬
* @date 2021/3/15 10:28
*/
public class FrameworkServlet extends HttpServletBean{
@Override
protected void initServletBean() {
onRefresh();
}
protected void onRefresh() {
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doService(req,resp);
}
protected void doService(HttpServletRequest req, HttpServletResponse resp) {
}
}
DispatcherServlet.java增加代码。重写doService方法,并调用doDispatch()。初始化容器
package com.servlet;
import com.method.HandlerMethod;
import com.view.ModelAndView;
import com.web.RequestMappingHandlerMapping;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author 龙小虬
* @date 2021/3/15 10:32
*/
public class DispatcherServlet extends FrameworkServlet{
RequestMappingHandlerMapping requestMappingHandlerMapping;
public DispatcherServlet() {
requestMappingHandlerMapping = new RequestMappingHandlerMapping();
}
@Override
protected void onRefresh() {
initStrategies();
}
protected void initStrategies() {
initHandlerMappings();
}
private void initHandlerMappings() {
// 初始化容器
System.out.println(">>>初始化initHandlerMappings对象<<<");
requestMappingHandlerMapping.registryMapping();
}
@Override
protected void doService(HttpServletRequest req, HttpServletResponse resp) {
try {
doDispatch(req,resp);
} catch (Exception e) {
e.printStackTrace();
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
}
}
在之前文章详细的了解到了doDispatch()进行的逻辑处理(这里只进行简单的处理,所以只有几步)
- 获取url
- 查找url
- 执行目标方法
- 渲染页面
首先获取urlString url = req.getRequestURI();
再来查找url是否存在HandlerExecutionChain handler = getHandler(url);
源码中getHandler()方法分离了。
public HandlerExecutionChain getHandler(String url) {
HandlerMethod handler = requestMappingHandlerMapping.getHandler(url);
if(handler == null){
return null;
}
return new HandlerExecutionChain(handler);
}
在查找url的时候获取的具体handler,是利用进行了包装,并且使用了反射执行目标方法,我们先写出HandlerExecutionChain.java
package com.servlet;
import com.method.HandlerMethod;
import com.view.ModelAndView;
import java.lang.reflect.Method;
/**
* @author 龙小虬
* @date 2021/3/15 14:32
*/
public class HandlerExecutionChain {
HandlerMethod handlerMethod;
public HandlerExecutionChain(HandlerMethod handlerMethod) {
this.handlerMethod = handlerMethod;
}
public ModelAndView handler() throws Exception{
Method method = handlerMethod.getMethod();
Object bean = handlerMethod.getBean();
// 因为我们的contrller只用了string,所以直接强转了
String invoke = (String)method.invoke(bean, null);
ModelAndView modelAndView = new ModelAndView(invoke);
return modelAndView;
}
}
ModelAndView.java
package com.view;
/**
* @author 龙小虬
* @date 2021/3/15 14:47
*/
public class ModelAndView {
private String view;
public void setView(String view) {
this.view = view;
}
public String getView() {
return view;
}
public ModelAndView(String view) {
this.view = view;
}
}
在我们没有找到url的情况下,源码中的是直接调用了noHandlerFound()方法,那么我们就直接使用他的,删除日志打印即可。
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response){
try{
response.sendError(HttpServletResponse.SC_NOT_FOUND);
response.getWriter().print("没有查找到该请求");
}catch (Exception e){
}
}
页面渲染我们直接利用getRequestDispatcher。
private void render(ModelAndView mv, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String view = mv.getView();
req.getRequestDispatcher("/WEB-INF/view/" + view + ".jsp").forward(req, resp);
}
所以最后我们的doDispatch()方法就写完了。
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
// 对请求进行解析
// 1.获取请求的url
String url = req.getRequestURI();
// 2.查找请求的url是否存在 获取具体的handler
HandlerExecutionChain handler = getHandler(url);
if(handler == null){
noHandlerFound(req,resp);
return;
}
// 3.执行对应的目标方法
ModelAndView mv = handler.handler();
// 4.渲染页面
render(mv,req,resp);
}
目前关于逻辑处理就全部写好了,但是,应该会发现,我们之前使用SpringMVC的时候,需要将DispatcherServlet注入容器内。我们现在没有xml,怎么进行注入呢?在之前SpringMVC源码分析------基础知识(二)中提到过,不使用xml怎么进行启动。
我们直接继承ServletContainerInitializer,在使用@HandlesTypes来定义感兴趣的类。之后利用反射的方法执行感兴趣的类中的onStartup()方法
创建SpringServletContainerInitializer.java
package com.web;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import java.lang.reflect.Method;
import java.util.Set;
/**
* @author 龙小虬
* @date 2021/3/15 11:06
*/
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
for (Class<?> classInfo : c) {
try {
// 使用Java反射技术执行onStartup方法
Object object = classInfo.newInstance();
Method onStartup = classInfo.getMethod("onStartup", ServletContext.class);
onStartup.invoke(object, ctx);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
WebApplicationInitializer.java
package com.web;
import javax.servlet.ServletContext;
/**
* @author 龙小虬
* @date 2021/3/15 11:08
*/
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext);
}
创建AbstractDispatcherServletInitializer.java并实现接口WebApplicationInitializer
package com.web.impl;
import com.servlet.DispatcherServlet;
import com.web.WebApplicationInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
/**
* @author 龙小虬
* @date 2021/3/15 11:10
*/
public class AbstractDispatcherServletInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
ServletRegistration.Dynamic dynamic = servletContext.addServlet("dispatcherServlet", new DispatcherServlet());
dynamic.addMapping("/");
System.out.println("反射启动onStartup()方法");
}
}
这个时候代码就直接写完了,但是有问题。少了一个很死板的配置
是的,就是这个玩意。文件中配置com.web.SpringServletContainerInitializer
然后再创建test.jsp,注意是在view下。
test.jsp
<html>
<body>
<h2>test</h2>
</body>
</html>