Java Web ——MVC基础框架讲解及代码演示(上):https://developer.aliyun.com/article/1508609
实现一个简单MVC框架
上文虽然结合JSP和Servlet可以发挥两者的优点,但是仍存在一些问题:
- Servlet提供的接口仍然偏向底层,我想用纯粹的Java语言咋办;
- JSP实现的页面比较简单,难以满足前端的各种骚操作,我想前端好看一点!
其实,廖大实现了一个简单的MVC框架(可以更好地理解Spring MVC的底层知识),可以直接阅读:
https://www.liaoxuefeng.com/wiki/1252599548343744/1337408645759009。
下面做一个简单介绍
首先,项目框架是这样的:
其中,bean其实就是后续Model将要用到的东西,根据具体的业务来定,User的具体实现如下:
public class User { public String email; public String password; public String name; public String description; public User() { } public User(String email, String password, String name, String description) { this.email = email; this.password = password; this.name = name; this.description = description; } }
controller其实就是日常开发中的业务处理逻辑,UserController负责用户登录、注销、用户展示:
public class UserController { private Map<String, User> userDatabase = new HashMap<String, User>() { { List<User> users = Arrays.asList( // new User("bob@example.com", "" + "", "Bob", "This is bob."), new User("tom@example.com", "tomcat", "Tom", "This is tom.")); users.forEach(user -> { put(user.email, user); }); } }; @GetMapping("/signin") public ModelAndView signin() { return new ModelAndView("/signin.html"); } @PostMapping("/signin") public ModelAndView doSignin(SignInBean bean, HttpServletResponse response, HttpSession session) throws IOException { User user = userDatabase.get(bean.email); if (user == null || !user.password.equals(bean.password)) { response.setContentType("application/json"); PrintWriter pw = response.getWriter(); pw.write("{\"error\":\"Bad email or password\"}"); pw.flush(); } else { session.setAttribute("user", user); response.setContentType("application/json"); PrintWriter pw = response.getWriter(); pw.write("{\"result\":true}"); pw.flush(); } return null; } @GetMapping("/signout") public ModelAndView signout(HttpSession session) { session.removeAttribute("user"); return new ModelAndView("redirect:/"); } @GetMapping("/user/profile") public ModelAndView profile(HttpSession session) { User user = (User) session.getAttribute("user"); if (user == null) { return new ModelAndView("redirect:/signin"); } return new ModelAndView("/profile.html", "user", user); } }
可以看到,编写业务处理逻辑只需要通过Get、Post注解来指明映射url地址即可,极为方便!返回的ModelAndView是给前端进行展示的东西,此处按下不表。
最后的framework就是MVC框架的核心内容,完全可以封装成一个包用作自己代码的后续开发。
首先,对于一个MVC框架来说,它需要一个分发器来接收所有的url,而后再根据请求内容将请求内容转发给对应的controller进行逻辑处理,分发器的实现:
//源码地址:https://www.liaoxuefeng.com/wiki/1252599548343744/1337408645759009 @WebServlet(urlPatterns = "/") public class DispatcherServlet extends HttpServlet { private final Map<String, GetDispatcher> getMappings = new HashMap<>(); private final Map<String, PostDispatcher> postMappings = new HashMap<>(); // 指定package并自动扫描: private final List<Class<?>> controllers = Arrays.asList(IndexController.class, UserController.class); private ViewEngine viewEngine; //扫描指定package中所有的Get和Post注解,获取对应的controller处理方法 @Override public void init() throws ServletException { // 利用反射,来获取所有Get和Post注解对应的method ...... // 创建ViewEngine: this.viewEngine = new ViewEngine(getServletContext()); } // 处理get请求 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { process(req, resp, this.getMappings); } catch (PebbleException e) { e.printStackTrace(); } } // 处理post请求 @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { process(req, resp, this.postMappings); } catch (PebbleException e) { e.printStackTrace(); } } // 主要逻辑是根据getMappings和postMappings获取指定的method,并通过反射调用 private void process(HttpServletRequest req, HttpServletResponse resp, Map<String, ? extends AbstractDispatcher> dispatcherMap) throws ServletException, IOException, PebbleException { ...... } }
上面所提到的注解,实现起来很简单:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface GetMapping { String value(); } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface PostMapping { String value(); }
之前提到的ModelAndView,其实主要有两个作用,一是指明模板html的地址,二是获取要展示的内容:
public class ModelAndView { Map<String, Object> model; String view; public ModelAndView(String view) { this.view = view; this.model = new HashMap<>(); } public ModelAndView(String view, String name, Object value) { this.view = view; this.model = new HashMap<>(); this.model.put(name, value); } public ModelAndView(String view, Map<String, Object> model) { this.view = view; this.model = new HashMap<>(model); } }
上述代码中,string view简单来说就是某一个html文件的路径,一般是通过模板引擎生成的,有关模板引擎可以参考:模板引擎.
另一个变量model,就是之前定义的bean,以获取用户信息为例:
@GetMapping("/user/profile") public ModelAndView profile(HttpSession session) { User user = (User) session.getAttribute("user"); if (user == null) { return new ModelAndView("redirect:/signin"); } return new ModelAndView("/profile.html", "user", user); }
最后还需要一个View对内容进行前端渲染,简单来说就是将需要展示的内容写入html中:
public class ViewEngine { private final PebbleEngine engine; public ViewEngine(ServletContext servletContext) { // 定义一个ServletLoader用于加载模板: ServletLoader loader = new ServletLoader(servletContext); // 模板编码: loader.setCharset("UTF-8"); // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录: loader.setPrefix("/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, PebbleException { // 查找模板: PebbleTemplate template = this.engine.getTemplate(mv.view); // 渲染: template.evaluate(writer, mv.model); } }
对以上内容做一个总结:
补充资料
重定向与转发
Redirect(重定向):服务器给浏览器回复一个302码同时附带新的url,浏览器接收到302响应后就会向新的url发送请求报文(浏览器发起了两次请求),如下:
Foward(转发):在Servlet内部进行转发,而不会发送到浏览器在对新的url进行请求,如下:
Session与Cookie
在Web应用程序中,经常需要保持用户的登录状态(不用频繁的输入账号密码),即我们需要有一个方式可以让Web应用能够识别用户的身份。
Servlet提供了一个基于Cookie的识别机制——Session。
Cookie是服务器给浏览器分配的唯一ID,并以Cookie的形式发送给浏览器,在后续的访问中,浏览器在请求报文中附带此Cookie,从而服务器可以识别其用户身份。
Session有什么缺点?
由于Session需要将用户数据保存在内存中,一方面,需要耗费一定的服务器资源;另一方面,当采用服务器集群时,必须保证同一个用户映射到固定的服务器上,这需要反向代理服务器的支持,比如采取Hash等方式将其转发至固定的服务器。
总之,由于Session的存在会使得服务器集群扩展较为艰难。
PS:Cookie只是请求报文中附加的信息而已,除了Cookie,还可以通过隐藏表单、URL末尾附加ID来追踪Session(只要有保证唯一性的标识即可)。
PS:所以平时的登录操作,可以看作是服务器给浏览器分配了一个Session ID;登出或者较长时间不操作,则从浏览器中移除这个ID,下次登录的时候就需要再登录了。