Servlet:JSP、MVC、Spring MVC 【转】

以下内容转载和参考自:廖雪峰的官方网站。

  1、JSP

  前面的Servlet开发是在HTTP处理中编写HTML代码,将要显示的变量值嵌入到HTML文本中,如果要开发一个复杂的网页的话这样编写HTML就很麻烦。我们可以专门编写一个保存的HTML文件,与普通的HTML文件不同的是可以在其中使用HttpServletRequest等HTTP对象,这个文件就是JSP。

  JSP是Java Server Pages的缩写,文件名必须以.jsp结尾,整个文件与HTML并无太大区别,但在其中可以插入Java代码,如下所示为hello.jsp的内容,包含在<%......%>之间的是Java代码,使用<%= xxx %>则可以快捷输出一个变量的值,包含在<%--和--%>之间的是注释。其中的out是JSP的内置变量,表示HttpServletResponse的PrintWriter,request表示HttpServletRequest对象,其它的还有session,表示当前HttpSession对象。


	
		Hello World - JSP
	
	
		<%-- JSP Comment --%>
		

Hello World!

<% String str = "Your IP address is "; out.println(str); %> <%= request.getRemoteAddr() %>

  将上面的hello.jsp放到Tomcat目录webapps的ROOT目录中的话,就可以在浏览器中输入http://localhost:8080/hello.jsp来访问这个JSP页面,如下所示,如果放到webapps下的hello目录中的话那么应该使用http://localhost:8080/hello/hello.jsp。实际上JSP在执行前会被编译成一个Servlet,可见JSP本质上就是一个Servlet,只不过无需配置映射路径,在Tomcat服务运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。可以看出,Servlet是在Java代码中嵌入输出HTML,而JSP则是在HTML中嵌入动态输出,比如嵌入Java代码,所以JSP是以Java语言作为脚本语言的。JSP目前已经很少使用。

Servlet:JSP、MVC、Spring MVC 【转】_第1张图片

  除了<% ... %>外,JSP页面还包含一些其它的指令,如page指令引入Java类来使用简单类名而不是完整类名,使用include指令可以引入另一个JSP文件:

<%@ page import="java.io.*" %>
<%@ page import="java.util.*" %>


    <%@ include file="header.jsp"%>
    

Index Page

<%@ include file="footer.jsp"%>

Servlet:JSP、MVC、Spring MVC 【转】_第2张图片  

 2、MVC

  可以只在Servlet中调用Java代码来处理数据,然后将处理好的数据交给jsp来进行渲染展示,各自负责自己擅长的工作,这样符合MVC模式,下面的UserServlet作为控制器(Controller),User作为模型(Model),user.jsp作为视图(View),整个MVC架构如下:

 Servlet:JSP、MVC、Spring MVC 【转】_第3张图片

package xsl;

public class User {
    public User(String name, String schoolName, String address){
        this.name = name;
        this.schoolName = schoolName;
        this.address = address;
    }
    public String name;
    public String schoolName;
    public String address;
}
import xsl.User;

@WebServlet(urlPatterns = "/test")
public class UserServlet  extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        User user = new User("Bob", "No.1 Middle School", "101 South Street");
        req.setAttribute("user", user); // 将数据放入到请求中
        req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp); //将请求发送给jsp来进行处理渲染
    }
}

  下面的user.jsp要放到Tomcat中对应的/WEB-INF/目录下,这里通过请求获取User数据,然后在页面中直接输出(此处未考虑HTML的转义问题,有潜在安全风险):

<%@ page import="xsl.User"%>
<%
    User user = (User) request.getAttribute("user");
%>


    Hello World - JSP


    

Hello <%= user.name %>!

School Name: <%= user.schoolName %>

School Address: <%= user.address %>

Servlet:JSP、MVC、Spring MVC 【转】_第4张图片

 Servlet:JSP、MVC、Spring MVC 【转】_第5张图片

 3、Spring MVC

  对于上面的MVC模式,还可以进一步的降低耦合度,如将控制器(Controller)从HttpServlet中剥离出来,在HttpServlet中调用控制器中的相关方法来实现对浏览器请求的处理。如下所示的UserController就是一个控制器,其signin()方法会在浏览器请求http://localhost:8080/signin的时候被Servlet服务调用(Servlet根据请求的路径"/signin"来找到注解值为"/signin"的控制器方法来调用),signout()、profile()同理也是。

    //使用GetMapping和PostMapping注解来判断控制器中方法对应POST还是GET,通过注解中的值来确定调用控制器中的哪个方法	
    
    //GetMapping注解,用来声明控制器中所有GET方法,其值为对应的Servlet映射路径
	public @interface GetMapping
	{
		String value();
	}

	//PostMapping注解,用来声明控制器中所有POST方法,其值为对应的Servlet映射路径
	public @interface PostMapping
	{
		String value();
	}
