在这里,我们继续完善上一期的MVC代码,我们的MVC是基于IOC的基础上进行实现的;上一期地址
加耀:仿spring-framework源码实现手写一个IOC容器zhuanlan.zhihu.com
加耀:仿spring-framework源码实现手写MVC(一)zhuanlan.zhihu.com
在前面的章节中,已经实现了仿spring手写IOC,然后完成了一个简单的mvc小demo;
在这一期中,我们对上一期的MVC进行一段优化,添加支持简单的参数类型,支持访问页面元素等功能;
在开始前,我们先对先前的MVC再进行一步扩充。先前的mvc没有读取配置文件,这里我们先读取配置文件,应支持类似springBoot中的多环境配置个配置引入功能;
一:IOC新增初始化配置文件设置
首先,我们在前面的ioc项目中,添加三个配置文件,分别是application.properties、application-dev.properties、application-extra.properties,其代码如下:
application.properties
testKey=value
myspring=测试读取配置文件
spring.profiles.active=dev
spring.profiles.include=extra
application-dev.properties
testKey=value
myspring=测试读取配置文件dev环境
mydev=测试读取dev
application-extra.properties
我是额外引入的配置文件=application-extra.properties
配置文件添加完毕后,我们开始定义一个常量类IocConstant,用来记录项目中可能需要使用到的一些常量信息
package constant;
/**
* 类 名: IocConstant 常量类
* @author: jiaYao
*/
public class IocConstant {
/**
* 模仿spring中的 多环境配置
*/
public final static String active = "spring.profiles.active";
/**
* 模仿spring中的 引入配置
*/
public final static String include = "spring.profiles.include";
}
常量类配置好后,我们开始对我们前面的类的扫描器ClassPathBeanDefinitionScanner进行改进,让它在开始扫描class文件之前,先扫描项目中的配置文件,优先加载配置文件;具体改动如下:
/**
* 获取所有的候选资源源数据信息
*
* @param basePackages
*/
private void doScan(String[] basePackages) {
Set<BeanDefinition> candidates = new HashSet<>();
// 扫描配置文件
Properties properties = findAllConfigurationFile();
registry.registerProperties(properties);
//扫描所有的候选资源列表
findCandidateComponents(basePackages, candidates);
// 将扫描出来的候选资源信息 添加到注册表中
candidates.forEach(beanDefinition -> {
registry.registerBeanDefinition(beanDefinition.getBeanName(), beanDefinition);
});
}
在上述代码中,添加了一个优先扫描配置文件的方法findAllConfigurationFile,扫描出资源目录下的配置文件,读取配置文件并注册到BeanFactory中;
/**
* 扫描配置文件
*/
private Properties findAllConfigurationFile() {
Properties properties = new Properties();
try {
InputStream is = this.getClass().getClassLoader().getResourceAsStream("application.properties");
if (!Objects.isNull(is)) {
InputStreamReader inputStreamReader = new InputStreamReader(is, "GBK");
properties.load(inputStreamReader);
}
if (properties.containsKey(IocConstant.active) && !Objects.isNull(properties.get(IocConstant.active))) {
InputStream is1 = this.getClass().getClassLoader().getResourceAsStream("application-" + properties.get(IocConstant.active) + ".properties");
if (!Objects.isNull(is1)) {
InputStreamReader inputStreamReader = new InputStreamReader(is1, "GBK");
properties.load(inputStreamReader);
}
}
if (properties.containsKey(IocConstant.include) && !Objects.isNull(properties.get(IocConstant.include))){
InputStream is1 = this.getClass().getClassLoader().getResourceAsStream("application-" + properties.get(IocConstant.include) + ".properties");
if (!Objects.isNull(is1)) {
InputStreamReader inputStreamReader = new InputStreamReader(is1, "GBK");
properties.load(inputStreamReader);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return properties;
}
上述代码findAllConfigurationFile中,通过读取主配置文件application.properties,然后判断是否有指定环境配置,是否有引入其他配置信息等;将所有的配置读取出来(后读取的配置会覆盖前面的配置信息);
我们在DefaultListableBeanFactory中添加以下代码,用于
/**
* 配置文件
*/
private Properties properties = null;
/**
* 从配置文件中获取数据
* @param key
*/
public String getProperties(String key){
return this.properties.getProperty(key);
}
/**
* 注册配置文件
* @param properties
*/
public void registerProperties(Properties properties) {
this.properties = properties;
}
用于在类的扫描器ClassPathBeanDefinitionScanner中将扫描出来的配置文件信息注册到BeanFactory中,并且提供对外查询接口,获取配置文件信息;(回头可优化添加@Value注解,实现属性注入),完成上述操作后,我们在IOC项目中启动程序,通过断点查看一下读取到的配置文件是否是我们上门所配置的配置信息
由此可见,已经成功的读取到了配置文件信息;代码已上传gitlab:
黄加耀 / spring-iocgitlab.com
二:实现MVC初始化工作
在上一节的时候,我们已经实现了一个简易的mvc,在没有参数的情况下,可以通过http请求到访问控制层,执行响应的访问控制业务方法并且返回一个字符串到页面;在这一期中,我们进一步优化这个MVC,使其支持参数传递,支持解析返回页面数据,也就是在jsp页面中经常会使用到的EL表达式;
在前面的MVC中,我们初始化IOC容器后,开始进行访问控制层的url映射,但是在映射结束后,我们并没有去保留每个方法的参数信息,所以这时候我们应该去解析一些方法的参数,并保留起来;我们新建一个类ParameterMapper,用来保留方法的参数信息
package handler;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 类 名: ParameterMapper
* 描 述:
*/
@Data
@AllArgsConstructor
public class ParameterMapper {
/**
* 参数名称
*/
private String parameterName;
/**
* 参数类型
*/
private Class<?> parameterType;
}
在上面的ParameterMapper类中,我们保留了参数的名称,参数的类型,然后我们再把它加到我们先前创建的类HandlerMappers中,由于一个方法可能会有很多的参数,所以这时候在HandlerMappers中应该使用一个集合来接收;
package handler;
import lombok.Data;
import java.lang.reflect.Method;
import java.util.List;
@Data
public class HandlerMappers {
/**
* 访问的url
*/
private String url;
/**
* 目标对象
*/
private Object targetObj;
/**
* 目标方法
*/
private Method method;
/**
* 存储方法参数
*/
private List<ParameterMapper> parameterMappers;
public HandlerMappers(String url, Object targetObj, Method method) {
this.url = url;
this.targetObj = targetObj;
this.method = method;
}
}
完成上述操作后,就可以在DispatcherServlet初始化时,进行相应的参数处理了;改动后的初始化方法如下:
@Override
public void init() throws ServletException {
/**
* 初始化容器
*/
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("com.jiayao");
this.context = context;
this.beanFactory = context.beanFactory;
/**
* 获取所有的Controller类的映射关系
*/
getTargetController();
/**
* 获取所有的方法参数关系
*/
getTargetProperties();
/**
* 初始化视图信息
*/
initViewResolver();
}
和上一期的相比,在初始化方法中新增了两个方法,分别是getTargetProperties和initViewResolver,用来初始化访问控制层的方法参数信息以及项目中的页面信息;可以猜测的到,在getTargetProperties方法中,应该就是遍历所有的访问控制层类的方法,获取参数列表以及参数类型,进行按顺序进行保存,其代码如下所示:
/**
* 获取访问控制层的方法参数,创建关系映射
*/
private void getTargetProperties() {
try {
Collection<HandlerMappers> values = handlerMappersMap.values();
for (HandlerMappers handlerMapper : values) {
Method method = handlerMapper.getMethod();
// 获取当前方法的参数名称列表
List<String> parameterNameList = MethodHelper.getMethodParamNames(handlerMapper.getTargetObj().getClass(), method);
Class<?>[] parameterTypes = handlerMapper.getMethod().getParameterTypes();
ArrayList<ParameterMapper> parameterMappers = new ArrayList<>();
for (int i = 0; i < parameterTypes.length; i++) {
parameterMappers.add(new ParameterMapper(parameterNameList.get(i), parameterTypes[i]));
}
handlerMapper.setParameterMappers(parameterMappers);
}
}catch (Exception e){
e.printStackTrace();
}
}
在上述代码中,我们获取上一步中获取所有的访问控制业务,获取方法的参数名称和参数类型,然后保存到访问控制业务代码中;在这里使用到了一个工具类MethodHelper,工具类可在gitlab上下载;这样,在MVC初始化时,就完成了IOC容器的初始化,完成了url与访问控制层的映射关系,完成了访问控制业务与方法参数的绑定关系了;下一步我们可以将项目中的视图文件扫描读取保存,以备在访问控制层返回视图文件时,可直接找到视图文件,不必再次查找和读取;其代码如下所示:
/**
* 存储项目中的页面信息
*/
private ArrayList<View> viewList=new ArrayList<>();
private void initViewResolver() {
try {
String path = beanFactory.getProperties("view.rootPath");
//遍历文件夹获取到所有jsp文件
URL url = this.getClass().getClassLoader().getResource(path);
//拿到文件夹
assert url != null;
File file = new File(url.toURI());
doLoadFile(file);
}catch (Exception e){
e.printStackTrace();
}
}
private void doLoadFile(File file) {
String suffix=beanFactory.getProperties("view.suffix");
//文件夹
if( file.isDirectory()){
File[] files=file.listFiles();
assert files != null;
for(File f:files){
doLoadFile(f);
}
}else{
//是否为jsp结尾的文件
if(file.getName().endsWith(suffix)){
View view=new View(file.getName(),file);
viewList.add(view);//保存文件
}
}
}
完成上述工作后,我们就完成了IOC容器的初始化和MVC的初始化,接下来我们只需要处理当一个请求进来后,需要进行哪些处理即可;
三:实现MVC的Http请求处理
前面,已经初始化了MVC,现在我们可以开始处理进入的请求信息了;由于我们返回的数据中可能保存中文等信息,返回到页面后会乱码,所以我们先处理一下返回数据信息
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
processRequest(request, response);
}
private void processRequest(HttpServletRequest request, HttpServletResponse response) {
//获取初始化的语言环境
response.setHeader("content-type", "text/html;charset=utf-8");
// 把request和response包装成ServletRequestAttributes对象
//异步请求管理
//统一调用doService
doService(request, response);
}
处理好响应的编码格式后,下一步我们就可以根据请求url获取到请求的访问控制层,当请求的url没有找到的话,就提示页面404,资源没有找到;其代码如下:
private void doService(HttpServletRequest request, HttpServletResponse response) {
// 获取当前请求的url的映射关系
String requestURI = request.getRequestURI();
try {
if (!handlerMappersMap.containsKey(requestURI)) {
response.getWriter().write("404");
return;
}
HandlerMappers handlerMappers = handlerMappersMap.get(requestURI);
/**
* 处理当前方法请求函数
*/
doHandleAdeapter(request, response, handlerMappers);
} catch (Exception e) {
e.printStackTrace();
try {
response.getWriter().write("error");
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
当我们根据请求的url找到了相应的访问控制层代码后,我们就可以封装请求的参数信息,按顺序对请求参数进行保存和类型转换,当这些工作都完成后,即可开始执行访问业务层的业务方法了;
private void doHandleAdeapter(HttpServletRequest request, HttpServletResponse response, HandlerMappers handlerMappers) throws InvocationTargetException, IllegalAccessException, IOException {
// 获取请求中所有的请求参数
Map<String, String[]> parameterMap = request.getParameterMap();
// 获取当前请求方法的参数信息
List<ParameterMapper> parameterMappers = handlerMappers.getParameterMappers();
// 获取当前请求参数中所有的参数名
Set<String> requestParamet = parameterMap.keySet();
// 创建请求参数数组
Object[] params = new Object[parameterMappers.size()];
for (int i = 0; i < parameterMappers.size(); i++) {
ParameterMapper parameterMapper = parameterMappers.get(i);
for (String requestParameter : requestParamet) {
// 如果请求的参数名和当前方法中的参数名一致
if (requestParameter.equals(parameterMapper.getParameterName())) {
params[i] = ParameterTypeUtils.typeConversion(parameterMapper.getParameterType(), request.getParameter(requestParameter));
}
}
}
// 开始执行目标方法的业务程序
invokeTargetMethod(response, handlerMappers, params);
}
在上面,使用了一个工具类对参数进行类型转换ParameterTypeUtils.typeConversion,其代码如下:
/**
* @param cls 参数类型
* @param value 参数值
* @return
*/
public static Object typeConversion(Class cls,String value){
if(cls == Integer.class){
return Integer.valueOf(value);
}else if(cls == Long.class){
return Long.parseLong(value);
} else if(cls == String.class){
return value;
}else if (cls == double.class){
return Double.valueOf(value);
}
return value;
}
完成了上述的工作,我们就可以拿到需要请求的controller和请求的方法,以及当前方法的参数信息,开始执行业务方法,获取业务方法返回值,我们将返回数据封装成一个ModelAndView,当返回的是字符的话,则直接进行返回,否则,获取到需要返回的jsp页面,读取页面,进行正则匹配替换页面中的${}标签;
新建类ModelAndView类,用来存储响应信息
package context;
import lombok.Data;
import java.util.HashMap;
/**
* 类 名: ModleAndView
* 描 述: 视图及相应参数
*/
@Data
public class ModelAndView {
/**
* 视图名称
*/
private Object viewData;
/**
* 响应参数
*/
private HashMap model = new HashMap();
/**
* 是否为一个视图对象
*/
private boolean hasView = false;
public void addObject(Object key, Object value){
this.model.put(key, value);
}
}
新建一个View类,用来存储页面信息以及对页面表达式进行替换代码
package context;
import lombok.Data;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 类 名: View
* 描 述: 视图文件
*/
@Data
public class View {
/**
* 视图名称mvc.jsp
*/
private String viewName;
/**
* 视图文件
*/
private File file;
public View(String viewName, File file) {
this.viewName = viewName;
this.file = file;
}
/**
* 正则表达式匹配 ${} 标签
*/
Pattern pattern=Pattern.compile("[.w]*[${]([w]+)[}]");
/**
* 解析视图文件,替换视图文件中的占位符
* @param modelAndView
* @param response
*/
public void replaceAllEl(ModelAndView modelAndView, HttpServletResponse response) throws IOException {
//读取flie内容
BufferedReader reader=new BufferedReader(new FileReader(file));
//替换完成结果
StringBuffer result=new StringBuffer();
while (reader.read()>0){
String line=reader.readLine();
//使用正则表达式替换掉 ${}内容
Matcher matcher=pattern.matcher(line);
//有匹配的表达式
while (matcher.find()) {
//拿到表达式名称
String key = matcher.group(1);
//从返回结果中找到与表达式匹配的变量,并替换
if (modelAndView.getModel().containsKey(key)) {
//替换对应的内容
line = line.replace("${" + key + "}", modelAndView.getModel().get(key).toString());
}
}
// 将替换后的 或者没替换的 进行新写入
result.append(line);
}
// 将数据写会响应
response.getWriter().write(result.toString());
}
}
完成这些工作后,我们再来执行业务代码
private void invokeTargetMethod(HttpServletResponse response, HandlerMappers handlerMappers, Object[] params) throws InvocationTargetException, IllegalAccessException, IOException {
ModelAndView modelAndView = new ModelAndView();
// 执行目标方法
Object invoke = handlerMappers.getMethod().invoke(handlerMappers.getTargetObj(), params);
// 无返回值类型
if (Objects.isNull(invoke))return;
if (invoke instanceof ModelAndView){
// 是一个视图的话,返回
modelAndView = (ModelAndView) invoke;
modelAndView.setHasView(true);
}else{
// 如果不是视图,返回的是Json字符串
modelAndView.setViewData(invoke);
}
//把视图名称解析成对应文件名
applyDefaultViewName(modelAndView);
// 将结果集进行返回
if (!modelAndView.isHasView()){
response.getWriter().write(modelAndView.getViewData().toString());
}
// 是一个视图,则解析视图
View view = getView(modelAndView.getViewData().toString());
if (!Objects.isNull(view)){
view.replaceAllEl(modelAndView, response);
}
}
private void applyDefaultViewName(ModelAndView modelAndView) {
if (modelAndView.isHasView()){
String viewName = modelAndView.getViewData().toString();
String prefix=beanFactory.getProperties("view.prefix", "");
String suffix=beanFactory.getProperties("view.suffix", ".jsp");
viewName=prefix+viewName+suffix;
modelAndView.setViewData(viewName);
}
}
这样,我们的MVC请求就完成了,编写一个页面和一个访问控制层进行测试一下:
页面jsp:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>${goodsName}</title>
<meta name="viewport" content="width=device-width">
</head>
<body onkeydown="enterlogin();">
<div >
<p>
goodsName:${goodsName}
goodsPrice:${goodsPrice}
</p>
</div>
</body>
</html>
访问控制层:
package com.jiayao.controller;
import com.jiayao.service.GoodsInfoServiceImpl;
import context.ModelAndView;
import core.annotation.MyAutowired;
import core.annotation.MyController;
import core.annotation.MyRequestMapping;
@MyRequestMapping("goods")
@MyController
public class GoodsInfoController {
@MyAutowired
private GoodsInfoServiceImpl goodsInfoService;
@MyRequestMapping("/goodsName")
public String queryGoodsName(String goodsName, double goodsPrice){
return "商品名称为:" + goodsName + ", 商品价值为:" + goodsPrice;
}
@MyRequestMapping("goodsIndex")
public ModelAndView queryGoodsIndex(String goodsName, double goodsPrice){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewData("goodsIndex.jsp");
modelAndView.addObject("goodsName",goodsName);
modelAndView.addObject("goodsPrice",goodsPrice);
return modelAndView;
}
}
启动项目后使用浏览器请求可以看到