1. 客户端发送请求到这个DispatcherServlet前端控制器
@WebServlet(value = "/*",loadOnStartup = 1,
initParams = @WebInitParam(name = "basePackages",value = "edu.uestc.monster.web.controller"))
public class DispatcherServlet extends HttpServlet {
}
前端控制器接收所有的请求,由前端控制器分发到不同的servlet进行执行处理。loadOnStartup = 1表示servlet容器加载该应用时就初始化该servlet。初始化参数basePackages表示要扫描该包及其子包下所有的控制器
2. 利用servlet生命周期的初始化方法映射url和handler(处理器映射器),就是通过这个输入的url,找到对应的控制器方法,同时准备执行该方法的环境(处理器适配器)。
@Slf4j
@WebServlet(value = "/*",loadOnStartup = 1,
initParams = @WebInitParam(name = "basePackages",value = "edu.uestc.monster.web.controller"))
public class DispatcherServlet extends HttpServlet {
//处理器映射器(请求的url和方法的对应关系)
private static final Map<String, Method> handlerMapping = new HashMap<>();
//处理器适配器(为处理器-方法)准备执行环境---该方法所对应的对象实例
private static final Map<Method,Object> instancePools = new HashMap<>();
/**
* 扫描控制层,得到控制层所有java文件的Class对象
* 使用FileVisitor遍历文件和目录(非递归)
*
* @param basePackages 包信息,扫描该包及其子包下的java文件
* @return 所有java文件的Class对象
*
*/
private List<Class<?>> packageScan(String basePackages) throws IOException, URISyntaxException {
var classes = new ArrayList<Class<?>>();
var root = basePackages.replace(".","\\");
var url = Thread.currentThread().getContextClassLoader().getResource(root);
//walkFileTree(path):遍历path路径下所有的文件和子目录
Files.walkFileTree(Paths.get(url.toURI()),new SimpleFileVisitor<Path>(){
@Override //访问到文件后触发该方法
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
try {
var filePath = path.toString();
if (filePath.endsWith(".class")){//是一个class字节码文件,得到java文件的全限定名称
var className = filePath.substring(filePath.indexOf(root) ,filePath.length() - 6).replace("\\",".");
classes.add(Class.forName(className));
}
} catch (Exception e) {
log.error(e.getMessage(),e);
}
return FileVisitResult.CONTINUE; //继续处理文件
}
});
return classes;
}
/**
* 处理器适配器
* 1、处理请求uri和Method之间的映射关系
* 2、映射Method和其对应的实例
*
* 路径:root //"/"
* +
* @RequestMapping("/control/book/")
* +
* @RequestMapping("/save")
*/
private void mappingProcess(String root,String basePackages) throws IOException, URISyntaxException {
//1获取basePackages包下的所有的Servlet类
var classes = packageScan(basePackages);
for(var clazz : classes){
//获取所有标注了@Controller注解的class
var controller = clazz.getAnnotation(Controller.class);
if(controller != null){//表明该class是一个控制器
try {
var path = root;
//创建该控制器的实例
var instance = clazz.getDeclaredConstructor().newInstance();
var mapping = clazz.getAnnotation(RequestMapping.class);
if(mapping != null){
var prefix = mapping.value().startsWith("/") ? mapping.value() : "/" + mapping.value();
path += prefix.endsWith("/") ? prefix : prefix + "/";
}
//获取该类下所有的方法
var methods = clazz.getMethods();
for(var method : methods){
var annotation = method.getAnnotation(RequestMapping.class);
if(annotation != null){
//计算出url
var suffix = annotation.value().startsWith("/") ? annotation.value().substring(1) : annotation.value();
var url = path + suffix;
if(handlerMapping.containsKey(url)){ //重复的url
log.error("url冲突:" + url);
throw new InitException("url冲突:" + url);
}
handlerMapping.put(url,method);
instancePools.put(method,instance);
}
}
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
log.error(e.getMessage(),e);
}
}
}
}
/**
* 生命周期方法,只执行一次
*/
@Override
public void init() throws ServletException {
try {
var basePackages = getInitParameter("basePackages");
var root = getServletContext().getContextPath();
mappingProcess(root,basePackages);
} catch (IOException | URISyntaxException e) {
log.error(e.getMessage(),e);
}
}
}
3. 通过servlet的http请求方法处理请求,会去找具体的handler方法
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
//获取请求的uri
var uri = request.getRequestURI();
var method = handlerMapping.get(uri);
if(method != null){//执行该方法
var instance = instancePools.get(method);
var result = method.invoke(instance,request,response);
if(result != null){//对返回值做出处理
//如果有@ResponseBody注解,直接将内容进行输出json
if (method.getAnnotation(ResponseBody.class) != null)
responseJson(response,result);
else
resolverView(request,response,result.toString()); //处理逻辑视图
}
} else {
var dispatcher = uri.endsWith(".jsp") || uri.equals(".jspx") ?
//获取tomcat处理jsp的servlet
getServletContext().getNamedDispatcher("jsp") :
//交给tomcat的默认Servlet进行处理
getServletContext().getNamedDispatcher("default");//获取tomcat的默认的servlet,处理静态资源
dispatcher.forward(request,response);
}
} catch (IllegalAccessException | InvocationTargetException e) {
log.error(e.getMessage(),e);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
4. 找到具体的controller之后,根据返回值处理不同的视图解析
/**
* 向客户端输出json
* @param response 响应对象
* @param value 将该value以json格式输出
* @throws IOException
*/
private void responseJson(HttpServletResponse response, Object value) throws IOException {
//response.setCharacterEncoding("UTF-8");
var writer = response.getWriter();
response.setContentType("application/json; charset=utf-8");
writer.write(JSONObject.toJSONString(value));
}
/**
* 解析视图
* 规则约定:1、视图默认存放于/pages/下
* 2、视图默认为jsp
* 3、控制层默认以分发的形式寻找渲染视图
* 4、重定向约定:redirect:/url
*/
private void resolverView(HttpServletRequest request,HttpServletResponse response,String viewName) throws IOException, ServletException {
if(viewName.startsWith("redirect:")){//需要重定向
var view = viewName.substring("redirect:".length());
response.sendRedirect(getServletContext().getContextPath() + view);
} else if(viewName.startsWith("forward:")){//分发到指定的视图
var view = viewName.substring("forward:".length());
request.getRequestDispatcher(view).forward(request,response);
} else {// 视图分发: 默认规则
var prefix = "/pages/";
var suffix = ".jsp";
// /pages/message.jsp
var view = prefix + viewName + suffix;
request.getRequestDispatcher(view).forward(request,response);
}
}
附:InitException代码
package edu.uestc.monster.exception;
/**
* 自定义异常
* 前端控制器初始化异常
*/
public class InitException extends RuntimeException{
public InitException(String message){
super(message);
}
}