public class UserController {
    @GetMapping("/signin") //自定义注解GetMapping,表明这是一个GET请求处理方法,注解值表明了该方法能够处理的路径
    public ModelAndView signin() {
        //...
    }

    @PostMapping("/signin") //自定义注解PostMapping,表明这是一个POST请求处理方法,注解值表明了该方法能够处理的路径
    public ModelAndView doSignin(SignInBean bean) {
        //...
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        //...
    }

    @GetMapping("/test")
    public ModelAndView testFun(int num) {
        //...
    }

    @GetMapping("/profile")
    public ModelAndView profile(HttpServletResponse response, HttpSession session) throws IOException {
        User user = (User) session.getAttribute("user");
        if (user == null) {
            return new ModelAndView("redirect:/signin"); // 未登录,跳转到登录页:
        }

        if (!user.isManager) {
            response.sendError(403); // 权限不够,返回403:
            return null;
        }

        String viewFileName = "/profile.html";
        Map modelData = Map.of("user", user);
        return new ModelAndView("/profile.html", modelData);
    }
}

  如下所示的DispatcherServlet是Servlet服务,在初始化中就将控制器UserController的成员方法保存到了mapGetMappings(GET请求)和mapPostMappings(POST请求)中,这样Servlet服务根据请求的路径来获得对应的控制器中的处理方法来调用。可以看到,使用反射的话Servlet服务无需知道控制器中的方法名称,通过方法的注解就可以获得指定的方法,而且我们能够自由的增加或者删除控制器中的方法,Servlet服务不用修改代码通过反射就能够获取控制器中的所有方法。

