可能大家经常使用Spring系列框架,使用Spring的注解进行开发,但是仅限于使用,对框架底层的原理并不是很清晰,今天就来实现一个简单的SpringMVC框架,在此之前需要先了解下java的几个元注解,Spring中的注解基本上都是基于这些元注解进行开发的,大家可以看下我的这篇博客,里面详细介绍了java的几种元注解,以及各自的含义。https://blog.csdn.net/qq_37113604/article/details/81974482
从头开始首先建一个web项目,就叫TestSpringMVC吧!
第二步,我们去建两个注解类,分别起名为Controller和RequestMapping,选择文件类型为aoontation注解类
然后我们定义两个注解类:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller{
}
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RequestMapping {
public String value(); //存放传入的路径
}
如果不清楚里面的注解是什么意思的话,可以看我的https://blog.csdn.net/qq_37113604/article/details/81974482这个博客,里面有详细介绍
定义完了这两个注解后,我们就可以在类中使用啦!但是这两个注解仅是可以使用,还没有什么实际的意义,我们编写一个class作为Controller测试类:
@Controller
@RequestMapping("/IndexController")
public class IndexController{
@RequestMapping("/index")
public void index() {
System.out.println("index运行");
}
@RequestMapping("/find")
public void find() {
System.out.println("find运行");
}
}
我们再想下,基于注解开发的情况下,SpringMVC之所以能够起作用,是不是因为在SpringMVC的配置文件中,配置了注解扫描器呢? 答案是肯定的,我们来编写一个SpringMVC的注解扫描器ClassScanner!下面会用到大量的反射,如果对java反射机制不是很了解的,可以先去查查反射相关的知识。
贴一下,ClassScanner的代码吧,里面都有解释:
public class ClassScanner {
//为什么是Map>类型呢? 因为String存储类名,Class对象存储反射生成的类对象
//basePackage为传入的包名
public static Map> scannerClass(String basePackage) {
Map> results = new HashMap<>();
//通过包将 . 替换成/
String filePath = basePackage.replace(".","/");
try {
//返回当前正在执行的线程对象的引用 Thread.currentThread()
Thread currentThread = Thread.currentThread();
//返回当前对象上的类装载器
ClassLoader contextClassLoader = currentThread.getContextClassLoader();
//拿到资源
URL resource = contextClassLoader.getResource(filePath);
//拿到资源的路径 /E:/TestSpringMVC/build/classes/com/bzy
String rootPath = resource.getPath();
if(rootPath != null){
//filePath为com/bzy rootPath为/E:/TestSpringMVC/build/classes/com/bzy 为什么不直接将filePath赋值给rootPath呢?
//这里是为了确保传入的路径存在 得到rootPath = com/bzy
rootPath = rootPath.substring(rootPath.lastIndexOf(filePath));
}
//查找具有给定名称的所有资源
Enumeration dirs = contextClassLoader.getResources(rootPath);
while(dirs.hasMoreElements()){
URL url = dirs.nextElement(); // url: file:/E:/TestSpringMVC/build/classes/com/bzy
//根据url 判定是否是文件对象
if(url.getProtocol().equals("file")){
File file = new File(url.getPath().substring(1)); //把头上的 / 去掉
scannerFile(file, rootPath, results); // 将文件传入文件扫描器
}
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return results;
}
private static void scannerFile(File folder,String rootPath,Map> classes) throws Exception{
//拿到这个folder下的所有文件对象
File[] files = folder.listFiles();
for(int i=0;files!=null && i
最终这个扫描器返回一个Map
然后我们再想下,现在已经拿到了所有的类对象,这些类对象中可能有的使用了注解,有的没有使用注解,现在是不是应该再遍历下这些类与类中的方法,判断下是否存在注解呢? 答案是肯定的,那么应该在哪里进行遍历呢? 我们想下,SpringMVC实现了前后台逻辑的跳转,并且在web.xml中配置了一个DispatcherServlet并设置servlet的启动优先级为1,即容器启动时就初始化这个servlet,那么我们就也写一个DispatcherServlet,然后既可以将他设置为容器启动时加载,也可以写成在访问这个servlet时加载然后写在servlet的init方法中,这两种方法是等效的,我们采用第二种吧,写在init方法中。
然后在类中再定义两个全局的集合来存储得到的类与方法。
我们再想下,init方法只是对servlet进行初始化,当你敲入url时,访问的实际是service方法,所以serlvet的service方法也是需要的!
再贴下自己写的DispacherServlet方法吧:
//配置映射路径与初始化参数
@WebServlet(urlPatterns={"*.do"},initParams={@WebInitParam(name="basePackage",value="com.bzy")})
public class DispacherServlet extends HttpServlet {
private Map controllers = new HashMap<>(); //存储含有RequestMapping注解与Controller注解的controller实例
private Map methods = new HashMap<>(); //存储含有RequestMapping注解的方法
public void init(){
ServletConfig servletConfig = this.getServletConfig();
String basePackage = servletConfig.getInitParameter("basePackage");
try {
Map> cons = ClassScanner.scannerClass(basePackage); //将com.bzy传入,得到其下面的所有类的集合
Iterator itor = cons.keySet().iterator(); // 迭代器遍历集合
while (itor.hasNext()) {
String className = itor.next(); //拿到每一个的类名与类对象
Class clazz = cons.get(className);
String path = "";
//判断是否包含requestMapping注解与Controller注解
if(clazz.isAnnotationPresent(RequestMapping.class) && clazz.isAnnotationPresent(Controller.class)){
RequestMapping reqAnno = (RequestMapping)clazz.getAnnotation(RequestMapping.class); //反射拿到注解类
path = reqAnno.value(); //拿到注解类中的路径
controllers.put(className,clazz.newInstance()); // 添加到新的集合中 object is not an instance of declaring class
Method[] ms = clazz.getDeclaredMethods(); //拿到Controller类中所有的方法
for (Method method : ms) {
if(!method.isAnnotationPresent(RequestMapping.class)){ //如果方法上没有RequestMapping注解则continue
continue;
}
methods.put(path+method.getAnnotation(RequestMapping.class).value(),method); //将方法的路径与方法对象添加到methods中
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
int value = uri.indexOf(contextPath)+contextPath.length();
String mappingPath = uri.substring(value,uri.indexOf(".do"));
Method method = methods.get(mappingPath);
try {
if(method == null){ //传入的路径不存在
resp.getWriter().println("404 404 404 404 404 ");
return;
}
String name = method.getDeclaringClass().getName(); //反射得到类名
Object invoke = method.invoke(controllers.get(name)); //调用得到的类中的method
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
上面的这5个类,就可以构成一个简单的SpringMVC框架啦,可以进行前后台的访问了,我们来测试下!
可以看到访问到了,find方法被访问到了!
其实就是将service方法中的request与response对象传递到find方法就好了。
首先我们定义一个req与resp的工具类:
public class BaseController {
protected HttpServletRequest req;
protected HttpServletResponse resp;
public HttpServletRequest getReq() {
return req;
}
public void setReq(HttpServletRequest req) {
this.req = req;
}
public HttpServletResponse getResp() {
return resp;
}
public void setResp(HttpServletResponse resp) {
this.resp = resp;
}
public void init(HttpServletRequest req,HttpServletResponse resp){
this.req = req;
this.resp = resp;
}
}
然后呢,我们让IndexController继承这个类,这样indexController就可以直接调用父类中的req与resp对象了!
@Controller
@RequestMapping("/IndexController")
public class IndexController extends BaseController{
@RequestMapping("/index")
public void index() {
System.out.println("index运行 "+req.getParameter("userName"));
}
@RequestMapping("/find")
public void find() {
System.out.println("find运行"+req.getParameter("userName"));
}
}
最后我们再改下servlet中的service方法:
protected void service(HttpServletRequest req, HttpServletResponse resp) {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
int value = uri.indexOf(contextPath)+contextPath.length();
String mappingPath = uri.substring(value,uri.indexOf(".do"));
Method method = methods.get(mappingPath);
try {
if(method == null){ //传入的路径不存在
resp.getWriter().println("404 404 404 404 404 ");
return;
}
Class> declaringClass = method.getDeclaringClass(); //反射得到Class>对象
BaseController controller =
(BaseController)controllers.get(declaringClass.getName());
controller.init(req, resp); //将req,resp对象传入
Object invoke = method.invoke(controller); //反射调用controller中的method
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
测试下结果:
最后贴下文件目录:
到现在可能大家对SpringMVC运行原理有了新的认识,这里只是模拟了一下SpringMVC的功能,如果去仔细看SpringMVC的源码,单是一个dispatchServlet类就是一个极为复杂的过程,最后再说下,对框架的理解不要死记硬背,我记得之前为了应付面试背的SpringMVC的运行原理,什么前端控制器,视图解析器,处理映射器乱七八糟,背过了也只是背过了,一点帮助都没有。对于框架的理解还是应该真正去看下底层的代码,然后会发现,能够学到很多的东西,对java会有更深入的了解。
贴一下我的github地址,有需要的可以去下载源码: