文章目录
前言
用了小半年的时间摸索了一下Java后端,最近在学框架的时候觉得之前学习的内容有所遗忘,因此想自己做一个特别简单但是功能还算齐全的学生管理系统,给自己加深一下对servlet的理解。本人平时用Eclipse比较多,为了熟悉一下其他的IDE,这次的项目就用IDEA开发了。笔者也是一个初学者,理解不到位的地方还请各位朋友指出。本项目不使用web.xml手动添加servlet和filter,均采用注解的方式添加,需要创建web3.0及以上的工程。
github传送门
:https://github.com/MemoForward/StudentManagement
项目功能
- 实现对学生列表的增删改查
运用知识
- mysql数据库增删改查语句(单表)
- c3p0数据库连接池(c3p0-config.xml)
- 使用dbutils简化操作(QueryRunner)
- 使用BeanUtils封装数据(BeanUtils.populate(…))
- MVC设计模式
- servlet的使用
- 过滤器的使用(涉及一点点)
- 反射
一 、前期准备工作
工欲善其事,必先利其器。每个人都要以一名项目经理的角度要求自己,做项目之前先把框架搭好了:导入必要的环境配置以及构建好项目的架构。
1. 导入jar包
寻找jar包一直是最令人头疼的事情,所有还是推荐大家能尽快学习Maven,这次因为是最基础的Web工程,因此我就手动导入了,这里给出Maven的中心仓库,所有的jar包都可以在里面下载:http://mvnrepository.com(千万别去CSDN上用积分下载,大部分在CSDN上用钱买的资源都是免费的),笔者用的jar都比较老旧,读者可以自行下载新版。所有的包都应放在项目web文件夹下的 WEB-INF/lib 中。
c3p0-0.9.2-pre5.jar
commons-beanutils-1.8.3.jar
mysql-connector-java-5.1.39-bin.jar
commons-dbutils-1.4.jar
mchange-commons-java-0.2.3.jar // c3p0在0.9.2版本后会多出来一个辅助包
jstl.jar
standard.jar // 使用jstl标签需要用到的包
commons-logging-1.1.1.jar
2. 创建包
MVC:Model,View,Controller 分别代表Web工程中的三层结构。Model相当于持久层,主要用于与数据库交互,处理一些业务逻辑;View相当于展现层(WEB层),用于展现数据;Controller相当于控制层,用于处理Web层的数据并提供给持久层。
基于MVC架构,我们的项目也应该分成一个个的小的组件,而Web层都是jsp界面,因此在这里我们应该针对持久层和控制层建相应的包(src目录下):
2.1 持久层
- dao(实现与数据库的交互 Data Access Object)
- service(实现业务逻辑)
- domain(存放Bean的包)
2.2 控制层(统一放在web包下)
- servlet
- base(本项目使用了反射原理强化了普通servlet)
- filter (过滤器)
2.3 其他
- utils(工具包)
3. 创建配置文件
主要需要两个配置文件:log4j.properties 和 c3p0-config.xml。分别是使用log4j和c3p0的依赖。这两个配置文件都应该放置在类路径下(src/)
3.1 log4j.properties
#参考资料:http://www.blogjava.net/zJun/archive/2006/06/28/55511.html
log4j.rootLogger=debug,stdout,D
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n
#log4j.appender.D=org.apache.log4j.DailyRollingFileAppender
##log4j.appender.D.File=填写
#log4j.appender.D.Append=true
#log4j.appender.D.Threshold=DEBUG
#log4j.appender.D.layout=org.apache.log4j.PatternLayout
#log4j.appender.D.layout.ConversionPattern=%d{[yyyy/MM/dd HH:mm:ss,SSS]} [%5p] [%c:%L] - %m%n
#log4j.appender.D=org.apache.log4j.DailyRollingFileAppender
##log4j.appender.D.File=填写
#log4j.appender.D.Append=true
#log4j.appender.D.Threshold=ERROR
#log4j.appender.D.layout=org.apache.log4j.PatternLayout
#log4j.appender.D.layout.ConversionPattern=%d{[yyyy/MM/dd HH:mm:ss,SSS]} [%5p] [%c:%L] - %m%n
3.2 c3p0-config.xml(最好不要修改名字)
<?xml version='1.0' encoding='UTF-8'?>
<c3p0-config>
<default-config>
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:3306/数据库名</property>
<property name="user">root</property>
<property name="password">密码</property>
<!-- 连接池用完后等待,超时则抛异常-->
<property name="checkoutTimeout">30000</property>
<!-- 每30秒检测连接池中的空闲连接-->
<property name="idleConnectionTestPeriod">30</property>
<!-- 初始化连接个数-->
<property name="initialPoolSize">10</property>
<!-- 最大空闲时间,大于30秒则释放连接-->
<property name="maxIdleTime">30</property>
<property name="maxPoolSize">50</property>
<property name="minPoolSize">10</property>
<!-- 预缓存语句总计不超过200条-->
<property name="maxStatements">200</property>
<property name="destroy-method">close</property>
</default-config>
</c3p0-config>
二、创建数据库
本项目只用到了一个表,所以非常的简单,学生表的字段只有6个,分别是:id(主键),学号,姓名,性别,年龄,生日。这里给出创建表的sql语句。
`stu`DROP DATABASE IF EXISTS student_00;
CREATE DATABASE student_00 DEFAULT CHARACTER SET utf8;
USE student_00;
CREATE`stu` TABLE stu(
sid VARCHAR(255) NOT NULL PRIMARY KEY,
snum INT(10),
sname VARCHAR(255),
sage INT(10),
sgender VARCHAR(20),
birthday DATE DEFAULT NULL
)
三、美工
在写代码之前,笔者比较喜欢先把界面做出一个雏形,这样调试起来比较直观。但是笔者的前端技术非常之差,接触代码也不过一年的时间,所以没有时间学,这次的界面做的比较糟糕,各种bootstrap的组件乱贴,代码估计也会有很多的冗余,这里只给出图片,具体的内容各位如果有兴趣可以去git上下载源码。各位朋友如果实在看不下去,笔者也十分渴望和各位多多交流。所有的资源都应放在项目的web文件夹下。(忽略年龄和出生日期不匹配的问题…)
1. 主界面
2.编辑学生和添加学生界面
四、书写代码
这一部分的代码量还是挺多的,各位如果有兴趣可以去git上下载源码。这里主要介绍一下(JSP+Servlet+MySQL)的流程,其实网上优秀的教程还是很多的,各位不嫌弃的话在这里稍微看一下也无妨。除了基本的概念外,笔者还将介绍一下如何增强Servlet和实现过滤的操作,以及自己编写的工具类的介绍。
4.1 Jsp+Servlet+MySQL
最典型的MVC设计模式莫过于Jsp+Servlet+JavaBean:Jsp负责传递用户的输入以及显示用户需要的数据;Servlet用来处理从Jsp界面中传递过来的请求(获取到用户输入的数据),将数据进行业务逻辑处理后,将需要展示的数据封装到域对象(一般是request,session)中;而Servlet处理数据的操作主要是通过JavaBean来与数据库进行对话。因此,可以很清晰的看到,Servlet在数据的交互的过程中,起到了一个中间层的控制作用。如下图所示,一般情况下,每一个不一样的Jsp的请求我们就需要去创建一个Servlet类去处理(这里注意一下本项目中Servlet是
单例多线程
的,因此会有线程安全的问题),所以我们需要去创建很多的Servlet类去实现功能。
这里必须说明一下Service和DAO的区别。Serive和DAO里面的内容都是业务逻辑,比如:查找某个学生,修改某个学生信息等。但是DAO层我们往往只关心单层逻辑,而在Service层中我们可以实现多层逻辑。这里举个比较直观的例子:
在本项目中实现了分页的功能,我们抽取的页面Bean(PageBean:可以理解为一个页面往往不会只包含了学生的个人信息,因此需要将页面的模型抽取出来)中包含两个变量:一个是数据库中总的学生数量,一个是本页(可见上图的主界面)中包含的学生信息。我们在Servlet中希望得到的数据是一个包含这两个变量数据的PageBean,而获取PageBean中两个变量的操作对应了两条sql语句:
select count(*) from stu
和
select * from stu limit ?,?
这两条语句其实就是两个业务逻辑,对应两个DAO层的方法,而Service的作用就是组装这两个逻辑,实现多层的逻辑,即获取到整个页面的数据。
因为本项目过于简单,所以这两层其实也没有必要细分,不过以后一定会遇到更为复杂的项目的,所以还是建议各位养成良好的习惯。
这里笔者还是稍加笔墨用一个简单的示例展现一下这个流程在本项目中的应用,如果用户想要
删除学生
:
4.2 增强Servlet
上一小节我们介绍了一下Jsp+Servlet+MySQL的流程,这种流程还是非常简单易懂的,但是有一个特别不方便的地方在于,每一个来自页面的请求,我们往往都需要一个Servlet类去满足这个请求,这其实是不符合我们的生活习惯的,试想一下:如果存钱和取钱对应不同的ATM机,估计我们会被搞疯掉。我们更希望面对的情况是:一台ATM机会处理来自所有关于存款的操作。也就是说,我们希望用一个关于学生的StudentServlet去满足所有针对情况的请求,不同的请求对应不同的方法。而
servlet的生命周期
是这样的:
- init() //第一次创建 Servlet 时被调用,后续的请求不再调用
- service(ServletRequest request, ServletResponse response) //Servlet处理客户端请求所调用的方法
- 在Service下有我们常见的doGet和doPost方法
- destory() //只会被调用一次,在 Servlet 生命周期结束时被调用
- 被垃圾回收
可以看到,servlet最核心的部分就是service()方法,我们自己创建的Servlet都是要继承HttpServlet类,如果有阅读源码习惯的小伙伴可以看一下这个类中实现d的service()方法,这个方法会分析界面的请求类型,自动调用doGet和doPost方法。
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod(); //在这里取界面出传递过来的方法,一般就是GET或者POST
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
而我们去实现自己的子类Servlet的时候,就是在重写doGet方法,笔者在这里因为篇幅原因不加赘述。但是如果要实现我们的目标应该怎么做呢?
由HttpServlet类中的service()方法得到启发:我们应该直接从界面传递要执行的Servlet请求方法,在service()方法中得到请求的方法名,从而直接利用反射调用子类的方法。因此我们需要重写service()方法,所以笔者建立了Base包来创造一个BaseServlet来重新实现service()方法
。话不多说,上代码:
import java.io.IOException;
import java.lang.reflect.Method;
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getParameter("method"); //客户端的请求应包含mehod的键来传递方法名
//如果没有method键,则调用默认方法
if(method == null || method.equals("")){
method = "excute";
}
//如果子类继承了这个类,则会创建子类的servlet,使用this会得到子类的类字节码
Class clazz = this.getClass();
try {
Method md = clazz.getMethod(method,HttpServletRequest.class, HttpServletResponse.class);
if(md != null){
//子类必须要返回一个供父类转发的页面路径,如果子类想重定向,则return null;
String jspPath = (String)md.invoke(this, req, resp);
if(jspPath != null){
//子类的所有方法,统一由父类转发
req.getRequestDispatcher(jspPath).forward(req,resp);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 默认方法
public String excute(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
return null;
}
}
至此,我们只需要在一个Servlet类中编写所有的请求方法就可以了,如果页面想发送请求,则需要编写如下个格式的代码:
<!--需要给出method参数,如果不给出则执行exctue默认方法-->
<a href="${pageContext.request.contextPath}/studentServlet.do?method=addStudent" >添加学生</a>
在StudentServlet中则对应如下的方法:
public String addStudent(HttpServletRequest req, HttpServletResponse resp){
Student student = new Student();
//将从页面获取的数据通过工具类封装到Bean里
MyBeanUtils.populate(student,req.getParameterMap());
StudentService stuService = new StudentServiceImpl();
student.setSid(UUIDUtils.getId());
// System.out.println(student);
try {
stuService.addStudent(student);
resp.sendRedirect("index.jsp");
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
4.3 过滤器的操作
众所周知,从客户端传递过来的数据,或有中文的编码问题,post和get方法对应的解决方法也不同,主要原因在于传递数据的途径不一样,post将所有的数据都封装在http请求头的消息体(entity-body)中,用户可以自定义编码的类型;而Get的数据则通过URL进行传递,由浏览器进行编码(大部分都是iso8859-1)传递给Servlet。因此,对于POST请求,我们需要自己设置编码类型,而对于GET请求,我们需要用iso8859-1进行解码,再编码成UTF-8进行使用。如果对每一个Servlet方法都进行编码设置的话,会有大量的重复代码,因此,我们需要通过一个过滤器去拦截所有的请求,统一这些请求的编码格式再传递给后端。过滤器的作用如下:
过滤器的代码如下(过滤器也是需要注解配置的或者在web.xml中配置)
package com.memoforward.web.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
// /*表示拦截所有请求
@WebFilter(filterName = "EncodingFilter", urlPatterns = "/*")
public class EncodingFilter implements Filter {
public void destroy() {}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)resp;
//这个方法是过滤器放行的方法,我们通过在通过增强request来保证Servlet中使用的request能够
//自动处理编码问题
chain.doFilter(new EnhancedRequest(request), response);
}
public void init(FilterConfig config) throws ServletException {}
//web容器在使用HttpServletRequest接口时会自动创建HttpServletRequestWrapper实例(这里应该是通过依赖注入实现的 ,也不知道我理解的对不对)
//继承一下这个类提醒一下这个知识点,毕竟原来是从接口直接调用的方法
class EnhancedRequest extends HttpServletRequestWrapper {
private HttpServletRequest request;
boolean flag = false;
public EnhancedRequest(HttpServletRequest req){
super(req);
this.request = req;
}
@Override
public String getParameter(String name) {
if(name==null || name.trim().length()==0){
return null;
}
String[] values = getParameterValues(name);
if(values==null || values.length==0){
return null;
}
return values[0];
}
@Override
/**
* hobby=[eat,drink]
*/
public String[] getParameterValues(String name) {
if(name==null || name.trim().length()==0){
return null;
}
Map<String, String[]> map = getParameterMap();
if(map==null || map.size()==0){
return null;
}
return map.get(name);
}
@Override
/**
* map{ username=[tom],password=[123],hobby=[eat,drink]}
*/
public Map<String,String[]> getParameterMap() {
/**
* 首先判断请求方式
* 若为post request.setchar...(utf-8)
* 若为get 将map中的值遍历编码就可以了
*/
String method = request.getMethod();
if("post".equalsIgnoreCase(method)){
try {
request.setCharacterEncoding("utf-8");
return request.getParameterMap();
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else if("get".equalsIgnoreCase(method)){
Map<String,String[]> map = request.getParameterMap();
if(flag){
for (String key:map.keySet()) {
String[] arr = map.get(key);
//继续遍历数组
for(int i=0;i<arr.length;i++){
//编码
try {
arr[i]=new String(arr[i].getBytes("iso-8859-1"),"utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
}
flag=false;
}
//需要遍历map 修改value的每一个数据的编码
return map;
}
return super.getParameterMap();
}
}
}
4.4 工具类的说明
其实用不用工具类都行,这些东西其实和项目的关系都不是很大了,自己编写工具类的好处就是用起来爽,有什么需要添加的功能直接在类里面写,真正在Servlet或者其他地方调用的时候显得简洁和清晰。笔者这里使用了三个工具类,具体代码还请给位看官移步github。
- 关于c3p0操作的C3O0Utils类,使用最多的就是获取连接操作。
- 关于Bean封装的MyBeanutils类,增强了beanutils包,主要实现了对日期格式的转换
- 关于学生唯一id生成的UUIDUtils类,增强了 UUID包,主要是因为UUID获得数据是UUID格式的,比较让人不爽
4.5 编写此项目的流程
在美工和数据库以及系统架构和环佩配置好了以后,就可以开始编写代码了,笔者比较喜欢的编码顺序是这样的:先把所有数据库中的实体Bean创建完毕(比如此项目中的Student),随后从美工界面出发一个个去将请求的Servlet方法编写完毕,servlet中调用的Service类就先定义出其接口。先把大逻辑搭建出来,等这个Servlet大逻辑编写完毕后再去实现低层的Service和Dao方法。当 “请求–servlet–service–dao” 的流程都都编写完成后,再去调试这个请求逻辑能不能跑通。当所有的请求都编写完后,这个项目就做完了。因为笔者比较菜,这个简单的项目用了一整天的时间才做完,期间也遇到了些许问题,如果有机会,希望能和各位多交流交流,各位如果有问题也可以在评论区问我。
五、本项目出现过的问题
在项目中遇到了两个比较非常棘手的问题:
-
关闭或重启服务器会提示有线程释放不掉,导致内存泄漏
。其实这是tomcat的一个误报,因为c3p0的线程需要时间去关闭,具体答案请参考StarkOverlFlow:
c3p0 Connection pool memory leak redeploy tomcat
-
热部署的时候总是会提示 Illegal access: this web application instance has been stopped already.
这个问题似乎是因为tomcat缓存了该项目的旧版本,所以相当于一个tomcat启动了两个相同的项目所以报了异常。不影响项目的使用和调试,不过笔者水平有限,并没有解决这个问题,各位如果在运行本项目的时候遇到了这个问题,可以移步:
Illegal access: this web application instance has been stopped already