public class User {
    public int id;
    public String name;
    public boolean isManager;
}
@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet{
    private UserController controller;
    private Map mapGetMappings = new HashMap<>(); //GET请求
    private Map mapPostMappings = new HashMap<>(); //POST请求
    private ViewEngine viewEngine; //模板引擎

    @Override
    public void init() throws ServletException {
        controller = new UserController(); //控制器对象实例
        scanInControllers(); //通过反射获得控制器中方法到mapGetMappings、mapPostMappings
        viewEngine = new ViewEngine(getServletContext()); //getServletContext()是HttpServlet中成员方法
    }

    private void scanInControllers(){
        try {
            for(Method m : Class.forName("UserController").getMethods()) //获得UserController类的所有方法
            {
                String strAnnotationValue = null; //注解中的值:路径,如"/signin"
                if(m.isAnnotationPresent(GetMapping.class)){ //方法是否包含GetMapping注解
                    GetDispatcher dispatcher = new GetDispatcher(controller, m, m.getParameterNames(), m.getParameterTypes());
                    strAnnotationValue = m.getAnnotation(GetMapping.class).value();
                    mapGetMappings.put(strAnnotationValue, dispatcher);
                }
                else if(m.isAnnotationPresent(PostMapping.class)){ //方法是否包含PostMapping注解
                    PostDispatcher dispatcher = new PostDispatcher(controller, m, m.getParameterTypes());
                    strAnnotationValue = m.getAnnotation(PostMapping.class).value();
                    mapPostMappings.put(strAnnotationValue, dispatcher);
                }
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");

        String strURI = req.getRequestURI(); //eg: "/hello"
        String strContext = req.getContextPath(); //上下文,即war包名,ROOT包的话该值为空
        String path = strURI.substring(strContext.length()); //eg: "/hello"
        GetDispatcher dispatcher = mapGetMappings.get(path); // 根据路径查找对应的控制器中的方法
        if (dispatcher == null) {
            resp.sendError(404);  // 未找到返回404:
            return;
        }

        ModelAndView mv = null;
        try {
            mv = dispatcher.invoke(req, resp); // 调用Controller对应的方法获得ModelAndView
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        if (mv == null) { //返回null表示内部已自行处理完毕,直接返回
            return;
        }

        if (mv.view.startsWith("redirect:")) { // 以"redirect:"开头的view表示重定向
            resp.sendRedirect(mv.view.substring(9));
            return;
        }

        // 将模板引擎渲染的内容写入响应:
        PrintWriter pw = resp.getWriter();
        this.viewEngine.render(mv, pw);
        pw.flush();
    }

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

   我们将一个方法保存到一个GetDispatcher/PostDispatcher中,调用Dispatcher的invoke()可以调用对应的方法。比如signout(HttpSession session)方法需要的参数是HttpSession对象,所以我们就获取当前请求的HttpSession来传给它,testFun()方法需要一个名为num的int对象,那么就从当前请求中查找名为num的参数来传给testFun():

public class GetDispatcher {
    Object instance; // Controller控制器实例
    Method method; // Controller中的方法
    String[] parameterNames; // 方法参数名称
    Class[] parameterClasses; // 方法参数类型

    public GetDispatcher(Object instance, Method method, String[] parameterNames, Class[] parameterClasses){
        this.instance = instance;
        this.method = method;
        this.parameterNames = parameterNames;
        this.parameterClasses = parameterClasses;
    }

    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
            throws InvocationTargetException, IllegalAccessException {

        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            String parameterName = parameterNames[i]; //参数名
            Class parameterClass = parameterClasses[i]; //参数类型
            if (parameterClass == HttpServletRequest.class) { //参数类型是HttpServletRequest
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) { //参数类型是HttpServletResponse
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) { //参数类型是HttpSession
                arguments[i] = request.getSession();
            } else if (parameterClass == int.class) { //参数类型是int,查找URL参数中是否包含该int
                arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == long.class) { //参数类型是long,查找URL参数中是否包含该long
                arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == boolean.class) { //参数类型是boolean,查找URL参数中是否包含该boolean
                arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
            } else if (parameterClass == String.class) { //参数类型是String,查找URL参数中是否包含该String
                arguments[i] = getOrDefault(request, parameterName, "");
            } else {
                throw new RuntimeException("Missing handler for type: " + parameterClass);
            }
        }

        return (ModelAndView) this.method.invoke(this.instance, arguments);
    }

    private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
        String s = request.getParameter(name); //请求中是否包含该名称的参数
        return s == null ? defaultValue : s;
    }
}

  和GET请求不同,POST请求严格地来说不能有URL参数,所以对于处理post请求的方法,HttpServletRequest、HttpServletResponse、HttpSession类型的参数照样从当前请求中获得传给该方法,其它类型的参数应当从当前请求的Body中读取数据传入。这里我们为了简化处理,只支持携带JSON格式数据的POST请求,这样把Post数据转化为JavaBean就非常容易。所以目前Controller中的POST请求处理方法的参数类型只支持HttpServletRequest、HttpServletResponse、HttpSession和JavaBean,如doSignin()方法。

public class PostDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    Class[] parameterClasses; // 方法参数类型
    ObjectMapper objectMapper; // 解析JSON数据

    public PostDispatcher(Object instance, Method method, Class[] parameterClasses){
        this.instance = instance;
        this.method = method;
        this.parameterClasses = parameterClasses;
        objectMapper = new ObjectMapper();
    }

    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response)
            throws IOException, InvocationTargetException, IllegalAccessException {

        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            Class parameterClass = parameterClasses[i]; //参数类型
            if (parameterClass == HttpServletRequest.class) { //参数类型是HttpServletRequest
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) { //参数类型是HttpServletResponse
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) { //参数类型是HttpSession
                arguments[i] = request.getSession();
            } else { //参数类型是JavaBean
                // 读取Body中的JSON并解析为JavaBean
                BufferedReader reader = request.getReader();
                arguments[i] = this.objectMapper.readValue(reader, parameterClass); //通过Jackson的objectMapper将JSON数据转换为JavaBean
            }
        }
        return (ModelAndView) this.method.invoke(instance, arguments);
    }
}

    可以看到,控制器中的方法都返回的是ModelAndView类型,它包含了要使用的html文件(类似jsp文件的功能)路径以及model数据。我们在servlet服务中可以将控制器返回的ModelAndView交给模板引擎来处理,模板引擎可以将html文件渲染好model数据后将内容写入到应答。我们还硬性规定模板必须放在webapp/WEB-INF/templates目录下。

