import com.alibaba.fastjson.JSON;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
/**
* 这个类对象不是框架,是框架的入口
*/
public class DispatcherServlet extends HttpServlet {
//缓存,存储所有请求与controller方法的映射关系
//String key 请求名 , MappingTarget value 对应的controller对象和方法
private Map<String,MappingTarget> mappings = new HashMap<String,MappingTarget>();
//缓存,存储创建的所有controller对象,确保单实例管理
//key=类路径 com.controller value=类对象
private Map<String,Object> controllers = new HashMap<String,Object>();
/**
* 所有浏览器的请求进入服务器后,都会通过DispacherServlet入口,访问当前的service
* 将不同的请求分发给不同controller的不同方法
* 所谓请求的分发,就是调用controller.method()
* 框架怎们知道哪个请求调用哪个controller的哪个方法呢
* 可以通过配置声明。
* 就像原来通过web.xml或注解的配置告诉tomcat一样
* 也可以通过配置告诉mvc框架
* * 配置的技术可以使用文件
* 人为规定:使用properties文件指定请求映射关系
* login=com.duyi.test2.UserController.login
* saveUser=com.duyi.test2.UserController.save
* deleteCar=com.duyi.test2.CarController.delete
* 在web.xml中配置DispatcherServlet时,通过<init-param>指定配置文件的(src)位置
* <init-param>
* <param-name>configLocation</param-name>
* <param-value>file/mvc.properties</param-value>
* </init-param>
* 暂时不考虑方法重载情况
* mvc框架读取配置文件,就可以获得映射关系
* * 配置的技术也可以使用注解
* 人为规定: 在指定请求对应的方法上使用自定义@RequestMapping注解
* class UserController{
* @RequestMapping("login")
* public void login(){}
*
* @RequestMapping("save")
* public void save(){}
* }
* mvc框架需要先找方法,通过反射获得方法的注解,从而获得映射关系
* mvc框架需要先找到类,通过反射获得类中的所有方法,在反射获得注解
* * 约定: 要求controller类必须写在controller名字的包中
* 并且类名必须以Controller为后缀
* * 配置: 在web.xml中使用<init-param>指定扫描注解的那些类所在的包。
* <init-param>
* <param-name>packageScans</param-name>
* <param-value>com.duyi.test2,com.duyi.test3</param-value>
* </init-param>
* 注意:每次请求到达框架,框架都需要通过上述的配置,找到此次请求的映射
* 这个映射关系只需要获得1次(存入缓存)
* static{}
* Servlet.init()
* 因为映射关系只读取一次,但需要使用多次
* 所以要(缓)存起来
* 每一个映射关系需要有请求,controller类,方法
* 自定义一个类,存储映射关系 class MappingTarget
* 一堆映射关系更适合存在map<请求,目标>集合中
*/
public void init(){
//servlet生命周期初始化操作,执行且仅执行一次
//获得请求映射信息,装入缓存。
//请求信息来自两个位置
//-------------------配置文件--------------------------
try{
//通过dispatcherServlet初始化参数configLocation获得配置文件在src下的路径
String configLocation = super.getInitParameter("configLocation");
if(configLocation != null && !"".equals(configLocation)){
//指定了配置文件
//获得读取这个文件的输入流
InputStream is = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(configLocation);
Properties p = new Properties();
p.load(is);
Enumeration keys = p.propertyNames() ;
while(keys.hasMoreElements()){
String key = (String) keys.nextElement(); //login
String value = p.getProperty(key);// com.duyi.test2.UserController.login
int index = value.lastIndexOf(".") ;
String classpath = value.substring(0,index); //com.duyi.test2.UserController 类
String methodname = value.substring(index+1) ; //login 方法名
Class clazz = Class.forName(classpath);
//单实例管理
Object controller = getSingleController(classpath);
//clazz.getMethod("login"); 只能获得无参的login()方法
//但我们未来可以需要通过login设置的方法参数,告诉mvc框架帮我们获得哪些请求参数
Method[] ms = clazz.getMethods() ;
for(Method m : ms){
if(m.getName().equals(methodname)){
//找到了对应的方法 (暂时不考虑重载)
MappingTarget target = new MappingTarget(key,controller,m) ;
mappings.put(key,target) ;
break ;
}
}
}
}
}catch(Exception e){
e.printStackTrace();
}
//--------------------注解----------------------------
//通过一系列的编码,只要找到@RequestMapping注解中指定的那个请求
//就等于获得了请求映射信息,就知道了这个请求对应哪个对象的哪个方法
//要想获得注解中配置的那个请求信息就需要先获得注解对象(反射)
//要想获得注解对象就需要先获得注解所在的那个方法(反射)
//要想获得方法就需要先获得方法所在的controller类(反射)
//要想获得类(Class)就需要获得 类路径,类对象,类模板
//Class c = Class.forName("com.duyi.test3.EmpController")
//Class c = EmpController.class
//Class c = new EmpController().getClass()
//后两种方式在当前框架中不可用,因为不确定,不能写死
//只能使用第一种,但又不知道类路径
//在web.xml中通过初始化参数指定扫描的包路径 com.duyi.test3 , com.duyi.test4
//表示告诉框架,这些包中的类中有可能包含@RequestMapping配置的方法。
//如何根据初始化指定的那些包,找到包中的类,从而获得这些类路径。
//我们知道jvm中的包对应的就是os中文件夹
//如果com.duyi.test3这个包中有类
//那么com/duyi/test3文件夹中就有类文件。(A.java,A.class)
//com这个文件夹在src目录下。可以使用Thread.currentThread.getXXLoader()...找到src目录
try{
String packageScans = super.getInitParameter("packageScans");
if(packageScans != null && !"".equals(packageScans)){
//指定了注解的扫描包 "com.duyi.test3 , com.duyi.test4 , com.duyi.test5"
String[] packages = packageScans.split(",");
for(String packageStr : packages){
//com.duyi.test3
packageStr = packageStr.trim();//去空格
String dirPath =packageStr.replace(".","/");//com/duyi/test3
dirPath = Thread.currentThread().getContextClassLoader()
.getResource(dirPath).getPath();//c:/xxxx/src/com/duyi/test3
File dir = new File(dirPath);
File[] files = dir.listFiles() ;//获得文件夹中的所有类文件
for(File file : files){
String filename = file.getName() ;// EmpController.class
int index = filename.indexOf(".");
String classname = filename.substring(0,index);//EmpConroller
String classpath = packageStr+"."+classname ;//com.duyi.test3.EmpController
Class clazz = Class.forName(classpath);
//单实例管理
Object controller = getSingleController(classpath);
Method[] methods = clazz.getMethods();
for(Method method : methods){
RequestMapping rp = method.getAnnotation(RequestMapping.class);//获得当前方法上指定的注解
if(rp == null){
//没有这个注解,此方法不是用来映射请求,放过
continue ;
}
String requestStr = rp.value();//save请求
MappingTarget target = new MappingTarget(requestStr,controller,method);
mappings.put(requestStr,target);
}
}
}
}
}catch(Exception e){
e.printStackTrace();
}
System.out.println(mappings);
}
/**
* 浏览器发送请求至服务器
* 服务器参考web.xml将所有的请求都交给mvc框架(调用DispatcherServlet.service)
* 1. 框架会参考映射关系,根据请求,调用指定对象的指定方法
* 需要获得此次的请求
* 2. 框架获得请求,传递请求时,还需要根据controller的方法参数列表,获得请求传递参数,类型转换,并注入参数
* 1. 获得零散参数
* url=login?uname=dmc&upass=123
* controller.login(String uname,String upass) ;
* 表示告诉框架,根据映射关系,匹配login这个方法的请求时,还需要获得2个String类型的参数,一个是uname,一个upass
* 框架可以根据反射获得要调用的那个login方法的参数列表,从而获得参数个数,类型和参数名
* 方法参数列表中参数的名字,即为请求时请求参数的名字
* 方法有一个String uname参数 -> request.getParameter("uname")
* 2. 获得组合参数
* 浏览器传递了一堆参数,这一堆参数在服务端恰好可以组成一个对象
* 只需要在controller.method中定义对象类型的参数列表
* url=save?cno=1&cname=bmw&color=blue&price=400000
* controller.save(Car car) ;
* * mvc框架在调用方法时,通过反射获得参数列表
* * 获得参数后,发现参数没有@RequestParam注解,表示不对应某一个参数
* 1. 忘写了 传递null
* 2. 可能需要组成对象,通过反射,获得对象的属性名即为获取的请求参数key
* 对象有几个属性,就获得几个参数,为对象的属性赋值。
* 要求对象的属性名与请求的参数名一致。
* 其实我们匹配的不是属性名,而是属性对应的set方法名。(封装)
* 3. 需要获得Servlet相关对象参数
* * mvc框架将之前web程序开发中许多常见操作封装了。
* 如: 获取参数,响应
* 即使不了解servlet的人,也可以实现请求-响应操作
* 解耦
* * 但mvc框架封装的毕竟是常用的功能,不是所有的功能
* * 某些特殊情况下,可能依然需要使用原生对象
* * 就可以通过指定方法参数,让框架注入原生servlet相关对象 request,response,session
* * 这些参数也不需要使用RequestParam注解指定,类型就可以匹配了
* 4. 可能是文件上传的参数
* 目标方法处理请求(业务,程序员)
* 目标处理请求完毕后,通过指定规则的返回值,让框架负责响应
* * return 普通字符串。 转发
* return "01.jsp"
* req.getRequestDispatcher("01.jsp").forward(req,resp)
* * return 带前缀字符串。 重定向
* return "redirect:01.jsp"
* resp.sendRedirect("01.jsp");
* * return 自定义ModelAndView对象 可以存储转发访问的请求信息和携带的数据信息
* * 所谓的直接响应,就是将方法中获得的数据,直接响应给浏览器
* 一般情况下,响应的多为字符串
* 也需要return 字符串
* 此时如何判断是转发重定向,还是直接响应呢?
* 完全可以再指定一个前缀规则 return "write:01.jsp" 但不这么做。
* 模拟以后所学的springmvc框架,自定义一个@ResponesBody注解,作用在方法上
* 如果controller的方法有该注解,则表示将返回的内容直接响应
* 如果返回字符串,直接响应
* 如果返回其他对象或集合,转换成json再响应。
*
*
*/
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try{
String requestStr = req.getRequestURI() ;//获得此次请求字符串 /login , /saveCar
requestStr = requestStr.substring(1);//去掉最前面的/ 因为映射关系中的请求名都没有/ -> login
MappingTarget target = mappings.get(requestStr);//根据请求找到对应的映射关系 CarController.save
if(target == null){
//此次请求没有找到匹配的映射关系
resp.sendError(404,"["+requestStr+"]");
return ;
}
//根据请求找到了映射关系,调用对象的方法
Object controller = target.getController() ;//UserController , CarController
Method method = target.getMethod();// login() , save(Car car)
Parameter[] parameters = method.getParameters();//获得方法的参数列表
Object result = null ;
if( parameters== null || parameters.length == 0){
//没有参数,直接调用
result = method.invoke(controller) ;//controller.login(无参);
}else{
//有参数,但此时的参数可能有2个来源
//1个是普通请求的参数
// 使用req.getParameter()获取
//1个是文件上传请求的参数
// 使用fileUpload上传组件,会将所有的参数都变成FileItem对象
//准备一个map集合,装载两种请求传递的参数
//Object 可能是String参数值,也可能是上传的文件参数值
Map<String,Object> params = new HashMap<String,Object>();
try {
//假设是文件上传请求
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> fis = upload.parseRequest(req); //当前行代码可能会出现异常,如果不是文件上传会出现异常
for(FileItem fi : fis){
//此时fi就表示一个参数,有可能是普通参数(pname),也可能是文件参数(file)
if(fi.isFormField()){
//是文件上传是的一个普通参数,如:pname
String key = fi.getFieldName() ;//pname
String value = fi.getString() ; //iphone12
params.put(key,value) ;
}else{
//是一个文件参数
String key = fi.getFieldName() ;//file
String fname = fi.getName() ;//文件名
long length = fi.getSize() ;//文件大小
InputStream content = fi.getInputStream() ;//一个可以获得文件内容的输入流 文件内容
//需要将文件参数的3个信息打包成一个对象装入map,自定义文件类MvcFile
MvcFile file = new MvcFile(fname,length,content) ;
params.put(key,file) ;
}
}
}catch(Exception e){
//如果出现异常,证明不是文件上传,是普通请求
Enumeration<String> paramNames = req.getParameterNames() ;
while(paramNames.hasMoreElements()){
String key = paramNames.nextElement() ;//uname,upass
String value = req.getParameter(key);
params.put(key,value) ;
}
}
//上述处理完毕后,无论什么请求,最终参数都存储在map集合中。
List<Object> paramterValues = new ArrayList<Object>();//准备装载参数列表的值
//一个一个获得参数
for(Parameter parameter :parameters){
//parameter.getName() ;//获得参数名,编码时参数名时uname,但编译后就变成了arg0,自定义RequestParam("uname")指定参数名
//Car car
RequestParam rp = parameter.getAnnotation(RequestParam.class);//获得参数列表的注解
if(rp == null){
//有参数,但没有指定注解,目前只有3种情况
Class parameterType = parameter.getType() ;//参数类型 Car car
//一种是需要原生servlet相关对象 req,resp,session
if(parameterType == HttpSession.class){
paramterValues.add( req.getSession() ) ;
}else if( parameterType == HttpServletRequest.class){
paramterValues.add( req ) ;
}else if(parameterType == HttpServletResponse.class){
paramterValues.add( resp ) ;
}else{
//另一种就是参数为对象类型的参数
//要根据对象的属性(set方法名)获得一系列请求参数,并组成对象,并注入
Object paramObj = parameterType.newInstance() ;//new Car() 要求Car必须有无参构造器
//car.setCno() -> cno属性赋值 -> request.getParameter("cno"); //约定优于配置
//car.setCname()
Method[] ms = parameterType.getMethods() ;
for(Method m : ms){
String mname = m.getName() ;//setCno , getCno
if(mname.startsWith("set")){
//是一个可以赋值的set方法,是一个与请求参数对应的set方法 setCno / setCarNo-> carNo / setA
String key = mname.substring(3) ;//去掉set -> Cno
if(key.length() == 1){
key = key.toLowerCase() ;
}else{
key = key.substring(0,1).toLowerCase() + key.substring(1) ; // cno
}
String value = params.get(key).toString() ; //获得set方法对应的属性对应的请求参数
//通过set方法,将请求参数赋值给参数对象前,还需要考虑类型问题,暂时只考虑int,String,double
Class fieldType = m.getParameterTypes()[0] ;
Object $value = caseType(value,fieldType);
m.invoke(paramObj,$value) ;
}
}
//循环结束,找到了所有的set方法,获得了所有属性对应的请求参数值,并完成了赋值
//现在这个car对象,就是一个装有请求参数的完整的对象
paramterValues.add(paramObj) ;
}
}else{
//有参数,有注解,就可以获得参数对应的请求参数key
//此时参数有2种可能
//一种普通参数 String,int,double
//一种文件参数MvcFile
String key = rp.value(); //uname
Object value = params.get(key) ; // req.getParameter("uname");
if(value == null){
//此次没有传递这个参数
paramterValues.add(null) ;
}
Class parameterType = parameter.getType() ;//获得参数类型,暂时只处理String和Integer,Double,各种集合数组不处理
Object $value = caseType(value,parameterType) ;
paramterValues.add($value);
}
}
//循环结束,就获得了所有的参数,并装入list集合
result = method.invoke(controller,paramterValues.toArray()); //调用方法传递参数(注入,框架主动为我们传参)
}
//目标方法处理完毕,根据目标方法的返回值,处理响应
ResponseBody rb = method.getAnnotation(ResponseBody.class) ;//获得方法上的直接响应的注解
if(rb != null){
//设置了该注解,表示要直接响应
String $result = "" ;
if(result instanceof String){
$result = (String) result;
}else if(result.getClass() == int.class || result.getClass() == Integer.class){
$result = result.toString() ;
}else{
//既不是String,也不是int,double之流,我们认为就是对象或集合
//需要将对象或集合转换成字符串响应给浏览器
//响应给浏览器的字符串,还能被浏览器中的js处理
//要求对象或集合转换的字符串要符合json格式。
// {} , [] , [{},{}] -> List<Phone>
//可以使用json转换工具帮我们实现json格式转换
// fastjson , json-lib , gson , jackson
$result = JSON.toJSONString(result) ;
}
resp.getWriter().write($result);
}else{
//没有注解,间接响应
if(result instanceof String){
//按照目前的分析,可能是转发,可能是重定向
String $result = (String) result;
if($result.startsWith("redirect:")){
//重定向 redirect:01.jsp
$result = $result.substring(9) ;//去掉redirect前缀
resp.sendRedirect($result);
}else{
//转发 01.jsp
req.getRequestDispatcher($result).forward(req,resp);
}
}else if(result instanceof ModelAndView){
//转发,并携带数据
ModelAndView $result = (ModelAndView) result;
//携带数据,就是将mv中map集合的数据,装入request
Map<String,Object> datas = $result.getDatas() ;
Set<String> keys = datas.keySet() ;
for(String key : keys){
Object value = datas.get(key) ;
req.setAttribute(key,value);
}
req.getRequestDispatcher($result.getViewName()).forward(req,resp); ;
}
}
}catch(Exception e){
e.printStackTrace();
}
}
//类型转换
private Object caseType(Object value,Class type){
if(value instanceof MvcFile){
return (MvcFile)value ;
}
String $value = (String) value;
if(type == String.class){
return $value ;
}else if(type == int.class || type == Integer.class){
return Integer.parseInt($value) ;
}else if(type == double.class || type == Double.class){
return Double.parseDouble($value);
}
return value ;
}
//单实例管理controller对象,根据controller类路径找对应的对象,有就返回,没有就新建
private Object getSingleController(String classpath) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class clazz = Class.forName(classpath);
Object controller = controllers.get(classpath);
if(controller == null){
synchronized (clazz){
//还没有缓存,就new一个
if(controller == null)
controller = clazz.newInstance() ;// new UserController()
}
}
return controller ;
}
}
import java.io.Serializable;
import java.lang.reflect.Method;
/**
* 请求映射的目标类
* 存储哪个请求对应哪个类对象的哪个方法
*/
public class MappingTarget implements Serializable {
private String reqStr ; //请求 login
private Object controller ; //目标controller对象 UserController
private Method method ; //请求对应的对象方法 login()
public String getReqStr() {
return reqStr;
}
public void setReqStr(String reqStr) {
this.reqStr = reqStr;
}
public Object getController() {
return controller;
}
public void setController(Object controller) {
this.controller = controller;
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public MappingTarget(String reqStr, Object controller, Method method) {
this.reqStr = reqStr;
this.controller = controller;
this.method = method;
}
public MappingTarget() {
}
@Override
public String toString() {
return "MappingTarget{" +
"reqStr='" + reqStr + '\'' +
", controller=" + controller +
", method=" + method +
'}';
}
}
import java.util.HashMap;
import java.util.Map;
/**
* 实现携带数据的转发响应时,装载转发的网页信息和携带的数据信息
*/
public class ModelAndView {
private String viewName ;//01.jsp
private Map<String,Object> datas = new HashMap<>();
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public Map<String, Object> getDatas() {
return datas;
}
public void setDatas(Map<String, Object> datas) {
this.datas = datas;
}
public void addAttribute(String key , Object value){
datas.put(key,value);
}
}
import java.io.InputStream;
import java.io.Serializable;
/**
* 装载文件上传的文件参数信息
*/
public class MvcFile implements Serializable {
private String filename ;
private long length ;
private InputStream content ;
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public InputStream getContent() {
return content;
}
public void setContent(InputStream content) {
this.content = content;
}
public MvcFile(String filename, long length, InputStream content) {
this.filename = filename;
this.length = length;
this.content = content;
}
public MvcFile() {
}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) //注解在运行时可以通过反射使用。
@Target(ElementType.METHOD) //自定义注解可以用在方法上
public @interface RequestMapping {
public String value() ; //存储注解所在方法对应的请求名。
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
public String value();
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ResponseBody {
}