首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringMVC实现原理

SpringMVC实现原理

作者头像
用户2141593
发布2019-02-20 11:02:58
4500
发布2019-02-20 11:02:58
举报
文章被收录于专栏:Java进阶Java进阶

今天我们来实现一个简单的springMVC框架,可以理解为 springMVC1.0这个版本,只是功能比较简单而已;

废话不多说,进入正题;先看看springMVC的简单流程;

我们请求的地址会被dispatchservlet这个springMVC的核心拦截器拦截,然后dispatchservlet会找到我们想要的那个controller里的那个方法,并调用。但是dispatchServlet不是人,它没那么智能,看到url就知道是谁了,但是我们可以让它变得智能起来,我们可以利用handlerMapping来告诉

dispatchServlet,它应该调用哪个方法;

为了让这个框架不那么笨,我借用了spring的IOC 思想, 实现一个容器来管理我的bean; 这个框架和你印象中使用springmvc 很相似,你应该能回想起

springMVC的零零点点,然后看看这个简单的框架是如何实现这些功能的;

首先看下项目工程:

首先,我们先看maven 依赖,没错,自己实现的框架当然不用spring的jar包了;

并为了方便大家理解,我的取名和spring原来的风格多少有些类似;

首先看到annotation包,@Autowired、@Controller、@RequestMapping、@service这些注解大家应该再熟悉不过了吧!

然后是 servlet包,模仿的springMVC的核心拦截器 dispatchServlet;

demo包的 controller service  不必解释啦~

这里我们模仿springMVC的调用规则,MyDispatcherServlet负责处理框架的逻辑,调用Controller,Controller调用service;

先看看自定义注解是如何定义的,这里挑选了几个代表;

开始编写核心拦截器 dispatchServilet

我们第一步模仿spring 的思想,先找到我们要扫描哪些类,下面是 spring的做法,

这是我的做法:

为什么我通过  String scanPackage = config.getInitParameter("scanPackage"); 就能找到xml中的配置呢?请参考这里 的初始化细节;

servlet 对象在初始化的时候,容器会给它提供一个 ServletConfig 对象  去读取 web.xml中的配置;

我们得到要扫描的路径后,可以就需要实现spring的IOC了;

我们为了得到所有bean;在拿到项目的包路径后,可以转换为文件路径,然后再从文件路径中得到所有的类名;

得到类名后,就可以通过反射进行实例化了,然后将这些需要管理的东西放到一个容器中管理,要用的时候从容器里拿就可以了;

我这里使用的容器是 Hashmap<String, Object>    类的简称(SimpleName)为key ,类的实例对象为value。

得到了所有的类名后,开始实例化的工作

private void instance(){
		//利用反射机制将扫描到的类名全部实例化
		if(classNames.size() == 0){ return; }
		try{
			for (String className : classNames) {
				
					Class<?> clazz = Class.forName(className);
					//没有@Controller、@Service注解标识的类不需要实例化
					if(clazz.isAnnotationPresent(LANController.class)){
						//getSimpleName() 除去包名,获取类名的简称  例如: MyAction
						String beanName = lowerFirstChar(clazz.getSimpleName());
						instanceMapping.put(beanName, clazz.newInstance());
					}else if(clazz.isAnnotationPresent(LANService.class)){
						LANService service = clazz.getAnnotation(LANService.class);
						String beanName = service.value();
						if(!"".equals(beanName.trim())){
							//beanName 这里就是aa
							instanceMapping.put(beanName, clazz.newInstance());
							continue;
						}
						//如果自己没有起名字,后面会通过接口自动注入
						Class<?> [] interfaces = clazz.getInterfaces();
						for (Class<?> i : interfaces) {
							instanceMapping.put(i.getName(), clazz.newInstance());
						}
					}else{
						continue;
					}
			}
		}catch(Exception e){
			e.getStackTrace();
		}
	}

实例化以后,就要准备注入了;