public class ModelAndView {
    ModelAndView(String view){
        this.view = view;
    }
    ModelAndView(String view, Map model){
        this.view = view;
        this.model = model;
    }
    Map model; //数据
    String view; //html文件地址
}
public class ViewEngine {
    private final PebbleEngine engine;
    public ViewEngine(ServletContext servletContext) { //创建模板引擎
        // 定义一个ServletLoader用于加载模板
        ServletLoader loader = new ServletLoader(servletContext);
        loader.setCharset("UTF-8"); // 模板编码:
        loader.setPrefix("/WEB-INF/templates"); // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录
        loader.setSuffix("");  // 模板后缀

        // 创建Pebble实例:
        this.engine = new PebbleEngine.Builder()
                .autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
                .cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
                .loader(loader).build();
    }
    public void render(ModelAndView mv, Writer writer) throws IOException {
        // 使用模板引擎进行渲染
        PebbleTemplate template = this.engine.getTemplate(mv.view);
        template.evaluate(writer, mv.model);
    }
}

  JSP对页面开发不友好,所以我们使用模板引擎(又称ViewResolver,视图解析器)来处理HTML和数据,有很多开源的模板引擎,比如Thymeleaf、FreeMarker、Velocity、Pebble,这里使用的是Pebble模板引擎,它使用Jinja语法,特点是语法简单,支持模板继承,编写出来的模板类似下面的内容,即变量用{{ xxx }}表示,控制语句用{% xxx %}表示:



  

  我们还硬性规定静态文件必须放在webapp/static目录下,因此,为了便于开发,我们还顺带实现一个FileServlet来处理静态文件的请求:

@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
 class FileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        ServletContext ctx = req.getServletContext();
        String strContextPath = ctx.getContextPath(); // 获取上下文路径,如"/warName",ROOT包的话为空
        String urlPath = req.getRequestURI(); //获取RequestURI,如"/warName/hello.ico",ROOT包的话为"/hello"
        String path = urlPath.substring(strContextPath.length()); // RequestURI包含ContextPath,需要去掉,所以path为"/hello.ico"

        String filepath = ctx.getRealPath(path); // 获取真实文件路径
        if (filepath == null) { // 无法获取到路径
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Path path = Paths.get(filepath);
        if (!path.toFile().isFile()) { // 文件不存在
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String mime = Files.probeContentType(path); // 根据文件名猜测Content-Type
        if (mime == null) {
            mime = "application/octet-stream";
        }
        resp.setContentType(mime);

        // 读取文件并写入Response
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
            input.transferTo(output);
        }

        output.flush();
    }
}

  为了把方法参数的名称编译到class文件中,以使能通过反射获得方法的参数名,需要打开编译器的一个参数,在Eclipse中勾选Preferences-Java-Compiler-Store information about method parameters (usable via reflection);在Idea中选择Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters,填入-parameters;在Maven的pom.xml添加一段配置如下:


    4.0.0
    ...
    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                
                    
                        -parameters
                    
                
            
        
    

    经过上面的一番调整后,整个工程实际上是 DispatcherServlet(Servlet)+ UserController(Controller)+ GetDispatcher/PostDispatcher(连接Servlet和Controller:保存Controller中方法给Servlet使用)+ ModelAndView(保存Model和View)+ ViewEngine(模板引擎(视图解析器),用来处理ModelAndView)。业务流程是DispatcherServlet保存了UserController中的相关处理方法到GetDispatcher/PostDispatcher,当DispatcherServlet收到浏览器的请求后,通过请求的路径获得对应的GetDispatcher/PostDispatcher来调用对应的方法,然后通过方法获得对应的模型和视图ModelAndView,然后交给模板引擎ViewEngine来处理,模板引擎将处理后的内容写入到对浏览器的应答。整个工程的结构类似下面,其中的framework就是Spring MVC框架,controller包下是我们需要编写的业务逻辑。

