MVC框架设计
WEB 开发模式
WEB 开发模式一
Java 组件代码
Java 语言最大的特点是可以基于其完善的面向对象设计编写出大量的可重用程序代码,这些代码只要配置了正确的 CLASSPATH 就可以在任意的项目中引用
WEB 开发模式一
在实际的项目开发中,这些可以被使用的 Java 程序组件,可能会以“*.jar”文件打包的形式出现,也有可能会在项目中以程序类的形式出现,而这些都被统一称为模型层(Model),在进行用户请求处理时,可以利用模型层所提供的代码支持完成特定的业务处理功能,这样就可以将大部分的操作逻辑封装在模型层中,而作为显示层的 JSP 组件只要简单的进行组件的调用即可
WEB 开发模式二
WEB 开发模式二
在 JavaWEB 开发中 JSP 和 Servlet 组件都可以进行用户请求的接收与处理,相比较两个组件的特点可以发现,JSP 适合于进行数据的显示而 Servlet 适合于进行 Java 代码的编写,所以最佳的 WEB 开发结果就是通过 Servlet 处理用户请求与业务处理,同时将所有需要显示的数据内容以 request 属性的形式通过服务器端跳转到 JSP 页面,在 JSP 页面中可以基于各种前端技术实现 HTML 输出响应
控制层与业务层
用户每一次请求都有一个必然要实现的处理业务,所以在控制层处理请求时往往需要结合业务层中给定的业务方法进行操作,所以控制层就相当于实现了一个业务处理的分发机制以及页面数据显示的转发机制,是整个项目中连接显示层与业务层之间的重要组件
MVC 开发案例
MVC 设计实现
为了便于读者理解 MVC 的代码实现,下面将通过一个数据信息的列表显示功能为例进行 MVC 的实现说明,在本次处理中,客户端向控制层(Servet)发送-个 message 数据列表的显示请求,而后控制层找到模型层中的业务接口,并通过数据层实现数据加载,随后将此信息交由 JSP 页面进行显示,而在显示时为了方便将直接通过“EL +JSTL”完成
1、
DROP DATABASE IF EXISTS yootk ;
CREATE DATABASE yootk CHARACTER SET UTF8 ;
USE yootk ;
CREATE TABLE message (
id BIGINT AUTO_INCREMENT ,
title VARCHAR(50) ,
content TEXT,
CONSTRAINT pk_id PRIMARY KEY(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO message (title, content) VALUES ('编程训练营', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('编程训练营-B', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('编程训练营-C', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('编程训练营-D', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('编程训练营-E', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('编程训练营-F', 'edu.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(壹)', 'www.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(贰)', 'www.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(叁)', 'www.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(肆)', 'www.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(伍)', 'www.yootk.com');
INSERT INTO message (title, content) VALUES ('沐言科技(陆)', 'www.yootk.com');
COMMIT;
、2、
package com.yootk.vo;
import java.io.Serializable;
public class Message implements Serializable {
private Long id;
private String title;
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "【Message】id = " + this.id + "、title = " + this.title + "、content = " + this.content;
}
}
3、
package com.yootk.dao;
import com.yootk.common.dao.base.IBaseDAO;
import com.yootk.vo.Message;
public interface IMessageDAO extends IBaseDAO<Long, Message> {
}
4、
package com.yootk.dao.impl;
import com.yootk.common.dao.abs.AbstractDAO;
import com.yootk.dao.IMessageDAO;
import com.yootk.vo.Message;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class MessageDAOImpl extends AbstractDAO implements IMessageDAO {
@Override
public boolean doCreate(Message message) throws SQLException {
return false;
}
@Override
public boolean doEdit(Message message) throws SQLException {
return false;
}
@Override
public boolean doRemove(Set<Long> longs) throws SQLException {
return false;
}
@Override
public Message findById(Long aLong) throws SQLException {
return null;
}
@Override
public List<Message> findAll() throws SQLException {
List<Message> messageList = new ArrayList<>();
String sql = "SELECT id, title, content FROM message";
super.pstmt = super.connection.prepareStatement(sql);
ResultSet rs = super.pstmt.executeQuery();
while (rs.next()) {
Message msg = new Message();
msg.setId(rs.getLong(1));
msg.setTitle(rs.getString(2));
msg.setContent(rs.getString(3));
messageList.add(msg);
}
return messageList;
}
@Override
public List<Message> findSplit(Integer currentPage, Integer lineSize) throws SQLException {
return null;
}
@Override
public List<Message> findSplit(Integer currentPage, Integer lineSize, String column, String keyword) throws SQLException {
return null;
}
@Override
public Long getAllCount() throws SQLException {
return null;
}
@Override
public Long getAllCount(String column, String keyword) throws SQLException {
return null;
}
}
5、
message.dao=com.yootk.dao.impl.MessageDAOImpl
6、
package com.yootk.service;
import com.yootk.vo.Message;
import java.util.List;
public interface IMessageService {
public List<Message> list() throws Exception;
}
7、
package com.yootk.service.impl;
import com.yootk.common.service.abs.AbstractService;
import com.yootk.common.util.factory.ObjectFactory;
import com.yootk.dao.IMessageDAO;
import com.yootk.service.IMessageService;
import com.yootk.vo.Message;
import java.util.List;
public class MessageServiceImpl extends AbstractService implements IMessageService {
private IMessageDAO messageDAO = ObjectFactory.getDAOInstance("message.dao", IMessageDAO.class);
@Override
public List<Message> list() throws Exception {
return this.messageDAO.findAll();
}
}
8、
message.service=com.yootk.service.impl.MessageServiceImpl
9、
package com.yootk.servlet;
import com.yootk.common.util.factory.ObjectFactory;
import com.yootk.service.IMessageService;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/message.action")
public class MessageServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取要操作的业务接口对象实例,此时返回的是代理设计的实例
IMessageService messageService = ObjectFactory.getServiceInstance("message.service", IMessageService.class);
try {
req.setAttribute("messageList", messageService.list());
} catch (Exception e) {
e.printStackTrace();
}
req.getRequestDispatcher("/message.jsp").forward(req, resp);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
}
10、
<%@ page pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>沐言科技:www.yootk.com</title>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://"
+ request.getServerName() + ":" + request.getServerPort()
+ path + "/";
%>
<base href="<%=basePath%>">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="images/favicon.ico" type="image/x-icon" />
<script type="text/javascript" src="jquery/jquery.min.js"></script>
<script type="text/javascript" src="jquery/jquery.validate.min.js"></script>
<script type="text/javascript" src="jquery/additional-methods.min.js"></script>
<script type="text/javascript" src="jquery/Message_zh_CN.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap-datetimepicker.js" charset="UTF-8"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap-datetimepicker.zh-CN.js" charset="UTF-8"></script>
<link href="bootstrap/css/bootstrap-datetimepicker.min.css" rel="stylesheet" media="screen">
<link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.css" />
<script type="text/javascript" src="js/index.js"></script>
</head>
<body>
<div> </div>
<div class="container">
<div class="panel panel-info">
<div class="panel-heading">
<strong><img src="images/logo.png" style="height: 30px;"> MVC设计模式</strong>
</div>
<div class="panel-body">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr id="message-title"><td>消息ID</td><td>消息标题</td><td>消息内容</td></tr>
</thead>
<tbody id="messageBody">
<c:forEach items="${messageList}" var="msg">
<tr id="message-${msg.id}"><td>${msg.id}</td><td>${msg.title}</td><td>${msg.content}</td></tr>
</c:forEach>
</tbody>
</table>
</div>
<div class="panel-footer">
<div style="text-align:right;">
<strong>沐言科技(www.yootk.com) —— 新时代软件教育领导品牌</strong>
</div>
</div>
</div>
</div>
</body>
</html>
MVC 设计问题汇总
传统业务处理
在 MVC 设计模式中,用户的所有请求都会提交到控制层中,而后由控制层进行相关业务方法的调用,但是在实际的项目开发中,任何一个业务接口中都有可能定义有大量的业务方法,所以如果按照之前开发的程序模式,最终要想完成一套数据表的基础 CRUD 操作,就可能需要编写多个 Servet 程序类
多业务设计
多业务设计简介
DispatcherServlet 分发处理类
是一个原生的 Servlet 程序类,映射路径为“*.action”,在该类中根据需要覆写 doGet()/doPost()方法,用户所发送的请求首先一定要通过此类进行接收,而后根据用户请求路径的不同去找到对应的 Action 业务处理类,最终用户的响应或者页面的跳转由 DispatcherServlet 来完成
Action 业务处理类
一个项目中可能会有多种不同的业务需要进行处理,所以一般都会存在有多个 Action 功能类,每一个 Action 类可以根据自己的需要定义业务处理方法,同时也需要为每一个业务处理方法匹配不同的映射路径。
1、
package com.yootk.common.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class EncodingFilter extends HttpFilter {
private String charset = "UTF-8"; // 定义默认的编码
@Override
public void init(FilterConfig filterConfig) throws ServletException {
if (filterConfig.getInitParameter("charset") != null) {
this.charset = filterConfig.getInitParameter("charset");
}
}
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding(this.charset);
response.setCharacterEncoding(this.charset);
chain.doFilter(request, response);
}
}
2、
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>com.yootk.common.filter.EncodingFilter</filter-class>
<init-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3、
package com.yootk.common.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("【用户请求】线程名称 = " + Thread.currentThread().getName() + "、request = " + req + "、response = " + resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
4、
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.yootk.common.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
5、
http://localhost/muyan.action
6、
package com.yootk.common.servlet;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
private String message; // 保存属性内容
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("【DispatcherServlet】属性输出,message = " + this.message);
this.message = req.getParameter("message"); // 接收请求参数
System.out.println("【用户请求】线程名称 = " + Thread.currentThread().getName() +
"、request = " + req + "、response = " + resp +
"、message = " + this.message);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
7、
http://localhost/muyan.action?message=www.yootk.com
http://localhost/muyan.action?message=edu.yootk.com
Action 控制层配置
Action 处理结构
在一个项目中为了完成所需要的程序功能,一般会建立大量的 Action 程序类,而后所有的 Action 类依据映射路径被 DispatcherServlet 所调用,当 Action 类处理完成后再将所需要的处理结果返回到 DispatcherServlet 类之中,并向客户端进行响应处理
控制层注解
项目中的会随着业务复杂程度的不同而包含有多个 Action 程序类,由于所有的 Action 类都需要通过 DispatcherServlet 类进行反射调用,一般都可以通过特定的注解进行标记,这样也可以便于开发者简化 Action 类的定义,在 MVC 框架设计中一般会在 Action 处理类中使用如下两个注解:
- “@Controler”:该注解主要用于定义控制层中的业务处理类,
- "@RequestMapping”:该注解主要进行 Action 类处理路径的映射在请求时根据此注解加载 Action 类实例。
1、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE}) // 该注解需要在类型的定义上使用
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface Controller {
public String value() default "none"; // 表示控制层名称
}
2、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
// 该注解可以在类上使用,也可以在方法上使用,如果在类上使用就表示定义父路径,而在访问上使用表示配置子路径
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface RequestMapping {
public String value() default "/"; // 路径的名称
}
3、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
}
4、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
@Controller // 表示当前的类是一个控制器处理类
//@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@RequestMapping("/pages/message/add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
}
5、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
@RequestMapping("list") // 真实访问路径:/pages/message/list.action
public void list() {
System.out.println("【MessageAction.list()】消息内容列表。");
}
@RequestMapping("remove") // 真实访问路径:/pages/message/remove.action
public void remove() {
System.out.println("【MessageAction.remove()】删除消息内容。");
}
@RequestMapping("edit") // 真实访问路径:/pages/message/edit.action
public void edit() {
System.out.println("【MessageAction.edit()】编辑消息内容。");
}
}
Action 解析处理
1、
package com.yootk.common.mvc.bean;
import java.lang.reflect.Method;
// 同一个Action之中的处理方法有可能会存在有若干个Method,每一个Method映射都要有一个此类对象包装
public class ControllerRequestMapping { // 控制层的数据关联
private Class<?> actionClazz; // 保存匹配的Action类的信息
private Method actionMethod; // 保存映射的访问
public ControllerRequestMapping(Class<?> actionClazz, Method actionMethod) {
this.actionClazz = actionClazz;
this.actionMethod = actionMethod;
}
public Class<?> getActionClazz() {
return actionClazz;
}
public Method getActionMethod() {
return actionMethod;
}
}
2、
\package com.yootk.common.mvc.util;
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import org.apache.hc.client5.http.fluent.Request;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class ConfigAnnotationParseUtil { // 配置Annotation解析类
// key为映射路径,而value保存的是Action与Method的关联对象实例
private Map<String, ControllerRequestMapping> controllerMapResult = new HashMap<>();
private String parentUrl; // 保存父路径
private Class<?> clazz;
private ConfigAnnotationParseUtil(Class<?> clazz) { // 解析类的处理
this.clazz = clazz; // 当前要根据传入的Action进行解析处理
this.classHandle(); // 实现解析控制
}
public void classHandle() { // 进行具体的解析处理操作
Annotation annotations [] = this.clazz.getAnnotations(); // 获取全部的Annotation
for (Annotation annotation : annotations) { // 迭代全部的Annotation
if (annotation.annotationType().equals(Controller.class)) { // 自定义Annotation
try { // 在整个的控制器类之中对于访问路径的配置有两种形式,一种是进行父路径配置,一种是直接在子路径上编写
RequestMapping mapping = this.clazz.getAnnotation(RequestMapping.class); // 获取指定的Annotation
this.parentUrl = mapping.value(); // 获取映射的父路径
if (this.parentUrl.lastIndexOf("/") == -1) { // 没有结尾路径符
this.parentUrl += "/"; // 追加一个结尾的路径分隔符
}
} catch (Exception e) {}
this.handleMappingMethod(); // 解析Action类的处理方法
}
}
}
// 在每一个Action程序类中还会存在有大量的程序控制方法,因为这些方法可以完成具体的业务处理
private void handleMappingMethod() { // 解析映射处理方法
if (this.parentUrl == null) { // 现在没有得到父路径
this.parentUrl = ""; // 现在没有父路径
}
// 获取当前类之中的所有的处理方法,从这些方法里面找到拥有“@RequestMapping”注解的方法
Method methods[] = this.clazz.getDeclaredMethods(); // 获取类中的全部方法
for (Method method : methods) { // 循环所有的方法
if (method.isAnnotationPresent(RequestMapping.class)) { // 判断拥有指定的注解项
RequestMapping mapping = method.getAnnotation(RequestMapping.class); // 获取指定Annotation
String path = this.parentUrl + mapping.value(); // 获取完整路径
this.controllerMapResult.put(path, new ControllerRequestMapping(this.clazz, method));
}
}
}
/**
* 将一个Action类的解析结果进行返回(key = 访问路径、value = Class/Method)
* @return 返回Map集合
*/
public Map<String, ControllerRequestMapping> getControllerMapResult() {
return controllerMapResult;
}
}
包扫描处理
1、
<context-param>
<param-name>base-package</param-name>
<param-value>com.yootk.action;com.yootk.service;com.yootk.dao</param-value>
</context-param>
2、
package com.yootk.common.mvc.util;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class ScannerPackageUtil { // 扫描工具类
// 保存整个项目之中全部控制器的访问映射处理,通过ConfigAnnotationParseUtil工具处理
private static final Map<String, ControllerRequestMapping> ACTION_MAP = new HashMap<>();
// 这个路径是随着项目的部署而动态获取的,此时的设计不考虑打包为*.jar文件后的处理
private static String baseDir = null; // 公共的项目路径
private ScannerPackageUtil() {} // 构造方法私有化
/**
* 实现扫描包的配置处理
* @param clazz 调用此类的程序类,一般就是DispatcherServlet
* @param packages 配置的扫描包名称,使用“;”进行分割
*/
public static void scannerHandle(Class<?> clazz, String packages) { // 进行解析处理
if (packages == null || "".equals(packages)) { // 没有配置扫描包
return; // 结束方法调用
}
String resultPackages[] = packages.split(";"); // 按照“;”拆分
// 传入的Class类型是DispatcherServlet程序类
baseDir = clazz.getResource("/").getPath();// 获取具体路径
baseDir = baseDir.substring(1).replace("/", File.separator); // 更换路径符号
for (int x = 0; x < resultPackages.length; x++) { // 扫描包的拼凑
String subDir = resultPackages[x].replace(".", File.separator); // 获取子路径
listDirClass(new File(baseDir, subDir)); // 列出所有的Class名称
}
}
public static void listDirClass(File file) { // 目录的列出
if (file.isDirectory()) { // 当前给定的是一个目录
File result [] = file.listFiles(); // 目录列表
if (result != null) { // 有可能无法列出信息
for (int x = 0; x < result.length; x ++) {
listDirClass(result[x]); // 递归的目录列表
}
}
} else { // 不是目录
if (file.isFile()) { // 给定的是一个文件
String className = file.getAbsolutePath().replace(baseDir, "") // 替换掉父路径
.replace(File.separator, ".").replace("*.class", ""); // 类名称
try { // 通过之前的解析工具类根据指定的类名称获取对应的程序结构
ACTION_MAP.putAll(new ConfigAnnotationParseUtil(Class.forName(className)).getControllerMapResult());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static Map<String, ControllerRequestMapping> getActionMap() {
return ACTION_MAP;
}
}
H:\workspace\idea\muyan_yootk\out\artifacts\mvc_war_exploded\WEB-INF\classes\com\yootk\action\DeptAction.class
控制层分发
业务分发处理
控制层分发的处理机制主要是通过 DispatcherServlet 程序类来实现的,在控制层接收用户请求后可以根据用户请求的 URI 地址并通过 ScannerPackageUtil 工具类来找到对应的 Action 类与 Method 对象,随后就可以基于反射进行调用
package com.yootk.common.servlet;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import com.yootk.common.mvc.util.ScannerPackageUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
@Override
public void init() throws ServletException { // 初始化的时候进行扫描配置
// 获取ServletContext所配置的初始化上下文参数
String basePackage = super.getServletContext().getInitParameter("base-package"); // 获取初始化参数
ScannerPackageUtil.scannerHandle(super.getClass(), basePackage); // 扫描处理
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取了当前请求路径,而这个路径恰好就是Action配置的映射路径
String path = req.getServletPath().substring(0, req.getServletPath().lastIndexOf("."));
// 包扫描完成之后,所有的映射路径实际上都保存在了“Map”集合之中
ControllerRequestMapping mapping = ScannerPackageUtil.getActionMap().get(path);
// 此时可以获取到映射的Action类(Class实例)以及对应的Method实例
try {
Object actionObject = mapping.getActionClazz().getDeclaredConstructor().newInstance(); // 反射获取Action类对象
mapping.getActionMethod().invoke(actionObject); // 方法的反射调用
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
内置对象包装
内置对象存储
用户向服务器端所发送的请求与响应,可以直接通过 Servet 类提供业务处理方法直接获取,但是现在的控制层由于 DispatcherServet 仅仅实现请求转发,所以真正的业务处理是在 Action 类中完成,为了便于 Action 类获取 request 与 response 对象实例,就可以通过 ThreadLocal 实现对象保存
1、
package com.yootk.common.servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class ServletObject {
private static final ThreadLocal<HttpServletRequest> THREAD_REQUEST = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> THREAD_RESPONSE = new ThreadLocal<>();
private ServletObject() {} // 构造方法私有化
public static void setRequest(HttpServletRequest request) {
THREAD_REQUEST.set(request);
}
public static void setResponse(HttpServletResponse response) {
THREAD_RESPONSE.set(response);
}
public static HttpServletRequest getRequest() {
return THREAD_REQUEST.get();
}
public static HttpServletResponse getResponse() {
return THREAD_RESPONSE.get();
}
public static HttpSession getSession() {
return THREAD_REQUEST.get().getSession();
}
public static ServletContext getApplication() {
return THREAD_REQUEST.get().getServletContext();
}
public static void clean() {
THREAD_REQUEST.remove();
THREAD_RESPONSE.remove();
}
}
2、
package com.yootk.common.servlet;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import com.yootk.common.mvc.util.ScannerPackageUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
@Override
public void init() throws ServletException { // 初始化的时候进行扫描配置
// 获取ServletContext所配置的初始化上下文参数
String basePackage = super.getServletContext().getInitParameter("base-package"); // 获取初始化参数
ScannerPackageUtil.scannerHandle(super.getClass(), basePackage); // 扫描处理
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取了当前请求路径,而这个路径恰好就是Action配置的映射路径
String path = req.getServletPath().substring(0, req.getServletPath().lastIndexOf("."));
// 包扫描完成之后,所有的映射路径实际上都保存在了“Map”集合之中
ControllerRequestMapping mapping = ScannerPackageUtil.getActionMap().get(path);
// 此时可以获取到映射的Action类(Class实例)以及对应的Method实例
try {
ServletObject.setRequest(req); // 在当前的线程内保存有request
ServletObject.setResponse(resp); // 在当前的线程内保存有response
Object actionObject = mapping.getActionClazz().getDeclaredConstructor().newInstance(); // 反射获取Action类对象
mapping.getActionMethod().invoke(actionObject); // 方法的反射调用
} catch (Exception e) {
e.printStackTrace();
} finally {
ServletObject.clean(); // 删除当前线程request/response
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
3、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.servlet.ServletObject;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【Request内置对象】" + ServletObject.getRequest().getContextPath());
System.out.println("【Session内置对象】" + ServletObject.getSession().getId());
System.out.println("【Application内置对象】" + ServletObject.getApplication().getVirtualServerName());
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
@RequestMapping("list") // 真实访问路径:/pages/message/list.action
public void list() {
System.out.println("【MessageAction.list()】消息内容列表。");
}
@RequestMapping("remove") // 真实访问路径:/pages/message/remove.action
public void remove() {
System.out.println("【MessageAction.remove()】删除消息内容。");
}
@RequestMapping("edit") // 真实访问路径:/pages/message/edit.action
public void edit() {
System.out.println("【MessageAction.edit()】编辑消息内容。");
}
}
ModelAndView
Action 处理形式
每一个 Action 中的业务处理方法有可能会根据需要实现数据的直接输出(无返回值)、页面跳转以及属性传递处理操作
1、
package com.yootk.common.servlet;
import java.util.Map;
public class ModelAndView {
private String view; // 视图路径(跳转路径)
public ModelAndView() {}
public ModelAndView(String view) {
this.view = view;
}
public ModelAndView(String view, String name, Object value) {
this.view = view;
this.add(name, value);
}
public ModelAndView(String view, Map<String, Object> map) {
this.view = view;
this.add(map);
}
public void add(String name, Object value) { // 属性的存储
ServletObject.getRequest().setAttribute(name, value);
}
public void add(Map<String, Object> map) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
ServletObject.getRequest().setAttribute(entry.getKey(), entry.getValue());
}
}
public String getView() {
return view;
}
public void setView(String view) {
this.view = view;
}
}
2、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.servlet.ModelAndView;
import com.yootk.common.servlet.ServletObject;
import com.yootk.vo.Message;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
@RequestMapping("list") // 真实访问路径:/pages/message/list.action
public String list() { // 跳转路径的配置
System.out.println("【MessageAction.list()】消息内容列表。");
return "/pages/front/message/message_list.jsp"; // 设置返回路径
}
@RequestMapping("remove") // 真实访问路径:/pages/message/remove.action
public void remove() throws Exception {
ServletObject.getResponse().getWriter().print("【MessageAction.remove()】删除消息内容。");
}
@RequestMapping("edit") // 真实访问路径:/pages/message/edit.action
public ModelAndView edit() {
System.out.println("【MessageAction.edit()】编辑消息内容。");
Message msg = new Message();
msg.setTitle("沐言科技");
msg.setContent("www.yootk.com");
ModelAndView mav = new ModelAndView("/pages/front/message/message_edit.jsp", "message", msg);
return mav;
}
}
3、
package com.yootk.common.servlet;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import com.yootk.common.mvc.util.ScannerPackageUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
@Override
public void init() throws ServletException { // 初始化的时候进行扫描配置
// 获取ServletContext所配置的初始化上下文参数
String basePackage = super.getServletContext().getInitParameter("base-package"); // 获取初始化参数
ScannerPackageUtil.scannerHandle(super.getClass(), basePackage); // 扫描处理
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String dispatcherPath = null; // 跳转路径
// 获取了当前请求路径,而这个路径恰好就是Action配置的映射路径
String path = req.getServletPath().substring(0, req.getServletPath().lastIndexOf("."));
// 包扫描完成之后,所有的映射路径实际上都保存在了“Map”集合之中
ControllerRequestMapping mapping = ScannerPackageUtil.getActionMap().get(path);
// 此时可以获取到映射的Action类(Class实例)以及对应的Method实例
try {
ServletObject.setRequest(req); // 在当前的线程内保存有request
ServletObject.setResponse(resp); // 在当前的线程内保存有response
Object actionObject = mapping.getActionClazz().getDeclaredConstructor().newInstance(); // 反射获取Action类对象
// 在进行方法反射调用的时候会有返回内容,而返回的内容统一类型为Object
Object returnValue = mapping.getActionMethod().invoke(actionObject); // 方法的反射调用
if (returnValue != null) { // 有返回值,方法的返回值不是void
if (returnValue instanceof String) { // 当前方法返回了字符串
dispatcherPath = returnValue.toString(); // 直接获取跳转路径
}
if (returnValue instanceof ModelAndView) { // 返回ModelAndView
ModelAndView modelAndView = (ModelAndView) returnValue;
dispatcherPath = modelAndView.getView(); // 获取跳转的视图路径
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
ServletObject.clean(); // 删除当前线程request/response
}
if (dispatcherPath != null) { // 有跳转路径配置
req.getRequestDispatcher(dispatcherPath).forward(req, resp);
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doGet(req, resp);
}
}
依赖注入基本模型
1、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE}) // 该注解需要在类型的定义上使用
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface Service {
public String value() default "none"; // 设置具体的注解名称
}
2、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE}) // 该注解需要在类型的定义上使用
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface Repository {
public String value() default "none"; // 设置具体的注解名称
}
3、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD, ElementType.FIELD}) // 成员属性以及方法上司hi用
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface Autowired { // 注入管理
public String name() default "none"; // 每一个注解的组件都会存在有一个名称
}
4、
package com.yootk.common.mvc.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE}) // 该注解需要在类型的定义上使用
@Retention(RetentionPolicy.RUNTIME) // 该注解在运行时生效
public @interface Aspect {}
5、
package com.yootk.service.impl;
import com.yootk.common.mvc.annotation.Aspect;
import com.yootk.common.mvc.annotation.Autowired;
import com.yootk.common.mvc.annotation.Service;
import com.yootk.common.service.abs.AbstractService;
import com.yootk.dao.IMessageDAO;
import com.yootk.service.IMessageService;
import com.yootk.vo.Message;
import java.util.List;
@Service
@Aspect
public class MessageServiceImpl extends AbstractService implements IMessageService {
@Autowired(name = "messageDAOImpl")
private IMessageDAO messageDAO;
@Override
public List<Message> list() throws Exception {
return this.messageDAO.findAll();
}
}
模型层扫描处理
1、
package com.yootk.common.util;
public class StringUtil { // 实现字符串的相关处经理
private StringUtil() {
} // 构造方法私有化
public static String initcap(String str) {
if (str == null || "".equals(str)) {
return str; // 现在内容是空字符串
}
if (str.length() == 1) {
return str.toUpperCase();
}
return str.substring(0, 1).toUpperCase() + str.substring(1); // 首字母大写
}
public static String firstLower(String str) { // 首字母小写
if (str == null || "".equals(str)) {
return str;
}
if (str.length() == 1) {
return str.toLowerCase();
}
return str.substring(0, 1).toLowerCase() + str.substring(1); //
}
}
2、
// 业务层和数据层的Map集合:key = 设置的名称、value = 对应的类型
private Map<String, Class> nameAndTypeMap = new HashMap<>();
// 业务层和数据层的Map集合:key = 接口的类型、value = 对应的类型
private Map<Class, Class> objectInterfaceAndClassMap = new HashMap<>();
3、
package com.yootk.common.mvc.util;
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.Repository;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.mvc.annotation.Service;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import com.yootk.common.util.StringUtil;
import org.apache.hc.client5.http.fluent.Request;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
public class ConfigAnnotationParseUtil { // 配置Annotation解析类
// key为映射路径,而value保存的是Action与Method的关联对象实例
private Map<String, ControllerRequestMapping> controllerMapResult = new HashMap<>();
// 业务层和数据层的Map集合:key = 设置的名称、value = 对应的类型
private Map<String, Class> nameAndTypeMap = new HashMap<>();
// 业务层和数据层的Map集合:key = 接口的类型、value = 对应的类型
private Map<Class, Class> objectInterfaceAndClassMap = new HashMap<>();
private String parentUrl; // 保存父路径
private Class<?> clazz;
public ConfigAnnotationParseUtil(Class<?> clazz) { // 解析类的处理
this.clazz = clazz; // 当前要根据传入的Action进行解析处理
this.classHandle(); // 实现解析控制
}
public void classHandle() { // 进行具体的解析处理操作
Annotation annotations [] = this.clazz.getAnnotations(); // 获取全部的Annotation
for (Annotation annotation : annotations) { // 迭代全部的Annotation
if (annotation.annotationType().equals(Controller.class)) { // 自定义Annotation
try { // 在整个的控制器类之中对于访问路径的配置有两种形式,一种是进行父路径配置,一种是直接在子路径上编写
RequestMapping mapping = this.clazz.getAnnotation(RequestMapping.class); // 获取指定的Annotation
this.parentUrl = mapping.value(); // 获取映射的父路径
if (this.parentUrl.lastIndexOf("/") == -1) { // 没有结尾路径符
this.parentUrl += "/"; // 追加一个结尾的路径分隔符
}
} catch (Exception e) {}
this.handleMappingMethod(); // 解析Action类的处理方法
} else if (annotation.annotationType().equals(Service.class)) { // 业务层注解
Service service = this.clazz.getAnnotation(Service.class);
if ("none".equalsIgnoreCase(service.value())) { // 是一个空内容
// 此时没有为Service设置具体的名称,那么这个名称就应该由类名称直接生成
// 此时将类名称首字母小写,例如:MessageServiceImpl子类,名称为“messageServiceImpl”
this.nameAndTypeMap.put(StringUtil.firstLower(this.clazz.getSimpleName()),this.clazz);
} else { // 用户设置了具体的Bean名称
this.nameAndTypeMap.put(service.value(), this.clazz);
}
// 除了实现名称的注入之外,还有可能会根据类型实现注入的管理操作,最佳的做法就是匹配接口
Class<?> clazzInterfaces[] = this.clazz.getInterfaces(); // 获取全部的接口
for (int x = 0; x < clazzInterfaces.length; x ++) { // 实现接口迭代
this.objectInterfaceAndClassMap.put(clazzInterfaces[x], this.clazz);
}
} else if (annotation.annotationType().equals(Repository.class)) { // 数据层注解
Repository repository = this.clazz.getAnnotation(Repository.class); // 获取数据层注解
if ("none".equals(repository.value())) { // 判断是否有名称
this.nameAndTypeMap.put(StringUtil.firstLower(this.clazz.getSimpleName()), this.clazz);
} else {
this.nameAndTypeMap.put(repository.value(), this.clazz);
}
Class<?> clazzInterfaces[] = this.clazz.getInterfaces(); // 获取全部接口
for (int x = 0; x < clazzInterfaces.length; x++) { // 迭代全部接口
this.objectInterfaceAndClassMap.put(clazzInterfaces[x], this.clazz);
}
}
}
}
// 在每一个Action程序类中还会存在有大量的程序控制方法,因为这些方法可以完成具体的业务处理
private void handleMappingMethod() { // 解析映射处理方法
if (this.parentUrl == null) { // 现在没有得到父路径
this.parentUrl = ""; // 现在没有父路径
}
// 获取当前类之中的所有的处理方法,从这些方法里面找到拥有“@RequestMapping”注解的方法
Method methods[] = this.clazz.getDeclaredMethods(); // 获取类中的全部方法
for (Method method : methods) { // 循环所有的方法
if (method.isAnnotationPresent(RequestMapping.class)) { // 判断拥有指定的注解项
RequestMapping mapping = method.getAnnotation(RequestMapping.class); // 获取指定Annotation
String path = this.parentUrl + mapping.value(); // 获取完整路径
this.controllerMapResult.put(path, new ControllerRequestMapping(this.clazz, method));
}
}
}
/**
* 将一个Action类的解析结果进行返回(key = 访问路径、value = Class/Method)
* @return 返回Map集合
*/
public Map<String, ControllerRequestMapping> getControllerMapResult() {
return controllerMapResult;
}
}
4、
package com.yootk.common.mvc.util;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class ScannerPackageUtil { // 扫描工具类
// 保存整个项目之中全部控制器的访问映射处理,通过ConfigAnnotationParseUtil工具处理
private static final Map<String, ControllerRequestMapping> ACTION_MAP = new HashMap<>();
// 保存整个项目之中全部根据名称匹配的Class实例
private static final Map<String, Class> BY_NAME_MAP = new HashMap<>();
// 保存整个项目之中全局根据类型匹配的Class实例(根据类型实现自动注入管理)
private static final Map<Class, Class> BY_TYPE_MAP = new HashMap<>();
// 这个路径是随着项目的部署而动态获取的,此时的设计不考虑打包为*.jar文件后的处理
private static String baseDir = null; // 公共的项目路径
private ScannerPackageUtil() {} // 构造方法私有化
/**
* 实现扫描包的配置处理
* @param clazz 调用此类的程序类,一般就是DispatcherServlet
* @param packages 配置的扫描包名称,使用“;”进行分割
*/
public static void scannerHandle(Class<?> clazz, String packages) { // 进行解析处理
if (packages == null || "".equals(packages)) { // 没有配置扫描包
return; // 结束方法调用
}
String resultPackages[] = packages.split(";"); // 按照“;”拆分
// 传入的Class类型是DispatcherServlet程序类
baseDir = clazz.getResource("/").getPath();// 获取具体路径
baseDir = baseDir.substring(1).replace("/", File.separator); // 更换路径符号
for (int x = 0; x < resultPackages.length; x++) { // 扫描包的拼凑
String subDir = resultPackages[x].replace(".", File.separator); // 获取子路径
listDirClass(new File(baseDir, subDir)); // 列出所有的Class名称
}
}
public static void listDirClass(File file) { // 目录的列出
if (file.isDirectory()) { // 当前给定的是一个目录
File result [] = file.listFiles(); // 目录列表
if (result != null) { // 有可能无法列出信息
for (int x = 0; x < result.length; x ++) {
listDirClass(result[x]); // 递归的目录列表
}
}
} else { // 不是目录
if (file.isFile()) { // 给定的是一个文件
String className = file.getAbsolutePath().replace(baseDir, "") // 替换掉父路径
.replace(File.separator, ".").replace(".class", ""); // 类名称
try { // 通过之前的解析工具类根据指定的类名称获取对应的程序结构
ConfigAnnotationParseUtil parseUtil = new ConfigAnnotationParseUtil(Class.forName(className));
ACTION_MAP.putAll(parseUtil.getControllerMapResult()); // 获取全部控制层
BY_NAME_MAP.putAll(parseUtil.getNameAndTypeMap());
BY_TYPE_MAP.putAll(parseUtil.getObjectInterfaceAndClassMap());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public static Map<String, ControllerRequestMapping> getActionMap() {
return ACTION_MAP;
}
}
注入对象实例
1、
package com.yootk.common.factory;
import com.yootk.common.service.proxy.ServiceProxy;
public class ObjectFactory { // 定义一个工厂类
// 考虑到后续的开发之中业务层的操作可能很多的地方都会使用到,所以将其保存在ThreadLocal之中
private static final ThreadLocal<Object> SERVICE_OBJECTS = new ThreadLocal<>();
private ObjectFactory() {} // 构造方法私有化
public static Object getInstance(Class<?> clazz) { // DAO接口实例
Object result = null;
try {
result = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {}
return result;
}
public static Object getServiceInstance(Class<?> clazz) { // 控制层调用
Object result = null;
try {
SERVICE_OBJECTS.set(clazz.getConstructor().newInstance());
result = new ServiceProxy().bind(SERVICE_OBJECTS.get()); // 返回代理对象
} catch (Exception e) {}
return result;
}
public static Object getOrignServiceObject() { // 留给后续其他方式使用
return SERVICE_OBJECTS.get();
}
}
2、
package com.yootk.common.mvc.bean;
import com.yootk.common.factory.ObjectFactory;
import com.yootk.common.mvc.annotation.Aspect;
import com.yootk.common.mvc.annotation.Autowired;
import com.yootk.common.mvc.util.ScannerPackageUtil;
import java.lang.reflect.Field;
public class DependObject {
// 如果是业务层的注入,则targetObject就是Action对象实例
// 如果是数据层的注入,则targetObject就是Service对象实例
private Object targetObject; // 要进行注入的管理类
// 一个类之中的属性上会存在有“@Autowired”注解,现在只考虑属性,所以扫描类中的属性即可
public DependObject(Object targetObject) {
this.targetObject = targetObject; // 根据目标对象进行控制
}
public void injectObject() { // 实现注入操作
// 当前只考虑成员属性的注入了,如果你有兴趣,则可以继续在此处实现方法之中的注入
Field fields [] = this.targetObject.getClass().getDeclaredFields(); // 获取全部的成员属性
for (int x = 0; x < fields.length; x ++) {
if (fields[x].isAnnotationPresent(Autowired.class)) { // 存在有注入的管理注解
Autowired autowired = fields[x].getAnnotation(Autowired.class);
// 当前的注入操作可以根据名称注入也可以根据类型注入,所以要先确定是否有名称
Class<?> injectClazz = null; // 通过ScannerPackageUtil工具找到
if ("none".equals(autowired.name())) { // 没有配置名称
// 根据类型实现注入操作
injectClazz = ScannerPackageUtil.getByTypeMap().get(fields[x].getType());
} else { // 此时存在有名称
injectClazz = ScannerPackageUtil.getByNameMap().get(autowired.name());
}
if (injectClazz != null) { // 已经查找到了对应的类型
try {
fields[x].setAccessible(true); // 取消封装
if (injectClazz.isAnnotationPresent(Aspect.class)) { // 业务层上存在这样的注解
fields[x].set(this.targetObject, ObjectFactory.getServiceInstance(injectClazz));
// 如果现在注入的是一个业务层对象,那么后续还有可能继续要注入数据层对象
new DependObject(ObjectFactory.getOrignServiceObject()).injectObject();
} else { // 没有事务的控制要求
fields[x].set(this.targetObject, ObjectFactory.getInstance(injectClazz));
}
} catch (Exception e) {}
}
}
}
}
}
参数接收设计方案
int age = Integer.parseInt(request.getParameter("age"))
1、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Autowired;
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.servlet.ModelAndView;
import com.yootk.common.servlet.ServletObject;
import com.yootk.service.IMessageService;
import com.yootk.vo.Message;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction { // 是一个独立的类
@Autowired // 如果每次都写上名字,实际上会过于繁琐,于是支持根据类型的注入
private IMessageService messageService;
@RequestMapping("add_pre")
public String addPre() {
return "/pages/front/message/message_add.jsp"; // 返回跳转的路径
}
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add() {
System.out.println("【MessageAction.add()】增加新的消息内容。");
}
@RequestMapping("list") // 真实访问路径:/pages/message/list.action
public String list() { // 跳转路径的配置
System.out.println("【MessageAction.list()】消息内容列表。");
try {
System.out.println(this.messageService.list());
} catch (Exception e) {}
return "/pages/front/message/message_list.jsp"; // 设置返回路径
}
@RequestMapping("remove") // 真实访问路径:/pages/message/remove.action
public void remove() throws Exception {
ServletObject.getResponse().getWriter().print("【MessageAction.remove()】删除消息内容。");
}
@RequestMapping("edit") // 真实访问路径:/pages/message/edit.action
public ModelAndView edit() {
System.out.println("【MessageAction.edit()】编辑消息内容。");
Message msg = new Message();
msg.setTitle("沐言科技");
msg.setContent("www.yootk.com");
ModelAndView mav = new ModelAndView("/pages/front/message/message_edit.jsp", "message", msg);
return mav;
}
}
2、
<%@ page pageEncoding="UTF-8" %>
<% // 通过request获取相关资源信息,拼凑成完整的访问路径
String basePath = request.getScheme() + "://" + request.getServerName() + ":" +
request.getServerPort() + request.getContextPath() + "/" ;
%>
<html>
<head>
<title>沐言科技:www.yootk.com</title>
<base href="<%=basePath%>">
<meta name="viewport" content="width=device-width,initial-scale=1">
<script type="text/javascript" src="js/jquery.min.js"></script>
<script type="text/javascript" src="bootstrap/js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="bootstrap/css/bootstrap.min.css" />
<script type="text/javascript" src="js/yootk.util.js"></script>
<script type="text/javascript" src="js/login.js"></script>
</head>
<body>
<div> </div>
<div class="container">
<div class="panel panel-info">
<div class="panel-heading">
<strong><img src="images/logo.png" style="height: 30px;"> 自定义MVC开发框架</strong>
</div>
<div class="panel-body">
<form action="/pages/message/add.action" class="form-horizontal" id="myform" method="post" enctype="multipart/form-data">
<fieldset>
<div class="form-group" id="titleDiv">
<label class="col-md-2 control-label" for="title">消息标题:</label>
<div class="col-md-5">
<input type="text" name="title" id="title" class="form-control input-sm" placeholder="请输入要发送的消息标题...">
</div>
<div class="col-md-4" id="titleMsg">*</div>
</div>
<div class="form-group" id="contentDiv">
<label class="col-md-2 control-label" for="title">消息内容:</label>
<div class="col-md-5">
<input type="text" name="content" id="content" class="form-control input-sm" placeholder="请输入要发送的消息内容...">
</div>
<div class="col-md-4" id="contentMsg">*</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-5">
<button type="button" class="btn btn-sm btn-primary" id="sendBut"><span class="glyphicon glyphicon-open"/> 发送</button>
<button type="reset" class="btn btn-sm btn-warning"><span class="glyphicon glyphicon-retweet"/> 重置</button>
</div>
</div>
</fieldset>
</form>
</div>
<div class="panel-footer">
<div style="text-align:right;">
<strong>沐言科技(www.yootk.com) —— 新时代软件教育领导品牌</strong>
</div>
</div>
</div>
</div>
</body>
</html>
3、
package com.yootk.vo;
import java.io.Serializable;
public class Message implements Serializable {
private Long id;
private String title;
private String content;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "【Message】id = " + this.id + "、title = " + this.title + "、content = " + this.content;
}
}
获取方法参数名称
参数自动接收
如果要想实现请求参数的自动接收处理,那么就必须建立 HTTP 请求参数和 Action 业务处理方法参数之间的关联,而最简单的关联形式就是保持参数名称的一致,而在触发某一个 Action 方法执行时,都可以动态的获取方法的参数名称列表,再通过列表实现参数的接收而要想实现这样的参数列表机制的获取,就需要开发者手工的进行字节码文件的二进制读取处理
1、
package com.yootk.test;
import com.yootk.action.MessageAction;
import java.lang.reflect.Method;
public class TestActionMethodReflect {
public static void main(String[] args) {
Class<?> clazz = MessageAction.class; // 获取Class实例
// 如果要想使用getMethod()方法获取一个方法的实例,那么首先就一定要获取方法的参数列表
Method actionMethod = getMethodByName(clazz, "add"); // 获取指定的方法对象
System.out.println(actionMethod); // public void com.yootk.action.MessageAction.add(java.lang.String,java.lang.String)
// 此时实际上是无法清楚的获取方法的参数的名称,而能够获取的仅仅是方法参数的类型
Class<?> types [] = actionMethod.getParameterTypes(); // 获取参数的类型
for (Class<?> type : types) {
System.out.println(type);
}
}
public static Method getMethodByName(Class<?> clazz, String methodName) {
Method methods[] = clazz.getMethods();
for (Method method : methods) {
if (methodName.equals(method.getName())) { // 名称相符合
return method;
}
}
return null;
}
}
2、https://mvnrepository.com/
3、
package com.yootk.common.mvc.util;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.Map;
public class MethodParameterUtil { // 参数解析的工具类
private MethodParameterUtil() {} // 构造方法私有化
/**
* 根据名称获取指定Method类对象实例
* @param clazz 要进行处理操作的Class实例
* @param methodName 方法名称
* @return Method实例,如果不存在则返回null
*/
public static Method getMethodByName(Class<?> clazz, String methodName) {
Method methods[] = clazz.getMethods();
for (Method method : methods) {
if (methodName.equals(method.getName())) { // 名称相符合
return method;
}
}
return null;
}
/**
* 根据Method对象实例来解析方法的参数信息
* @param clazz 要解析的Class实例
* @param method Method对象实例
* @return 所有的方法参数通过Map集合保存,KEY为参数名称、VALUE为参数类型
*/
public static Map<String, Class> getMethodParameter(Class<?> clazz, Method method) {
// 参数是有顺序的,不同的参数有不同的类型,所以参数的顺序很重要
Map<String, Class> map = new LinkedHashMap<>(); // 链表实现,属于有顺序的保存
if (method == null) {
return map;
}
// 获取全部方法中的参数名称,这个时候是按照参数定义的顺序返回的数组内容
String names [] = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
Class<?> parameterTypes [] = method.getParameterTypes();
for (int x = 0; x < parameterTypes.length; x ++) {
map.put(names[x], parameterTypes[x]); // 参数名称以及参数类型的匹配
}
return map;
}
/**
* 根据指定名称的方法实现方法参数的结构解析
* @param clazz 要解析的Class实例
* @param methodName 方法名称
* @return 所有的方法参数通过Map集合保存,KEY为参数名称、VALUE为参数类型
*/
public static Map<String, Class> getMethodParameter(Class<?> clazz, String methodName) {
if (clazz == null || methodName == null || "".equals(methodName)) {
return Map.of();
}
return getMethodParameter(clazz, getMethodByName(clazz, methodName));
}
}
4、
package com.yootk.test;
import com.yootk.action.MessageAction;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.Method;
import java.util.Map;
public class TestActionMethodSpring {
public static void main(String[] args) {
Class<?> clazz = MessageAction.class; // 获取Class实例
Map<String, Class> map = MethodParameterUtil.getMethodParameter(clazz, "add");
for (Map.Entry<String, Class> entry : map.entrySet()) {
System.out.println(entry);
}
}
}
4、
package com.yootk.test;
import com.yootk.action.MessageAction;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.Method;
import java.util.Map;
public class TestActionMethodSpring {
public static void main(String[] args) {
Class<?> clazz = MessageAction.class; // 获取Class实例
Map<String, Class> map = MethodParameterUtil.getMethodParameter(clazz, "add");
for (Map.Entry<String, Class> entry : map.entrySet()) {
System.out.println(entry);
}
}
}
引入 ParameterUtil 组件
文件上传管理
在项目开发中,每一个用户的请求都会为其分配不同的 Action 处理实例不同的业务功能可能会有不同的上传文件的存储目录,也有可能会使用一个统一的目录进行存储,所以最佳的做法是可以定义一个 AbstractAction 抽象类,在该类中定义默认的上传目录,如果要是子类有不同的存储需求,也可以利用覆写的方式进行修改
1、
package com.yootk.common.action.abs;
public abstract class AbstractAction { // 所有Action的父类
public String getUploadPath() { // 是为了上传准备的
return "/upload/";
}
}
2、
package com.yootk.common.mvc.bean;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ActionUtil {
private static final String UPLOAD_METHOD_NAME = "getUploadPath"; // 固定方法名称
public static String getUpload(Object actionObject) { // 根据Action类的实例获取上传路径
String path = "/upload/"; // 设置一个默认的路径项
Method method = MethodParameterUtil.getMethodByName(actionObject.getClass(), UPLOAD_METHOD_NAME); // 根据方法名称获取方法
if (method != null) {
try {
path = (String) method.invoke(actionObject);
} catch (Exception e) {
}
}
return path;
}
}
3、
package com.yootk.common.servlet;
import com.yootk.common.mvc.util.ParameterUtil;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class ServletObject {
private static final ThreadLocal<HttpServletRequest> THREAD_REQUEST = new ThreadLocal<>();
private static final ThreadLocal<HttpServletResponse> THREAD_RESPONSE = new ThreadLocal<>();
private static final ThreadLocal<ParameterUtil> THREAD_PARAMETER = new ThreadLocal<>();
private ServletObject() {} // 构造方法私有化
public static void setParameterUtil(ParameterUtil parameterUtil) {
THREAD_PARAMETER.set(parameterUtil);
}
public static ParameterUtil getParameterUtil() {
return THREAD_PARAMETER.get();
}
public static void setRequest(HttpServletRequest request) {
THREAD_REQUEST.set(request);
}
public static void setResponse(HttpServletResponse response) {
THREAD_RESPONSE.set(response);
}
public static HttpServletRequest getRequest() {
return THREAD_REQUEST.get();
}
public static HttpServletResponse getResponse() {
return THREAD_RESPONSE.get();
}
public static HttpSession getSession() {
return THREAD_REQUEST.get().getSession();
}
public static ServletContext getApplication() {
return THREAD_REQUEST.get().getServletContext();
}
public static void clean() {
THREAD_REQUEST.remove();
THREAD_RESPONSE.remove();
THREAD_PARAMETER.remove();
}
}
接收单个请求参数
数据类型转换
每一个 Action 类中的方法如果要进行请求处理,一般都需要接收相关的请求参数,而在进行请求参数接收时,不是简单的通过 ParameterUtil 工具类进行处理,而是要根据目标的参数类型进行数据类型的转换操作,那么此时最佳的做法是定义一个 DataTypeConverterUtil 工具类进行数据类型转换的处理
1、
package com.yootk.common.mvc.util;
import com.yootk.common.servlet.ServletObject;
import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class DataTypeConverterUtil { // 数据类型转换处理
// 如果字符串要想转为日期或者是日期时间,就必须考虑到多线程并发访问下的处理情况
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final ZoneId ZONE_ID = ZoneId.systemDefault(); // 获取默认的区域
private DataTypeConverterUtil() { } // 构造方法私有化
/**
* 将指定名称的参数内容按照指定的类型进行转换,由于最终的数据转换需要通过反射传递,那么就返回Object即可
* @param paramName 请求参数名称
* @param type 目标接收的数据类型
* @return 与目标数据类型一致的数据,或者是null
*/
public static Object convert(String paramName, Class<?> type) {
try {
if (paramName == null || "".equals(paramName)) { // 没有参数名称
return null;
}
// 根据请求参数的名称实现请求参数的内容接收
String value = ServletObject.getParameterUtil().getParameter(paramName);
if (value == null || "".equals(value)) { // 没有任何的参数内容
return null;
}
if ("int".equals(type.getName()) || "java.lang.Integer".equals(type.getName())) { // 整型
if (value.matches("\\d+")) { // 匹配正则
return Integer.parseInt(value); // 字符串转为数字
}
} else if ("long".equals(type.getName()) || "java.lang.Long".equals(type.getName())) {
if (value.matches("\\d+")) { // 匹配正则
return Long.parseLong(value); // 字符串转为数字
}
} else if ("double".equals(type.getName()) || "java.lang.Double".equals(type.getName())) {
if (value.matches("\\d+(\\d.\\d+)?")) { // 匹配正则
return Double.parseDouble(value); // 字符串转为数字
}
} else if ("boolean".equals(type.getName()) || "java.lang.Boolean".equals(type.getName())) {
return Boolean.parseBoolean(value);
} else if ("java.util.Date".equals(type.getName())) {
if (value.matches("\\d{4}-\\d{2}-\\d{2}")) { // 匹配正则
LocalDate localDate = LocalDate.parse(value, DATE_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else if (value.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
LocalDate localDate = LocalDate.parse(value, DATE_TIME_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else {
return null; // 是为了后续服务的
}
} else if ("java.lang.String".equals(type.getName())) {
return value; // 字符串直接返回
}
} catch (Exception e) {} // 不需要考虑任何的异常处理
return null;
}
}
2、
package com.yootk.common.mvc.bean;
import com.yootk.common.mvc.util.DataTypeConverterUtil;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
public class ActionUtil {
private static final String UPLOAD_METHOD_NAME = "getUploadPath"; // 固定方法名称
/**
* 获取指定Action中方法的参数内容的数组集合
* @param actionObject 要进行反射调用的Action对象
* @param method 要调用的方法,这个方法可以分析出参数的结构
* @return 方法的数组内容
*/
public static Object[] getMethodParameterValue(Object actionObject, Method method) {
Object result[] = null; // 最终的返回结果
Map<String, Class> params = MethodParameterUtil.getMethodParameter(actionObject.getClass(), method);
if (params.size() == 0) { // 当前的Action方法没有参数
result = new Object[] {}; // 空数组返回
} else { // 方法上有参数
result = new Object[params.size()]; // 数组的长度就是参数的个数
int foot = 0; // 进行数组的下标控制
for (Map.Entry<String, Class> entry : params.entrySet()) { // 参数名称以及参数类型
result[foot ++] = DataTypeConverterUtil.convert(entry.getKey(), entry.getValue());
}
}
return result;
}
public static String getUpload(Object actionObject) { // 根据Action类的实例获取上传路径
String path = "/upload/"; // 设置一个默认的路径项
Method method = MethodParameterUtil.getMethodByName(actionObject.getClass(), UPLOAD_METHOD_NAME); // 根据方法名称获取方法
if (method != null) {
try {
path = (String) method.invoke(actionObject);
} catch (Exception e) {
}
}
return path;
}
}
接收 VO 对象
1、
package com.yootk.common.mvc.util;
import com.yootk.common.servlet.ServletObject;
import com.yootk.common.util.StringUtil;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class DataTypeConverterUtil { // 数据类型转换处理
// 如果字符串要想转为日期或者是日期时间,就必须考虑到多线程并发访问下的处理情况
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final ZoneId ZONE_ID = ZoneId.systemDefault(); // 获取默认的区域
private DataTypeConverterUtil() { } // 构造方法私有化
/**
* Action中方法接收的参数不再是基本类型(int、long、double、boolean、Date都属于基本类型),而是一个VO对象
* 如果是VO对象,就必须通过里面的成员属性列表获取全部的成员属性名称
* 根据这个名称再结合setXxx()方法调用来实现属性内容的设置,而这个内容就是通过用户的请求参数获取
* @param obj 当前要操作的VO对象
*/
public static void setObjectFieldValue(Object obj) { // 设置对象的属性
Field fields [] = obj.getClass().getDeclaredFields(); // 获取类中的全部成员属性
for (Field field : fields) { // 成员属性的循环
try {// 所有的属性设置的方法名称采用的方式为:setXxx(数据类型 参数名称)
Method setMethod = obj.getClass().getDeclaredMethod("set" +
StringUtil.initcap(field.getName()), field.getType());
setMethod.invoke(obj, convert(field.getName(), field.getType()));// 方法调用
} catch (Exception e) {}
}
}
/**
* 将指定名称的参数内容按照指定的类型进行转换,由于最终的数据转换需要通过反射传递,那么就返回Object即可
* @param paramName 请求参数名称
* @param type 目标接收的数据类型
* @return 与目标数据类型一致的数据,或者是null
*/
public static Object convert(String paramName, Class<?> type) {
try {
if (paramName == null || "".equals(paramName)) { // 没有参数名称
return null;
}
// 根据请求参数的名称实现请求参数的内容接收
String value = ServletObject.getParameterUtil().getParameter(paramName);
if (value == null || "".equals(value)) { // 没有任何的参数内容
return null;
}
if ("int".equals(type.getName()) || "java.lang.Integer".equals(type.getName())) { // 整型
if (value.matches("\\d+")) { // 匹配正则
return Integer.parseInt(value); // 字符串转为数字
}
} else if ("long".equals(type.getName()) || "java.lang.Long".equals(type.getName())) {
if (value.matches("\\d+")) { // 匹配正则
return Long.parseLong(value); // 字符串转为数字
}
} else if ("double".equals(type.getName()) || "java.lang.Double".equals(type.getName())) {
if (value.matches("\\d+(\\d.\\d+)?")) { // 匹配正则
return Double.parseDouble(value); // 字符串转为数字
}
} else if ("boolean".equals(type.getName()) || "java.lang.Boolean".equals(type.getName())) {
return Boolean.parseBoolean(value);
} else if ("java.util.Date".equals(type.getName())) {
if (value.matches("\\d{4}-\\d{2}-\\d{2}")) { // 匹配正则
LocalDate localDate = LocalDate.parse(value, DATE_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else if (value.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
LocalDate localDate = LocalDate.parse(value, DATE_TIME_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else {
return null; // 是为了后续服务的
}
} else if ("java.lang.String".equals(type.getName())) {
return value; // 字符串直接返回
}
} catch (Exception e) {} // 不需要考虑任何的异常处理
return null;
}
}
2、
package com.yootk.common.mvc.bean;
import com.yootk.common.mvc.util.DataTypeConverterUtil;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.Method;
import java.util.Map;
public class ActionUtil {
private static final String UPLOAD_METHOD_NAME = "getUploadPath"; // 固定方法名称
/**
* 获取指定Action中方法的参数内容的数组集合
* @param actionObject 要进行反射调用的Action对象
* @param method 要调用的方法,这个方法可以分析出参数的结构
* @return 方法的数组内容
*/
public static Object[] getMethodParameterValue(Object actionObject, Method method) {
Object result[] = null; // 最终的返回结果
Map<String, Class> params = MethodParameterUtil.getMethodParameter(actionObject.getClass(), method);
if (params.size() == 0) { // 当前的Action方法没有参数
result = new Object[] {}; // 空数组返回
} else { // 方法上有参数
result = new Object[params.size()]; // 数组的长度就是参数的个数
int foot = 0; // 进行数组的下标控制
for (Map.Entry<String, Class> entry : params.entrySet()) { // 参数名称以及参数类型
if (isBasic(entry.getValue())) { // 当前所传递的是一个普通类型
result[foot++] = DataTypeConverterUtil.convert(entry.getKey(), entry.getValue());
} else {
Object vo = null; // 当前操作的类型是一个VO实例
try {
vo = entry.getValue().getDeclaredConstructor().newInstance(); // 反射实例化对象
DataTypeConverterUtil.setObjectFieldValue(vo); // 设置对象中的属性信息
} catch (Exception e) {}
result[foot ++] = vo;
}
}
}
return result;
}
private static boolean isBasic(Class<?> type) { // 判断当前操作的类型是否为普通类型
return "long".equals(type.getName()) ||
"java.lang.Long".equals(type.getName())||
"int".equals(type.getName()) ||
"java.lang.Integer".equals(type.getName()) ||
"double".equals(type.getName()) ||
"java.lang.Double".equals(type.getName()) ||
"boolean".equals(type.getName()) ||
"java.lang.Boolean".equals(type.getName()) ||
"java.lang.String".equals(type.getName()) ||
"java.util.Date".equals(type.getName());
}
public static String getUpload(Object actionObject) { // 根据Action类的实例获取上传路径
String path = "/upload/"; // 设置一个默认的路径项
Method method = MethodParameterUtil.getMethodByName(actionObject.getClass(), UPLOAD_METHOD_NAME); // 根据方法名称获取方法
if (method != null) {
try {
path = (String) method.invoke(actionObject);
} catch (Exception e) {
}
}
return path;
}
}
接收数组参数
请求参数与数组转换
在进行请求参数提交时,有可能会同时传递若干个名称相同的参数这样在接收时就需要通过数组的形式进行处理,同时在 Action 类方法中也应该将所有的请求参数转为指定类型的数组结构
1、
package com.yootk.common.mvc.util;
import com.yootk.common.servlet.ServletObject;
import com.yootk.common.util.StringUtil;
import jakarta.servlet.Servlet;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.Date;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class DataTypeConverterUtil { // 数据类型转换处理
// 如果字符串要想转为日期或者是日期时间,就必须考虑到多线程并发访问下的处理情况
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final ZoneId ZONE_ID = ZoneId.systemDefault(); // 获取默认的区域
private DataTypeConverterUtil() { } // 构造方法私有化
/**
* 将接收到的请求数组参数的内容(字符串数组)转为特定的数据类型的数组(int[]、double[]、...)
* @param paramName
* @param type
* @return
*/
public static Object convertArray(String paramName, Class<?> type) {
String values[] = ServletObject.getParameterUtil().getParameterValues(paramName); // 根据请求参数的名称获取数组内容
if (values == null || values.length == 0) { // 没有数组内容
return null; // 直接返回一个null的数据信息
} else { // 当前数组存在有内容,当前只考虑几种特定的数组
if ("int[]".equals(type.getSimpleName())) { // 当前是否为整形数组
int array[] = new int[values.length]; // 开辟一个新的数组
for (int x = 0; x < array.length; x ++) { // 实现数组的循环
if (values[x].matches("\\d+")) { // 符合正则要求
array[x] = Integer.parseInt(values[x]); // 数据转换
}
}
return array;
} else if ("Integer[]".equals(type.getSimpleName())) {
Integer array[] = new Integer[values.length]; // 开辟一个新的数组
for (int x = 0; x < array.length; x ++) { // 实现数组的循环
if (values[x].matches("\\d+")) { // 符合正则要求
array[x] = Integer.parseInt(values[x]); // 数据转换
}
}
return array;
} else if ("long[]".equals(type.getSimpleName())) {
long array[] = new long[values.length]; // 开辟一个新的数组
for (int x = 0; x < array.length; x ++) { // 实现数组的循环
if (values[x].matches("\\d+")) { // 符合正则要求
array[x] = Long.parseLong(values[x]); // 数据转换
}
}
return array;
} else if ("Long[]".equals(type.getSimpleName())) {
Long array[] = new Long[values.length]; // 开辟一个新的数组
for (int x = 0; x < array.length; x ++) { // 实现数组的循环
if (values[x].matches("\\d+")) { // 符合正则要求
array[x] = Long.parseLong(values[x]); // 数据转换
}
}
return array;
} else if ("String[]".equals(type.getSimpleName())){
return values;
}
}
return null; // 没有满足的判断返回null
}
/**
* Action中方法接收的参数不再是基本类型(int、long、double、boolean、Date都属于基本类型),而是一个VO对象
* 如果是VO对象,就必须通过里面的成员属性列表获取全部的成员属性名称
* 根据这个名称再结合setXxx()方法调用来实现属性内容的设置,而这个内容就是通过用户的请求参数获取
* @param obj 当前要操作的VO对象
*/
public static void setObjectFieldValue(Object obj) { // 设置对象的属性
Field fields [] = obj.getClass().getDeclaredFields(); // 获取类中的全部成员属性
for (Field field : fields) { // 成员属性的循环
try {// 所有的属性设置的方法名称采用的方式为:setXxx(数据类型 参数名称)
Method setMethod = obj.getClass().getDeclaredMethod("set" +
StringUtil.initcap(field.getName()), field.getType());
setMethod.invoke(obj, convert(field.getName(), field.getType()));// 方法调用
} catch (Exception e) {}
}
}
/**
* 将指定名称的参数内容按照指定的类型进行转换,由于最终的数据转换需要通过反射传递,那么就返回Object即可
* @param paramName 请求参数名称
* @param type 目标接收的数据类型
* @return 与目标数据类型一致的数据,或者是null
*/
public static Object convert(String paramName, Class<?> type) {
try {
if (paramName == null || "".equals(paramName)) { // 没有参数名称
return null;
}
// 根据请求参数的名称实现请求参数的内容接收
String value = ServletObject.getParameterUtil().getParameter(paramName);
if (value == null || "".equals(value)) { // 没有任何的参数内容
return null;
}
if ("int".equals(type.getName()) || "java.lang.Integer".equals(type.getName())) { // 整型
if (value.matches("\\d+")) { // 匹配正则
return Integer.parseInt(value); // 字符串转为数字
}
} else if ("long".equals(type.getName()) || "java.lang.Long".equals(type.getName())) {
if (value.matches("\\d+")) { // 匹配正则
return Long.parseLong(value); // 字符串转为数字
}
} else if ("double".equals(type.getName()) || "java.lang.Double".equals(type.getName())) {
if (value.matches("\\d+(\\d.\\d+)?")) { // 匹配正则
return Double.parseDouble(value); // 字符串转为数字
}
} else if ("boolean".equals(type.getName()) || "java.lang.Boolean".equals(type.getName())) {
return Boolean.parseBoolean(value);
} else if ("java.util.Date".equals(type.getName())) {
if (value.matches("\\d{4}-\\d{2}-\\d{2}")) { // 匹配正则
LocalDate localDate = LocalDate.parse(value, DATE_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else if (value.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
LocalDate localDate = LocalDate.parse(value, DATE_TIME_FORMATTER);
Instant instant = localDate.atStartOfDay().atZone(ZONE_ID).toInstant();
return Date.from(instant);
} else {
return null; // 是为了后续服务的
}
} else if ("java.lang.String".equals(type.getName())) {
return value; // 字符串直接返回
}
} catch (Exception e) {} // 不需要考虑任何的异常处理
return null;
}
}
2、
package com.yootk.common.mvc.bean;
import com.yootk.common.mvc.util.DataTypeConverterUtil;
import com.yootk.common.mvc.util.MethodParameterUtil;
import java.lang.reflect.Method;
import java.util.Map;
public class ActionUtil {
private static final String UPLOAD_METHOD_NAME = "getUploadPath"; // 固定方法名称
/**
* 获取指定Action中方法的参数内容的数组集合
* @param actionObject 要进行反射调用的Action对象
* @param method 要调用的方法,这个方法可以分析出参数的结构
* @return 方法的数组内容
*/
public static Object[] getMethodParameterValue(Object actionObject, Method method) {
Object result[] = null; // 最终的返回结果
Map<String, Class> params = MethodParameterUtil.getMethodParameter(actionObject.getClass(), method);
if (params.size() == 0) { // 当前的Action方法没有参数
result = new Object[] {}; // 空数组返回
} else { // 方法上有参数
result = new Object[params.size()]; // 数组的长度就是参数的个数
int foot = 0; // 进行数组的下标控制
for (Map.Entry<String, Class> entry : params.entrySet()) { // 参数名称以及参数类型
if (isBasic(entry.getValue())) { // 当前所传递的是一个普通类型
result[foot++] = DataTypeConverterUtil.convert(entry.getKey(), entry.getValue());
} else if (isArray(entry.getValue())) { // 判断是否为数组类型
result[foot ++] = DataTypeConverterUtil.convertArray(entry.getKey(), entry.getValue());
} else {
Object vo = null; // 当前操作的类型是一个VO实例
try {
vo = entry.getValue().getDeclaredConstructor().newInstance(); // 反射实例化对象
DataTypeConverterUtil.setObjectFieldValue(vo); // 设置对象中的属性信息
} catch (Exception e) {}
result[foot ++] = vo;
}
}
}
return result;
}
private static boolean isBasic(Class<?> type) { // 判断当前操作的类型是否为普通类型
return "long".equals(type.getName()) ||
"java.lang.Long".equals(type.getName())||
"int".equals(type.getName()) ||
"java.lang.Integer".equals(type.getName()) ||
"double".equals(type.getName()) ||
"java.lang.Double".equals(type.getName()) ||
"boolean".equals(type.getName()) ||
"java.lang.Boolean".equals(type.getName()) ||
"java.lang.String".equals(type.getName()) ||
"java.util.Date".equals(type.getName());
}
private static boolean isArray(Class<?> type) { // 判断当前的类型是否为数组
return "int[]".equals(type.getTypeName()) ||
"long[]".equals(type.getTypeName()) ||
"java.lang.String[]".equals(type.getTypeName()) ||
"java.lang.Integer[]".equals(type.getTypeName()) ||
"java.lang.Long[]".equals(type.getTypeName());
}
public static String getUpload(Object actionObject) { // 根据Action类的实例获取上传路径
String path = "/upload/"; // 设置一个默认的路径项
Method method = MethodParameterUtil.getMethodByName(actionObject.getClass(), UPLOAD_METHOD_NAME); // 根据方法名称获取方法
if (method != null) {
try {
path = (String) method.invoke(actionObject);
} catch (Exception e) {
}
}
return path;
}
}
3、
package com.yootk.action; // 独立的程序开发
import com.yootk.common.mvc.annotation.Autowired;
import com.yootk.common.mvc.annotation.Controller;
import com.yootk.common.mvc.annotation.RequestMapping;
import com.yootk.common.servlet.ModelAndView;
import com.yootk.common.servlet.ServletObject;
import com.yootk.service.IMessageService;
import com.yootk.vo.Message;
import com.yootk.common.action.abs.*;
import java.util.Arrays;
@Controller // 表示当前的类是一个控制器处理类
@RequestMapping("/pages/message/") // 映射父路径
public class MessageAction extends AbstractAction { // 是一个独立的类
@Autowired // 如果每次都写上名字,实际上会过于繁琐,于是支持根据类型的注入
private IMessageService messageService;
@RequestMapping("add_pre")
public String addPre() {
return "/pages/front/message/message_add.jsp"; // 返回跳转的路径
}
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add(Message msg, String[] url) { // 请求参数的title和content可以自动赋值
System.out.println("【MessageAction.add()】增加新的消息内容。");
System.out.println(msg);
System.out.println("【复选框数组】" + Arrays.toString(url));
}
@RequestMapping("list") // 真实访问路径:/pages/message/list.action
public String list() { // 跳转路径的配置
System.out.println("【MessageAction.list()】消息内容列表。");
try {
System.out.println(this.messageService.list());
} catch (Exception e) {}
return "/pages/front/message/message_list.jsp"; // 设置返回路径
}
@RequestMapping("remove") // 真实访问路径:/pages/message/remove.action
public void remove() throws Exception {
ServletObject.getResponse().getWriter().print("【MessageAction.remove()】删除消息内容。");
}
@RequestMapping("edit") // 真实访问路径:/pages/message/edit.action
public ModelAndView edit() {
System.out.println("【MessageAction.edit()】编辑消息内容。");
Message msg = new Message();
msg.setTitle("沐言科技");
msg.setContent("www.yootk.com");
ModelAndView mav = new ModelAndView("/pages/front/message/message_edit.jsp", "message", msg);
return mav;
}
}
服务端验证
服务端验证实现流程
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>com.yootk.common.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>validationBaseName</param-name>
<param-value>com.yootk.resources.Validation</param-value>
</init-param>
<init-param>
<param-name>errorPageBaseName</param-name>
<param-value>com.yootk.resources.ErrorPage</param-value>
</init-param>
<init-param>
<param-name>messageBaseName</param-name>
<param-value>com.yootk.resources.Message</param-value>
</init-param>
<init-param>
<param-name>500</param-name>
<param-value>/pages/error/httpcode-500.jsp</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
验证规则
package com.yootk.common.validate;
import com.yootk.common.servlet.ServletObject;
public enum ValidateRegular { // 定义验证处理
STRING { // 该内容不允许为空(null和“""”都不允许)
@Override
public boolean check(String... value) { // 方法覆写
if (value == null || value.length == 0) {
return false; // 验证不通过
}
if (value.length != 1) { // 单内容判断
return false;
}
if (value[0] == null || "".equals(value[0])) {
return false;
}
return true; // 验证通过
}
},
INT { // 该内容必须是一个整数
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return value[0].matches("\\d+"); // 由数字所组成
}
return false;
}
},
LONG { // 该内容必须是一个整数
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return value[0].matches("\\d+"); // 由数字所组成
}
return false;
}
},
DOUBLE { // 该内容必须是一个小数
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return value[0].matches("\\d+(\\.\\d+)?"); // 由数字所组成
}
return false;
}
},
BOOLEAN {
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return "true".equals(value[0]) || "false".equals(value[0]);
}
return false;
}
},
DATE { // 该内容必须是一个日期(yyyy-MM-dd)
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return value[0].matches("\\d{4}-\\d{2}-\\d{2}"); // 由数字所组成
}
return false;
}
},
DATETIME { // 该内容必须是一个日期时间(yyyy-MM-dd HH:mm:ss)
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
return value[0].matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}"); // 由数字所组成
}
return false;
}
},
RAND { // 该内容需要进行验证码的匹配
@Override
public boolean check(String... value) {
if (ValidateRegular.STRING.check(value)) { // 数据不为空
String rand = (String) ServletObject.getSession().getAttribute("rand");
if (ValidateRegular.STRING.check(rand)) {
return rand.equalsIgnoreCase(value[0]);
}
}
return false;
}
},
INT_ARRAY { // 传递的是一个整型数组
@Override
public boolean check(String... value) {
if (value == null || value.length == 0) {
return false;
}
for (String str : value) { // 数据的循环判断
if (str == null || "".equals(str)) {
return false;
} else {
if (!str.matches("\\d+")) { // 不是数字组成
return false;
}
}
}
return true;
}
},
LONG_ARRAY { // 传递的是一个整型数组
@Override
public boolean check(String... value) {
if (value == null || value.length == 0) {
return false;
}
for (String str : value) { // 数据的循环判断
if (str == null || "".equals(str)) {
return false;
} else {
if (!str.matches("\\d+")) { // 不是数字组成
return false;
}
}
}
return true;
}
},
STRING_ARRAY { // 传递的是一个字符串数组(数组中的每一个内容都不允许为空)
@Override
public boolean check(String... value) {
if (value == null || value.length == 0) {
return false;
}
for (String str : value) { // 数据的循环判断
if (str == null || "".equals(str)) {
return false;
}
}
return true;
}
};
/**
* 定义一个公共的抽象方法,实现具体的数据检查,因为所有传入的数据类型都是字符串
* request.getParameter()方法可以返回的类型就是字符串
* @param value 要验证的数据内容
* @return 验证通过返回true,否则返回false
*/
public abstract boolean check(String ... value);
}
数据验证
1、
com.yootk.action.MessageAction.add=title:string|content:string|url:string[]
2、
validation.error.string=输入的内容不允许为空!
validation.error.int=输入的数据必须是整数!
validation.error.long=输入的数据必须是整数!
validation.error.double=输入的数据必须是小数!
validation.error.boolean=输入的数据必须是“true”或者是“false”!
validation.error.date=输入的数据必须是日期型格式(yyyy-MM-dd)!
validation.error.datetime=输入的数据必须是日期时间型格式(yyyy-MM-dd HH:mm:ss)!
validation.error.rand=输入的验证码错误!
validation.error.int[]=数组的内容必须是数字!
validation.error.long[]=输入的内容必须是数字!
validation.error.string[]=输入的内容不允许为空!
3、
package com.yootk.common.mvc.util;
import java.util.ResourceBundle;
public class ResourceUtil { // 资源读取
private static ResourceBundle resource; // 资源读取类
private ResourceUtil() {}
public static void setMessageBaseName(String baseName) {
resource = ResourceBundle.getBundle(baseName); // 获取资源对象
}
public static String getString(String key) {
try {
return resource.getString(key);
} catch (Exception e) {
return null;
}
}
}
4、
package com.yootk.common.validate;
import com.yootk.common.mvc.bean.ControllerRequestMapping;
import com.yootk.common.mvc.util.ResourceUtil;
import com.yootk.common.servlet.ServletObject;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
public class ValidationRuleUtil { // 验证规则类
/**
* 将读取到的验证规则进行拆分操作,同时调用具体的验证处理方法进行验证
* @param rule 全部的验证规则
* @return 所有的错误信息,KEY为参数名称、VALUE为错误信息(Message.properties定义的)
*/
private static Map<String, String> validate(String rule) {
// rule:title:string|content:string|url:string[]
Map<String, String> errors = new HashMap<>();
String ruleResults [] = rule.split("\\|"); // 数据拆分
for (int x = 0; x < ruleResults.length; x ++) { // 规则循环
String temp [] = ruleResults[x].split(":"); // 第一个内容为请求参数,第二个为规则
if (!check(temp[1], temp[0])) { // 验证失败
// 此时Map集合之中的KEY的内容为参数名称,而VALUE的内容为错误信息,而错误信息在Message.properties定义
// 此时在进行资源获取的时候ResourceUtil之中的资源项是通过外部设置的
errors.put(temp[0], ResourceUtil.getString("validation.error." + temp[1]));
}
}
return errors;
}
public static String getValidateRule(String baseName, ControllerRequestMapping crm) {
if (baseName == null) { // 没有读取资源的名称
return null;
}
String rule = null; // 保存读取后的规则信息
try {
ResourceBundle bundle = ResourceBundle.getBundle(baseName);
String validationKey = crm.getActionClazz().getName() + "." + crm.getActionMethod().getName(); // 验证规则KEY
rule = bundle.getString(validationKey); // 获取规则
} catch (Exception e) {}
return rule;
}
/**
* 根据当前传入的验证的规则进行请求参数的结构验证
* @param regular 验证规则
* @param paramName 请求参数的名称
* @return 验证通过返回true,否则返回false
*/
private static boolean check(String regular, String paramName) {
// 本次在进行数据验证的时候,全部的类型使用的都是数组
String value [] = ServletObject.getParameterUtil().getParameterValues(paramName);
switch (regular) { // 规则配置
case "string": // 字符串的规则
return ValidateRegular.STRING.check(value);
case "int":
return ValidateRegular.INT.check(value);
case "long":
return ValidateRegular.LONG.check(value);
case "double":
return ValidateRegular.DOUBLE.check(value);
case "boolean":
return ValidateRegular.BOOLEAN.check(value);
case "date":
return ValidateRegular.DATE.check(value);
case "datetime":
return ValidateRegular.DATETIME.check(value);
case "rand":
return ValidateRegular.RAND.check(value);
case "int[]":
return ValidateRegular.INT_ARRAY.check(value);
case "long[]":
return ValidateRegular.LONG_ARRAY.check(value);
case "string[]":
return ValidateRegular.STRING_ARRAY.check(value);
}
return false;
}
}
错误页
1、
global.error.page=/pages/error/httpcode-500.jsp
com.yootk.action.MessageAction.add=/pages/message/add_pre.action
文件上传验证
1、
/**
* 对已经上传的文件(保存在临时目录之中的文件)进行MIME类型检测
* @param paramName 上传文件的参数名称
* @param mimeTypes 所有允许你使用的MIME类型
* @return 如果类型相符合返回true,否则返回false
*/
public boolean uploadMimeCheck(String paramName, List<String> mimeTypes) {
// 所有的表单的提交信息都保存在了ParamMap集合里面,所以要进行文件的验证之前需要进行参数是否存在的判断
if (this.map.containsKey(paramName)) { // 当前的参数存在
List<FileItem> items = this.map.get(paramName); // 获取上传文件
for (FileItem item : items) {
if (item.getSize() > 0) { // 当前存在有上传文件
if (!mimeTypes.contains(item.getContentType())) { // 当前的文件类型不匹配
return false;// 文件属于非法状态
}
}
}
}
return true; // 不需要进行文件的验证,直接返回true
}
2、
validation.error.image=只允许上传图片!
3、
IMAGE {
@Override
public boolean check(String... value) {
// 如果在后续设计之中,你需要进行此类的MIME信息扩充,可以通过配置文件定义
List<String> mimeTypes = List.of("image/bmp", "image/png", "image/jpg", "image/jpeg");
if (value == null) {
return true;
}
return ServletObject.getParameterUtil().uploadMimeCheck(value[0], mimeTypes);
}
};
4、
private static boolean check(String regular, String paramName) {
// 本次在进行数据验证的时候,全部的类型使用的都是数组
String value [] = ServletObject.getParameterUtil().getParameterValues(paramName);
switch (regular) { // 规则配置
case "string": // 字符串的规则
return ValidateRegular.STRING.check(value);
case "int":
return ValidateRegular.INT.check(value);
case "long":
return ValidateRegular.LONG.check(value);
case "double":
return ValidateRegular.DOUBLE.check(value);
case "boolean":
return ValidateRegular.BOOLEAN.check(value);
case "date":
return ValidateRegular.DATE.check(value);
case "datetime":
return ValidateRegular.DATETIME.check(value);
case "rand":
return ValidateRegular.RAND.check(value);
case "int[]":
return ValidateRegular.INT_ARRAY.check(value);
case "long[]":
return ValidateRegular.LONG_ARRAY.check(value);
case "string[]":
return ValidateRegular.STRING_ARRAY.check(value);
case "image":
return ValidateRegular.IMAGE.check(paramName);
}
return false;
}
5、
<div class="form-group" id="photoDiv">
<label class="col-md-2 control-label" for="title">消息图片:</label>
<div class="col-md-5">
<input type="file" name="photo" id="photo" class="form-control input-sm" placeholder="请选择一个要上传的图片...">
</div>
<div class="col-md-4" id="photoMsg">${errors['photo']}</div>
</div>
multipartFile
1、
package com.yootk.common.servlet;
import java.io.File;
public class MultipartFile extends File { // 自定义的文件类
private String contentType; // 文件的MIME类型
private String originFileNamel; // 文件原始名称
public MultipartFile(String path) {
super(path);
}
public MultipartFile(File parent, String child) {
super(parent, child);
}
public String getContentType() {
return contentType;
}
public void setContentType(String contentType) {
this.contentType = contentType;
}
public String getOriginFileNamel() {
return originFileNamel;
}
public void setOriginFileNamel(String originFileNamel) {
this.originFileNamel = originFileNamel;
}
}
保存上传文件
1、
@RequestMapping("add") // 真实访问路径:/pages/message/add.action
public void add(Message msg, String[] url, MultipartFile photo) { // 请求参数的title和content可以自动赋值
System.out.println("【MessageAction.add()】增加新的消息内容。");
System.out.println(msg);
System.out.println("【复选框数组】" + Arrays.toString(url));
System.out.println(photo); // 输出上传文件信息
}
2、
else if ("com.yootk.common.servlet.MultipartFile".equals(type.getName())) {
return ServletObject.getParameterUtil().getAllUploadFile().get(paramName).get(0);
}
3、
else if ("MultipartFile[]".equals(type.getSimpleName())) { // 上传文件
return ServletObject.getParameterUtil().getAllUploadFile().get(paramName).toArray(new MultipartFile[]{});
}
4、
private static boolean isBasic(Class<?> type) { // 判断当前操作的类型是否为普通类型
return "long".equals(type.getName()) ||
"java.lang.Long".equals(type.getName())||
"int".equals(type.getName()) ||
"java.lang.Integer".equals(type.getName()) ||
"double".equals(type.getName()) ||
"java.lang.Double".equals(type.getName()) ||
"boolean".equals(type.getName()) ||
"java.lang.Boolean".equals(type.getName()) ||
"java.lang.String".equals(type.getName()) ||
"java.util.Date".equals(type.getName()) ||
"com.yootk.common.servlet.MultipartFile".equals(type.getName());
}
private static boolean isArray(Class<?> type) { // 判断当前的类型是否为数组
return "int[]".equals(type.getTypeName()) ||
"long[]".equals(type.getTypeName()) ||
"java.lang.String[]".equals(type.getTypeName()) ||
"java.lang.Integer[]".equals(type.getTypeName()) ||
"java.lang.Long[]".equals(type.getTypeName()) ||
"com.yootk.common.servlet.MultipartFile[]".equals(type.getTypeName());
}
数据分页
数据分页简介
数据加载处理
为了便于项目中的数据管理,一般都需要进行数据列表的展示而如果开发者只是简单的向数据库发出一个数据查询指令,这样当数据表存储的数据量过大时就会占用大量的程序内存,从而导致程序的响应性能降低
所以此时就需要进行数据的批量加载,根据需要每次只加载一部分的数据信息,实际上你们现在所看见的 1、2、3、4 页,就属于这种分页操作的实现了。但是在早期的时候由于大部分的项目数据量不大,那么对于数据的分页显示就有了两种实现方案:
【假分页】基于算法的分页:将所有的数据全部都加载到内存之中,而后只输出部分的数据信息,占用的内存较大;
【真分页】基于数据库的分页:在数据库的层次上进行分页的实现,每次只加载一部分;
对于现阶段的设计开发,分页的操作基本上都是围绕着数据库实现的,也就是说通过 SQL 语句实现分页的处理操作,这样一来就会存在有一个问题,不同的数据库是存在有不同的分页语法的,程序就会出现可移植性变差的问题。
1、
DROP DATABASE IF EXISTS yootk ;
CREATE DATABASE yootk CHARACTER SET UTF8 ;
USE yootk ;
CREATE TABLE message (
id BIGINT AUTO_INCREMENT,
title VARCHAR(50),
content TEXT,
CONSTRAINT pk_id PRIMARY KEY(id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、
package com.yootk.common.dao.abs;
import com.yootk.common.mvc.util.DatabaseConnection;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public abstract class AbstractDAO { // 这个类是一个不完整的类
protected PreparedStatement pstmt; // 作为一个公共属性存在
protected Connection connection; // 作为一个公共属性存在
// 按照面向对象的设计原则,所有的子类在进行对象实例化时都一定要调用父类构造
public AbstractDAO() { // 无参构造方法
this.connection = DatabaseConnection.getConnection(); // 获取数据库连接
}
public Long countHandle(String tableName) throws SQLException {
String sql = "SELECT COUNT(*) FROM " + tableName;
this.pstmt = this.connection.prepareStatement(sql);
ResultSet rs = this.pstmt.executeQuery();
if (rs.next()) {
return rs.getLong(1);
}
return 0L;
}
public Long countHandle(String tableName, String column, String keyword) throws SQLException {
String sql = "SELECT COUNT(*) FROM " + tableName + " WHERE " + column + " LIKE ?";
this.pstmt = this.connection.prepareStatement(sql);
this.pstmt.setString(1, "%" + keyword + "%");
ResultSet rs = this.pstmt.executeQuery();
if (rs.next()) {
return rs.getLong(1);
}
return 0L;
}
}
3、
package com.yootk.dao.impl;
import com.yootk.common.dao.abs.AbstractDAO;
import com.yootk.common.mvc.annotation.Repository;
import com.yootk.dao.IMessageDAO;
import com.yootk.vo.Message;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Repository // 此为数据层
public class MessageDAOImpl extends AbstractDAO implements IMessageDAO {
// 本次只考虑数据的列表操作,而其他的数据层方法并没有使用,所以方法体全部为空
@Override
public boolean doCreate(Message message) throws SQLException {
String sql = "INSERT INTO message (title, content) VALUES (?, ?)";
super.pstmt = super.connection.prepareStatement(sql);
super.pstmt.setString(1, message.getTitle());
super.pstmt.setString(2, message.getContent());
return super.pstmt.executeUpdate() > 0;
}
@Override
public boolean doEdit(Message message) throws SQLException {
return false;
}
@Override
public boolean doRemove(Set<Long> longs) throws SQLException {
return false;
}
@Override
public Message findById(Long aLong) throws SQLException {
return null;
}
@Override
public List<Message> findAll() throws SQLException {
List<Message> messageList = new ArrayList<>();
String sql = "SELECT id, title, content FROM message";
super.pstmt = super.connection.prepareStatement(sql);
ResultSet rs = super.pstmt.executeQuery();
while (rs.next()) {
Message msg = new Message();
msg.setId(rs.getLong(1));
msg.setTitle(rs.getString(2));
msg.setContent(rs.getString(3));
messageList.add(msg);
}
return messageList;
}
@Override
public List<Message> findSplit(Integer currentPage, Integer lineSize) throws SQLException {
List<Message> messageList = new ArrayList<>();
String sql = "SELECT id, title, content FROM message LIMIT ?,?";
super.pstmt = super.connection.prepareStatement(sql);
super.pstmt.setInt(1, (currentPage - 1) * lineSize);
super.pstmt.setInt(2, lineSize);
ResultSet rs = super.pstmt.executeQuery();
while (rs.next()) {
Message msg = new Message();
msg.setId(rs.getLong(1));
msg.setTitle(rs.getString(2));
msg.setContent(rs.getString(3));
messageList.add(msg);
}
return messageList;
}
@Override
public List<Message> findSplit(Integer currentPage, Integer lineSize, String column, String keyword) throws SQLException {
List<Message> messageList = new ArrayList<>();
String sql = "SELECT id, title, content FROM message WHERE " + column + " LIKE ? LIMIT ?,?";
super.pstmt = super.connection.prepareStatement(sql);
super.pstmt.setString(1, "%" + keyword + "%");
super.pstmt.setInt(2, (currentPage - 1) * lineSize);
super.pstmt.setInt(3, lineSize);
ResultSet rs = super.pstmt.executeQuery();
while (rs.next()) {
Message msg = new Message();
msg.setId(rs.getLong(1));
msg.setTitle(rs.getString(2));
msg.setContent(rs.getString(3));
messageList.add(msg);
}
return messageList;
}
@Override
public Long getAllCount() throws SQLException {
return super.countHandle("message");
}
@Override
public Long getAllCount(String column, String keyword) throws SQLException {
return super.countHandle("message", column, keyword);
}
}
4、
package com.yootk.service;
import com.yootk.vo.Message;
import java.util.List;
import java.util.Map;
public interface IMessageService {
public List<Message> list() throws Exception;
public boolean addBatch(List<Message> messages) throws Exception; // 批量增加
/**
* 分页数据查询处理
* @param currentPage 当前所在页码
* @param lineSize 每页显示的数据行
* @param column 模糊查询数据列
* @param keyword 模糊查询内容
* @return 返回的内容包含有统计数据以及List集合,所以对于Map集合之中的内容包含有两类信息:
* 1、key = allMessages、value = List集合,全部的Message数据
* 2、key = allRecorders、value = message数据表的行数统计
* @throws Exception
*/
public Map<String, Object> split(int currentPage, int lineSize, String column, String keyword) throws Exception; // 分页查询
}
5、
package com.yix.service.impl;
import com.yix.common.mvc.annotation.Aspect;
import com.yix.common.mvc.annotation.Autowired;
import com.yix.common.mvc.annotation.Service;
import com.yix.common.service.abs.AbstractService;
import com.yix.dao.IMessageDAO;
import com.yix.service.IMessageService;
import com.yix.vo.Message;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author wangdx
*/
@Service
@Aspect
public class MessageServiceImpl extends AbstractService implements IMessageService {
@Autowired(name = "messageDAOImpl")
private IMessageDAO messageDAO;
@Override
public List<Message> list() throws Exception {
return this.messageDAO.findAll();
}
@Override
public boolean addBatch(List<Message> messages) throws Exception {
for (Message message : messages) {
this.messageDAO.doCreate(message);
}
return true;
}
@Override
public Map<String, Object> split(int currentPage, int lineSize, String column, String keyword) throws Exception {
Map<String, Object> re = new HashMap<>();
if (super.checkEmpty(column, keyword)) {
re.put("allMessages", this.messageDAO.findSplit(currentPage, lineSize, column, keyword));
re.put("allRecorders", this.messageDAO.getAllCount(column, keyword));
} else {
re.put("allMessages", this.messageDAO.findSplit(currentPage, lineSize));
re.put("allRecorders", this.messageDAO.getAllCount());
}
return re;
}
}
分页基础实现
传递分页参数
数据库的分页处理机制需要四个核心参数的控制:currentPage(当前所在页码)、lineSize(每页显示长度)column(模糊查询列)、keyword(模糊查询关键字)
1、
@RequestMapping("split")
public ModelAndView split() {
ModelAndView mav = null;
try {
mav = new ModelAndView("/pages/front/message/message_list.jsp",
this.messageService.split(1, 20, null, null));
} catch (Exception e) {
e.printStackTrace();
}
return mav; // 返回跳转的路径
}
2、
<table class="table table-hover table-striped">
<thead><th>
<tr><td>消息ID</td><td>标题</td><td>内容</td></tr>
</th></thead>
<tbody>
<c:forEach items="${allMessages}" var="message">
<tr><td>${message.id}</td><td>${message.title}</td><td>${message.content}</td></tr>
</c:forEach>
</tbody>
</table>
3、
@RequestMapping("split")
public ModelAndView split(int cp, int ls, String col, String kw) {
ModelAndView mav = null;
try {
mav = new ModelAndView("/pages/front/message/message_list.jsp",
this.messageService.split(cp, ls, col, kw));
} catch (Exception e) {
e.printStackTrace();
}
return mav; // 返回跳转的路径
}
4、
http://localhost/pages/message/split.action?cp=2&ls=10&col=&kw=
5、
http://localhost/pages/message/split.action
6、
http://localhost/pages/message/split.action?cp=a&ls=6cc&col=&kw=
7、
package com.yootk.common.util;
import com.yootk.common.servlet.ServletObject;
public class PageUtil { // 分页工具类
private Integer currentPage = 1; // 默认当前页
private Integer lineSize = 10; // 默认每页显示的数据行数
private String column; // 模糊查询列
private String keyword; // 模糊查询关键字
private String columnData; // column候选参数配置
private String url; // 分页路径
public PageUtil(String url) { // 下一次的加载路径
this(url, null);
}
public PageUtil(String url, String columnData) {
this.url = url;
this.columnData = columnData;
this.splitHandle(); // 进行分页参数的控制
}
// 分页参数有可能没有传递,有可能传递是错误的,那么应该设置有默认值
private void splitHandle() { // 定义分页的处理方法
try { // 如果出现了错误,那么当前页就是第一页
this.currentPage = Integer.parseInt(ServletObject.getParameterUtil().getParameter("cp"));
} catch (Exception e) {}
try { // 如果出现了错误,使用默认的每页行数长度
this.lineSize = Integer.parseInt(ServletObject.getParameterUtil().getParameter("ls"));
} catch (Exception e) {}
// 在业务层之中对这两个参数已经进行了判断,而这个判断在处理的时候就直接以null的形式完成
this.column = ServletObject.getParameterUtil().getParameter("col");
this.keyword = ServletObject.getParameterUtil().getParameter("kw");
// 考虑到后续的应用还有可能要继续使用分页操作,所以要将这部分的信息继续向后续的传递
ServletObject.getRequest().setAttribute("currentPage", this.currentPage);
ServletObject.getRequest().setAttribute("lineSize", this.lineSize);
ServletObject.getRequest().setAttribute("column", this.column);
ServletObject.getRequest().setAttribute("keyword", this.keyword);
ServletObject.getRequest().setAttribute("url", this.url);
ServletObject.getRequest().setAttribute("columnData", this.columnData);
}
// 随后定义若干个数据获取的操作,因为分页参数处理完成之后肯定要把信息交给业务层进行数据加载
public Integer getCurrentPage() {
return currentPage;
}
public void setCurrentPage(Integer currentPage) {
this.currentPage = currentPage;
}
public Integer getLineSize() {
return lineSize;
}
public void setLineSize(Integer lineSize) {
this.lineSize = lineSize;
}
public String getColumn() {
return column;
}
public void setColumn(String column) {
this.column = column;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getColumnData() {
return columnData;
}
public void setColumnData(String columnData) {
this.columnData = columnData;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
8、
@RequestMapping("split")
public ModelAndView split() {
PageUtil pageUtil = new PageUtil("/pages/message/split.action", "消息标题:title|消息内容:content");
ModelAndView mav = null;
try {
mav = new ModelAndView("/pages/front/message/message_list.jsp",
this.messageService.split(pageUtil.getCurrentPage(), pageUtil.getLineSize(), pageUtil.getColumn(), pageUtil.getKeyword()));
} catch (Exception e) {
e.printStackTrace();
}
return mav; // 返回跳转的路径
}
9、http://localhost/pages/message/split.action?cp=2&ls=20&col=&kw=
dem 页码控制
生成页码控制
数据的分页显示是由两个部分组成:数据库数据的分页查询分页参数控制,在之前是通过手工的方式实现了分页参数的传输控制,这种操作机制是不便于用户使用的,所以最佳的做法是根据页面的数据记录个数动态的生成页码,而后使用者点击不同的页码就可以传递不同的 cp 参数,从而实现分页操作
1、http://localhost/pages/message/split.action?cp=1
2、
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
3、<jsp:include page="/pages/plugins/split_page_bar_plugin.jsp"/>
4、
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
int currentPage = 1; // 当前所在页
int lineSize = 10; // 每页显示的数据行数
long allRecorders = 0; // 总记录数
long pageSize = 0; // 总页数,这个值是需要计算得到的
%>
<% // 通过request属性实现数据的接收
try {
currentPage = (Integer) request.getAttribute("currentPage");
} catch (Exception e) {}
try {
lineSize = (Integer) request.getAttribute("lineSize");
} catch (Exception e) {}
try {
allRecorders = (Long) request.getAttribute("allRecorders");
} catch (Exception e) {}
pageSize = (allRecorders + lineSize - 1) / lineSize; // 计算总页数
if (pageSize == 0) { // 没有传递allRecorders属性并且没有数据记录
pageSize = 1; // 默认显示1页内容
}
%>
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<%
for (int x = 1; x <= pageSize; x ++) {
%>
<li><span><%=x%></span></li>
<%
}
%>
</ul>
</div>
5、
<li class="<%=x == currentPage ? "active" : ""%>"><span><%=x%></span></li>
6、
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<%
for (int x = 1; x <= pageSize; x ++) {
%>
<li class="<%=x == currentPage ? "active" : ""%>">
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
%>
</ul>
</div>
7、
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<li class="<%= currentPage == 1 ? "disabled" : ""%>">
<a href="<%=url%>?cp=<%=currentPage - 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><<</a>
</li>
<%
for (int x = 1; x <= pageSize; x ++) {
%>
<li class="<%=x == currentPage ? "active" : ""%>">
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
%>
<li class="<%= currentPage == pageSize ? "disabled" : ""%>">
<a href="<%=url%>?cp=<%=currentPage + 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>">>></a>
</li>
</ul>
</div>
8、
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
int currentPage = 1; // 当前所在页
int lineSize = 10; // 每页显示的数据行数
long allRecorders = 0; // 总记录数
long pageSize = 0; // 总页数,这个值是需要计算得到的
String column = null; // 模糊查询列
String keyword = null; // 查询关键字
String url = null; // 下一次跳转路径
%>
<% // 通过request属性实现数据的接收
try {
currentPage = (Integer) request.getAttribute("currentPage");
} catch (Exception e) {}
try {
lineSize = (Integer) request.getAttribute("lineSize");
} catch (Exception e) {}
try {
allRecorders = (Long) request.getAttribute("allRecorders");
} catch (Exception e) {}
pageSize = (allRecorders + lineSize - 1) / lineSize; // 计算总页数
if (pageSize == 0) { // 没有传递allRecorders属性并且没有数据记录
pageSize = 1; // 默认显示1页内容
}
column = (String) request.getAttribute("column");
keyword = (String) request.getAttribute("keyword");
url = (String) request.getAttribute("url");
%>
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<li class="<%= currentPage == 1 ? "disabled" : ""%>">
<%
if (currentPage == 1) {
%>
<span><<</span>
<%
} else {
%>
<a href="<%=url%>?cp=<%=currentPage - 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><<</a>
<%
}
%>
</li>
<%
for (int x = 1; x <= pageSize; x ++) {
%>
<li class="<%=x == currentPage ? "active" : ""%>">
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
%>
<li class="<%= currentPage == pageSize ? "disabled" : ""%>">
<%
if (currentPage == pageSize) {
%>
<span>>></span>
<%
} else {
%>
<a href="<%=url%>?cp=<%=currentPage + 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>">>></a>
<%
}
%>
</li>
</ul>
</div>
9、
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
int currentPage = 1; // 当前所在页
int lineSize = 10; // 每页显示的数据行数
long allRecorders = 0; // 总记录数
long pageSize = 0; // 总页数,这个值是需要计算得到的
String column = null; // 模糊查询列
String keyword = null; // 查询关键字
String url = null; // 下一次跳转路径
int seed = 3; // 定义一个配置的种子数
%>
<% // 通过request属性实现数据的接收
try {
currentPage = (Integer) request.getAttribute("currentPage");
} catch (Exception e) {}
try {
lineSize = (Integer) request.getAttribute("lineSize");
} catch (Exception e) {}
try {
allRecorders = (Long) request.getAttribute("allRecorders");
} catch (Exception e) {}
pageSize = (allRecorders + lineSize - 1) / lineSize; // 计算总页数
if (pageSize == 0) { // 没有传递allRecorders属性并且没有数据记录
pageSize = 1; // 默认显示1页内容
}
column = (String) request.getAttribute("column");
keyword = (String) request.getAttribute("keyword");
url = (String) request.getAttribute("url");
%>
<div id="pagebarDiv" style="float:right">
<ul class="pagination">
<li class="<%= currentPage == 1 ? "disabled" : ""%>">
<%
if (currentPage == 1) {
%>
<span><<</span>
<%
} else {
%>
<a href="<%=url%>?cp=<%=currentPage - 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><<</a>
<%
}
%>
</li>
<%
if (pageSize <= seed * 3) { // 当前的页码没有这么多
for (int x = 1; x <= pageSize; x ++) { // 生成全部的页码
%>
<li class="<%=x == currentPage ? "active" : ""%>">
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
} else { // 现在的页码很多
if (currentPage == 1) {
%>
<li class="active"><span>1</span></li>
<%
} else {
%>
<li>
<a href="<%=url%>?cp=1&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>">1</a>
</li>
<%
}
%>
<% // 生成中间的显示页码
if (currentPage < seed * 2 - 1) { // 设置一个生成中间爱你省略符号的判断条件
for (long x = 2; x <= seed * 2; x ++) {
if (currentPage == x) { // 当前所在页面
%>
<li class="active"><span><%=x%></span></li>
<%
} else {
%>
<li>
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
}
}
if (currentPage > seed * 2 - 1) { // 页码多了
%>
<li class="disabled"><span>...</span></li>
<%
}
if (currentPage >= seed * 2 - 1) { // 页码多了
long startPage = currentPage - seed; // 页码很多
long endPage = currentPage + seed; // 页码很多
if (endPage >= pageSize) { // 超过了总页数
endPage = pageSize - 1;
}
for (long x = startPage; x <= endPage; x ++) {
if (currentPage == x) {
%>
<li class="active"><span><%=x%></span></li>
<%
} else {
%>
<li>
<a href="<%=url%>?cp=<%=x%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=x%></a>
</li>
<%
}
}
}
if ((currentPage + seed + 1) < pageSize) {
%>
<li class="disabled"><span>...</span></li>
<%
}
%>
<%
if (currentPage == pageSize) {
%>
<li class="active"><span><%=pageSize%></span></li>
<%
} else {
%>
<li>
<a href="<%=url%>?cp=<%=pageSize%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>"><%=pageSize%></a>
</li>
<%
}
}
%>
<li class="<%= currentPage == pageSize ? "disabled" : ""%>">
<%
if (currentPage == pageSize) {
%>
<span>>></span>
<%
} else {
%>
<a href="<%=url%>?cp=<%=currentPage + 1%>&ls=<%=lineSize%>&col=<%=column%>&keyword=<%=keyword%>">>></a>
<%
}
%>
</li>
</ul>
</div>
数据查询控制
1、
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String url = (String) request.getAttribute("url"); // 分页的处理路径
// 数据的内容为“消息标题:title|消息内容:content”,采用的是“标签:字段名称|标签:字段名称|...”
String columnData = (String) request.getAttribute("columnData"); // 分页列表数据
String keyword = (String) request.getAttribute("keyword");
String column = (String) request.getAttribute("column");
%>
<div id="searchDiv">
<form action="<%=url%>" class="form-horizontal" id="myform" method="post" enctype="multipart/form-data">
<fieldset>
<div class="form-group" id="fileDiv">
<%
if (!(columnData == null || "".equals(columnData))) { // 有数据
%>
<div class="col-md-4">
<select id="col" name="col" class="form-control">
<%
String columnResult[] = columnData.split("\\|"); // 拆分查询列
for (int x = 0; x < columnResult.length; x ++) {
String temp[] = columnResult[x].split(":"); // 拆分
%>
<option value="<%=temp[1]%>" <%=temp[1].equals(column) ? "selected": ""%>><%=temp[0]%></option>
<%
}
%>
</select>
</div>
<%
}
%>
<div class="col-md-7">
<input type="text" name="kw" id="kw" class="form-control input-sm"
value="<%=keyword == null ? "" : keyword%>"
placeholder="请输入模糊查询关键字">
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-sm btn-primary" id="sendBut">搜索</button>
</div>
</div>
</fieldset>
</form>
</div>
2、<jsp:include page="/pages/plugins/split_page_search_plugin.jsp"/>
demo