private void autowired(){
		if(instanceMapping.isEmpty()){ return; }
		for (Entry<String, Object> entry : instanceMapping.entrySet()) {
			//getDeclaredFields()获取自己声明的所有字段
			Field [] fields = entry.getValue().getClass().getDeclaredFields();
			for (Field field : fields) {
				if(!field.isAnnotationPresent(LANAutowired.class)){ continue; }
				LANAutowired autowired = field.getAnnotation(LANAutowired.class);
				//如果是私有属性,设置可以访问的权限
				field.setAccessible(true);
				//自己取的名字   获取注解的值
				String beanName = autowired.value().trim();
				System.out.println("beanName=="+beanName);
				//如果没有自己取名字
				if("".equals(beanName)){
					//getType()获取该字段声明时的     类型对象   根据类型注入
					beanName = field.getType().getName();
				}
				try {
					System.out.println("field.getName()***"+field.getName());
					 // 注入接口的实现类,  
					 System.out.println("entry.getValue()======"+entry.getValue());
					 System.out.println("instanceMapping.get(beanName)---------"+instanceMapping.get(beanName));
					 //将Action 这个 类的 IModifyService 字段设置成为   aa 代表的实现类  ModifyServiceImpl
					field.set(entry.getValue(),instanceMapping.get(beanName));
				} catch (Exception e) {
					e.printStackTrace();
					continue;
				}
			}
		}
	}
field.set(entry.getValue(),instanceMapping.get(beanName));

这一行代码是关键,这就是为什么我们在注入接口,就能找到实现类的根本所在。

这里的field,就是我们注入的那个接口, entry.getValue() 得到的是接口所在的类,instanceMapping.get(beanName)是这个接口对应的

那个实现类,   意思就是:在 运行阶段, 将 controller中 的某个service接口字段 替换成 这个service的实现类;

这样我们在编写代码的时候是用使用接口调用方法,但实际运行时,就是它的实现类在调用这个方法了;

不得不感叹,反射的强大。

完成注入后,开始处理handlermapping上的value与之对应的method 的映射关系了

public void handlerMapping(){
		if(instanceMapping.isEmpty()){ return; }
		for (Entry<String, Object> entry : instanceMapping.entrySet()) {
			Class<?> clazz = entry.getValue().getClass();
			//RequestMapping只在 Controller中
			if(!clazz.isAnnotationPresent(LANController.class)){ continue; }
			String url = "";
			if(clazz.isAnnotationPresent(LANRequestMapping.class)){
				LANRequestMapping requstMapping = clazz.getAnnotation(LANRequestMapping.class);
				//得到RequstMapping的value(/web)准备与 方法上的 RequstMapping(search/add/remove)进行拼接;
				url = requstMapping.value();//(/web)
			}
			Method [] methods = clazz.getMethods();
			for (Method method : methods) {
				if(!method.isAnnotationPresent(LANRequestMapping.class)){ continue; }
				LANRequestMapping requstMapping = method.getAnnotation(LANRequestMapping.class);
				String regex =  url + requstMapping.value();
			 // regex = /web/add.json
				regex = regex.replaceAll("/+", "/").replaceAll("\\*", ".*");
				System.out.println("regex: "+regex);
				Map<String,Integer> pm = new HashMap<String,Integer>();
				//因为每个参数可能有多个注解,所以会是个二维数组
				Annotation [] [] pa = method.getParameterAnnotations();
				for(int i = 0; i < pa.length; i ++){
					for (Annotation a : pa[i]){
						if(a instanceof LANRequestParam){
							String paramName = ((LANRequestParam) a).value();
							if(!"".equals(paramName.trim())){
					  //方法参数的名字(值)  name/addr,下标
								pm.put(paramName, i);
							}
						}
					}
				}
				//提取Request和Response的索引
				Class<?> [] paramsTypes = method.getParameterTypes();
				for(int i = 0 ; i < paramsTypes.length; i ++){
					Class<?> type = paramsTypes[i];
					if(type == HttpServletRequest.class ||  type == HttpServletResponse.class){
						
						pm.put(type.getName(), i);
					}
				}
				handlerMapping.add(new Handler(Pattern.compile(regex),entry.getValue(),method, pm));
			}
		}
	}

完成所有的初始化工作后,当然就等着用户发过来的请求咯。

那自然是调用servlet的 doPost 和 doGet方法了,

为了简单点,我在doGet中调用doPost