web - mvc
├── pom.xml
└── src
└── main
├── java
│   └── xsl
│               ├── Main.java
│               ├── bean
│               │   ├── SignInBean.java
│               │   └── User.java
│               ├── controller
│               │   ├── IndexController.java
│               │   └── UserController.java
│               └── framework
│                   ├── DispatcherServlet.java
│                   ├── FileServlet.java
│                   ├── GetMapping.java
│                   ├── ModelAndView.java
│                   ├── PostMapping.java
│                   └── ViewEngine.java
└── webapp
├── WEB - INF
│   ├── templates
│   │   ├── _base.html
│   │   ├── hello.html
│   │   ├── index.html
│   │   ├── profile.html
│   │   └── signin.html
│   └── web.xml
└── static
        ├── css
        │   └── bootstrap.css
        └── js
             ├── bootstrap.js
             └── jquery.js

   Servlet:JSP、MVC、Spring MVC 【转】_第6张图片

Servlet:JSP、MVC、Spring MVC 【转】_第7张图片

  可以看到我们可以包含多个控制器,如下在每个控制器类中设置一个注解RequestMapping,可以通过解析请求的首个路径名称来区别使用哪个控制器:

	@RequestMapping("user")
	public class UserController {
		......
	};

	@RequestMapping("student")
		public class StudentController {
		......
	};

	http://HostName/user/edit
	http://HostName/student/edit

4、Spring MVC 不使用反射的方法  

  如果不使用反射的话,那么只能是在DispatcherServlet中定义一个处理类RequestHandle类型的对象,当请求到来后调用该对象的handlePost() / handleGet()。我们的UserController应该从RequestHandle继承,然后赋给DispatcherServlet中的RequestHandle类型的对象。在UserController中重写handlePost() / handleGet(),在handlePost() / handleGet()中选择调用UserController的哪个方法。

public class RequestHandle{
    public ModelAndView handleGet(HttpServletRequest req, HttpServletResponse resp){}
    public ModelAndView handlePost(HttpServletRequest req, HttpServletResponse resp){}
}
public class DispatcherServlet extends HttpServlet{
    RequestHandle requestHandle;
    void setHandle(RequestHandle requestHandle){
        this.requestHandle = requestHandle;
    }
    
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");

        ModelAndView mv = requestHandle.handleGet(req, resp);

        //将ModelAndView交给模板引擎处理
        ......
    }
}
public class UserController extends RequestHandle{
    @Override
    public ModelAndView handleGet(HttpServletRequest req, HttpServletResponse resp){

        ModelAndView mv = null;
        String path = req.getRequestURI().substring(req.getContextPath().length());
        if(path == "/signin"){
            mv = signin();
        }else if(path == "/signout"){
            HttpSession session = req.getSession();
            mv = signout(session);
        }
        ......

        return mv;
    }

    @Override
    public ModelAndView handlePost(HttpServletRequest req, HttpServletResponse resp){
        ModelAndView mv = null;
        String path = req.getRequestURI().substring(req.getContextPath().length());
        if(path == "/signin"){
            SignInBean bean = getBeanFromRequestBody(req);
            mv = doSignin(bean);
        }
        ......

        return mv;
    }

    public ModelAndView signin() {
        ...
    }

    public ModelAndView doSignin(SignInBean bean) {
        ...
    }

    public ModelAndView signout(HttpSession session) {
        ...
    }
    ......
}

5、总结

   使用JSP的话只需要向Tomcat提供一个html文件就可以展现网站内容,在html文件中可以嵌入java代码,并引入HttpServletRequest、HttpServletResponse等Servlet对象来进行相关操作。

   使用Servlet是在Java代码中添加HTML(HTTP请求处理中生成HTML内容),使用JSP是在HTML中添加Java代码(HTML文件中通过引入Servlet对象来获得展示数据)。可以使用MVC模式来处理Web请求,使Java代码和HTML不再混合,即页面展示和Web请求处理分离,在Web请求处理(Control)中生成页面展示需要的数据(Model)后传给页面(View)。

   Spring MVC就使用了MVC模式,Servlet根据用户的请求来调用Control类中对应的方法,我们提供Control实现类来编写处理用户请求的方法,Control类中方法将需要展示的JSP页面文件即View和其需要的数据Model 一同交给模板引擎,由模板引擎将页面和数据组合为标准HTML内容后发送给浏览器。Spring MVC实际上并不是使用JSP作为页面文件,比如可以使用Pebble模板文件,其也是一个类似HTML的文件,同JSP一样,在模板文件中也可以引用传给它的Model数据,模板文件由对应的模板引擎来生成标准HTML内容。

你可能感兴趣的:(Java,Web,java)