@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		this.doPost(req, resp);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		//System.out.println(req.getRequestURI());
		try{
			//从上面已经初始化的信息中匹配
			//拿着用户请求url去找到其对应的Method
			boolean isMatcher = pattern(req,resp);
		if(!isMatcher){
			resp.getWriter().write("对不起,你遇到了  404 Not Found");
		}
		}catch(Exception e){
			resp.getWriter().write("500 Exception,Details:\r\n" + 
			e.getMessage() + "\r\n" +
			Arrays.toString(e.getStackTrace()).replaceAll("\\[\\]", "")
			.replaceAll(",\\s", "\r\n"));
		}
	}

如果如果没有匹配成功就返回404,说明用户的路径输错了,

发送异常就报500;

如果匹配成功怎么办? 当然是调用controller里的方法咯;

怎么调用?还是通过反射~通过方法的反射~

public boolean pattern(HttpServletRequest req, HttpServletResponse resp) throws Exception{
		if(handlerMapping.isEmpty()){  return false; }
		//获取请求的url
		String url = req.getRequestURI();
		//获取容器路径
		String contextPath = req.getContextPath();
		url = url.replace(contextPath, "").replaceAll("/+", "/");	
		for (Handler handler : handlerMapping) {
			try{
				Matcher matcher = handler.pattern.matcher(url);
				//如果没匹配上就跳出
				if(!matcher.matches()){ continue ;}
				//按照声明顺序返回  Class 对象的数组,这些对象描述了此 Method 对象所表示的方法的形参类型。
				Class<?> [] paramTypes = handler.method.getParameterTypes();
				//里面存放 反射是需要的具体参数
				Object [] paramValues = new Object[paramTypes.length];
				//获取前端请求参数和请求参数值的映射关系
				Map<String,String[]> params = req.getParameterMap();
				for (Entry<String, String []> param : params.entrySet()) {
					String value = Arrays.toString(param.getValue()).replaceAll("\\]|\\[", "").replaceAll(",\\s", ",");
					if(!handler.paramMapping.containsKey(param.getKey())){ continue;}
					//如果匹配,则获取该参数在方法中的下标;
					int index = handler.paramMapping.get(param.getKey());
					//涉及到类型转换
					paramValues[index] = castStringValue(value,paramTypes[index]);
				}
				//
				int reqIndex = handler.paramMapping.get(HttpServletRequest.class.getName());
				paramValues[reqIndex] = req;
				int respIndex = handler.paramMapping.get(HttpServletResponse.class.getName());
				paramValues[respIndex] = resp;
				//方法的反射   需要对象.方法
				handler.method.invoke(handler.controller, paramValues);
				return true;
			}catch(Exception e){
				throw e;
			}
		}
		return false;
	}

我们来看看controller的代码,准备测试:

public class MyController {
	
	public  MyController() {
		System.out.println("初始化了"+this);
	}
	
	@LANAutowired 
	private searchService searchService;
	
	@LANAutowired("aa") 
	private modifyService modifyService;
	
	@LANRequestMapping("/search/*.json")
	public void search(HttpServletRequest request,HttpServletResponse response,
			@LANRequestParam("name") String name){
		String result = searchService.search(name);
		System.out.println("查询成功");
		out(response,result);
	}
	
	
	@LANRequestMapping("/add.json")
	public void add(HttpServletRequest request,HttpServletResponse response,
			@LANRequestParam("name") String name,
			@LANRequestParam("addr") String addr){
		System.out.println("添加成功");
		String result = modifyService.add(name,addr);
		out(response,result);
	}
	
	
	@LANRequestMapping("/delete.json")
	public void remove(HttpServletRequest request,HttpServletResponse response,
			@LANRequestParam("id") Integer id){
		System.out.println("删除成功");
		String result = modifyService.remove(id);
		out(response,result);
	}

是不是和你刚学springMVC时的一致?

赶紧启动项目在地址栏输入:http://localhost:8080/LanSpringMVC/web/add.json?name=witt&addr=shenzhen

出现:

add name = witt,addr=shenzhen

说明大工告成~

想要源码的小伙伴点这里:源码

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017年09月27日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档