"易班"——项目总结

  • 1. 项目概述
  • 2.项目背景
  • 3. 项目应用到的技术
    • 1.Maven
    • 2.JDBC
    • 3.Servlet
    • 4 .Tomcat
  • 4.项目功能
    • 1 .用户登录
    • 2 .学生信息管理
      • 1.建库建表
      • 2.功能
  • 5.项目中的闪光点
    • 1.页面重定向(Filter过滤器)
    • 2.数据分页
  • 6.总结和展望
    • 1.总结
    • 2.展望
  • 7.开源代码

1. 项目概述

"易班"项目是基于Java语言的一个Web项目,是一个学生和班级信息管理平台,它可以对学生或者班级信息进行增加,删除,改正,查询的一个信息平台,通过网页访问管理人员可以了解到实时的学生信息和班级信息,最直接的适用场景是适用于各个学校,对字段进行修改后也可以适用于公司,组织等等一切需要对人力资源信息进行管理的场景下,因此这个项目具有相当的实用性和必要性。

2.项目背景

总的来说,易班项目是我在学习了数据库和servlet、请求响应处理后、第一次尝试正式写一个项目,虽然来说项目并不算很复杂,但是重在学习实际应用中项目开发流程,也是为以后的学习和工作打下更牢固的基础。只学习没有实践会让知识掌握的不牢固,更何况对于我来说,一个项目的完成更能查找我在学习中忽略的漏洞和鉴定我的信心。

3. 项目应用到的技术

1.Maven

Maven是Apche下的一个纯java开发的开源项目,基于项目对象模型(POM),Maven是一个项目管理工具,可以对java项目进行构建,依赖管理。使用Maven像使用其他的构建工具一样,可以极大的提高工作效率。

使用Maven首先我们要创建一个项目对象模型(POM),POM的默认配置是pom.xml文件,它描述了程序及其相关性。简而言之就是我们在开发一个项目时需要一些特定的我们没有实现的类,这个时侯需要导入外部别人写好这个类并已经打成的jar包,导入到我们的Library中后我们就可以调用这些jar包中已经实现好的类。有了Maven之后我们可以省去繁琐的手动导入的步骤,直接通过pom.xml中的配置来获取jar包。

说到这里,我一直有一个误解就是Maven项目是独立于java项目和web项目的一种特有的项目类型,后来学习了大佬的解释才发现,其实在java学习中主要有两种项目,一个是普通java项目还有一个就是基于java的web项目,而所谓的Maven项目就是在上述的两个项目上套一个Maven框架,,其实就是加上之后我们可以在项目开发过程中使用一些Maven为我们提供的功能,方便我们进行开发,这也是为什么我们叫它项目管理工具的原因。

配置一个pom.xml大致就分为三个部分(个人总结可能不准确):

一、设置自己的信息

    <groupId>iron-man</groupId>//项目作者的标识<artifactId>yiban</artifactId>//项目的名字<version>1.0-SNAPSHOT</version>//项目的版本<packaging>war</packaging>//最终打包的格式

上面三个可能还很好理解,第四个打包形式可能有点疑惑,我们打包格式往往有两种形式一个是jar包就是我们普通的java项目,还有一种就是Web项目打包形成的war包,拿着这个war包布置到容器中,本项目来说就是Tomcat,将war包放在Tomcat目录下的\webapps\下,Tomcat会自动帮我们解压,相当于发布成功。

二、需要的别人的信息

<dependencies>//所有的依赖包<dependency>//依赖包<groupId>mysql</groupId>//项目名字<artifactId>mysql-connector-java</artifactId>//一般是包名,也就是域名的反写<version>5.1.48</version>//需要的jar的版本</dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><scope>provided</scope></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.12</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.10.3</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.8.2</version><scope>compile</scope></dependency></dependencies>

这个部分即是我这个项目开发时用到的那些依赖包,这些依赖包都有的三个属性是 groupId、artifactId、version 根据这三个信息,我们可以唯一确定一个我们需要的jar包。 scope这个的意思是,这个jar包只会在测试的时候用到。

三、编写完成后构建项目时的信息

 <build>//构建项目时需要的配置<finalName>sis</finalName>//最终打成包的名字比如这里就是sis.war<plugins>//使用的所有插件列表<plugin>//使用的某一个插件<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration>//作为DOM对象的配置<source>1.8</source>//配置源代码使用的开发版本<target>1.8</target>//配置需要生成的目标class文件的编译版本<encoding>UTF-8</encoding>//编码格式</configuration></plugin></plugins></build>

Mawen项目目录结构:

2.JDBC

JDBC是基于java操作Mysql数据库的常用驱动包,同样编程语言和使用数据库的不同,所需要的驱动包也就不同,例如java语言操作Oracle数据库时需要的驱动包名字叫ojdbc。

JDBC的全称JAVA Database Connectivity,翻译过来就是java数据库连接。是一种用于执行SQL语句的java API(应用程序接口或者应用编程接口),说白了就是程序开发者通过编程软件直接访问数据库,直接面向已经抽象好的接口编程,在我们实际操作和数据库之间架起了一道桥梁。不仅仅是提高了工作效率,JDBC操作往往有其固定的步骤,最大的好处是程序的可移植性大大增强。

JDBC的连接往往分为六步,但是我想强调的是初学者比如我,往往忽略了导入数据库驱动包的过程,也就是导入jar包作为依赖,虽然在开发过程中往往不需要这一步,对于初学者有这种概念固然是没错的。

第一步:建立数据库链接
本项目中采用一个单例模式创建一个数据源DataSource,通过getDatasource().getConnection()的方式获取了一个数据库的连接,使用这种方式的好处是我们可以设置getDatasource()方法为私有属性,封装了连接数据库参数,并且只需要创建一次,以后返回的都是这个对象。只是外部提供了一个获取数据库连接的接口。

private static DataSource getDatasource() {if (DS == null) {//懒汉模式的单例synchronized (DBUtil.class) {if (DS == null) {DS = new MysqlDataSource();((MysqlDataSource) DS).setURL(URL);((MysqlDataSource) DS).setUser(USERNAME);((MysqlDataSource) DS).setPassword(PASSWORD);}}}return DS;}public static Connection getConnection() {try {return getDatasource().getConnection();} catch (SQLException e) {throw new RuntimeException("获取数据库连接失败", e);}}

第二步:需要执行的sql语句要进行预编译,也就是针对要对数据库完成的操作,写一条String类型的sql语句。

第三步:创建操作命令对象,这个对象包含了要查询的sql语言。

ps=c.prepareStatement(sql);

第四步:执行查询操作,拿到这个对象执行后返回的结果。

rs=ps.executeQuery();

第五步:对返回的查询结果集进行处理
第六步:释放资源,由于获取连接和释放资源在本项目中都应用了多次,所以写成了一个固定的DBUtil类提供了获取连接和释放资源的方法,使代码看起来更清爽。

  public static void close(Connection c, Statement s){//更新操作调用的是两个参数的重载方法close(c,s,null);}public static void close(Connection c, Statement s, ResultSet r) {//查询操作调用的是三个参数的方法try {if (c != null) {c.close();}if (s != null) {s.close();}if (r != null) {r.close();}} catch (SQLException e) {throw new RuntimeException("释放数据库资源失败", e);}}

3.Servlet

还是先说什么是Servlet,“Servlet是运行在Web服务器或应用服务器上的程序,它是作为来自Web浏览器或者其他Htpp客户端的请求和HTTP客户端的请求和HTTP服务器上的数据库或者应用数据之间的中间层”。如何理解这句话呢?

打开java源码就能发现,其实我们说的servlet就是一个接口,接口肯定是实现了某个功能,功能就是处理客户端和服务器端之间的请求和响应,它是处理这些的一套规范的流程。只不过这个流程需要处理请求的不同,所以他是一个接口,具体的处理步骤交给我们开发人员去实现。

至于servlet是怎么工作的?我们就可以大致理解为两个步骤:
第一个:处理来的请求比如,一个HTTP类型的请求过来,容器会把这个请求封装成servlet中的request对象,然后可以拿着这个对象获取你以可得到的所有的HTTP信息。
第二个:把数据封装成servlet中的response对象,容器再将这个response对象解析之后封装成一个HTTP响应再返回。就是一个完整的过程。

再来讨论一下servlet的生命周期:
不论java中什么谈到生命周期,其实都是一个创建到毁灭的过程。servlet也遵循这个过程,它的生命周期是。
*Servlet通过调用init()方法进行初始化。
*Servlet通过service()方法处理客户端请求。
*Servlet通过destroy()方法终止。最后被JVM垃圾回收期进行垃圾回收。结束工具人的一生。

多次提到servlet容器,目前我们常用的容器是tomcat,在下文也有介绍。

再回到项目中,因为本项目有多个servlet类而且除了调用dopost和doget的区别,其他过程都类似,因此我们采用模板方法的设计思想,模板方法就是提供一个统一的处理逻辑,在不同条件调用不同的方法。设计了一个抽象类继承了HttpServlet,重写了doget方法和dopost方法。这两种方法的执行流程我们已经确定,唯一的改变在于返回值是Object 的process抽象方法,这个方法里边才根据业务的不同,重写这抽象个方法达到我们设计一个模板方法的目的,采用这种方法好处是增加代码的复用性。

public  abstract   class AbstractBaseServlet   extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doPost(req,resp);}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {req.setCharacterEncoding("UTF-8");resp.setCharacterEncoding("UTF-8");resp.setContentType("application/json"); // 1.表单提交的参数,这里的表单是指默认的表单提交方式,表示的是Content-Type字段x-www-form-urlencoded//get在url中,post在body请求体中,格式是k1=v1&k2=v2;//如果不采用默认方法,比如手写前端的ajax请求,请求格式为application/json;请求体,转为字符串PrintWriter pw=resp.getWriter();Response r=new Response();//对数据库JDBC的操作一般不要和Servlet混在一起。重新写了一个包dao,专门写JDBC//HttpServletRequest对象.getParameter()表示接收参数,这里的参数为页面提交的参数。// 2.URL重写(就是***?id = 1)传的参数等,// 因此这个没有设置参数的方法(没有SetParameter),而接收参数的返回是String.//HttpServlet对象。getInputStream();通过输入流获取?请求体都可以获取到,但是需要解析(依赖代码实现)try {Object o=process(req,resp);r.setSuccess(true);//页面在设计的时候,是根据如果这和success字段是true的时候才进行业务数据的处理。r.setCode("COK200");r.setMessage("操作成功");r.setTotal(ThreadLocalHolder.getTOTAL().get());//不管是否分页操作,都获取当前线程中的Total字段r.setData(o);} catch (Exception e) {r.setCode("ERR500");r.setMessage(e.getMessage());StringWriter sw = new StringWriter();//通过输出流获取出现异常的堆栈信息PrintWriter writer = new PrintWriter(sw);e.printStackTrace(writer);//异常打印堆栈信息String stackTrace =sw.toString();System.err.println(stackTrace);//打印到后台的控制台r.setStackTrace(stackTrace);//把stackTrace作为异常信息再返回到前端}finally {ThreadLocalHolder.getTOTAL().remove();//在线程结束前,一定要记得删除变量,否则存在内存泄露问题}pw.println(JSONUtil.write(r));pw.flush();}protected abstract Object process(HttpServletRequest req, HttpServletResponse resp) throws Exception;}

4 .Tomcat

tomcat是什么,中文直译过来就是三脚猫嘛?哈哈,不是,歪果仁似乎对 tom这个名字格外的喜爱,就比如我们叫谁狗蛋翠花一样。回到是什么的问题上?我们似乎了解的只是我们的项目能在tomcat上运行起来,至于是什么我们好像只有在tom报错,或者8080端口被占用的时候才不情愿的看他一样,那么实质上tomcat是什么呢?
Tomcat实质上是一个web服务器器和servlet容器的结合体(也可以是jsp容器,13规范里它只实现了这两,这也是它为什么是叫轻量级服务器的原因,“J2EE规范”:JDBC,JNDI,EJB,RMI,JSP,SERVLET,XML,JMS,JAVA IDL ,JTS,JTA,JAF,JAVA MAIL ,但是我不会,我骄傲了嘛)。那么我们关注的就变成了什么是web服务器?什么是servlet容器?

web服务器:说起来xue微有点抽象,我本人来说也不是很了解,只能说是什么作用,比如说你主机上有一张照片(就是资源)是我想要的,我怎么拿到呢?抱走你的电脑嘛?其实不是,那么web服务器的作用就是把你这个照片(资源)映射为一个url提供给我访问。(映射具体怎么解释我也没有一个没正统的理解,意会意会)

servlet容器:刚才我们都说了它的作用就是就是存放了我们创造的servlet对象,就拿刚刚那个问题说,请求这个照片时,机器不是人,他不明白你说的话,servlet的作用就是处理你的请求让我明白,然后再给你一个服务器的响应。重述一下servlet程序处理的三个过程:
1.接受请求
2.处理请求
3.响应请求
这三个过程,都是必要的,但是我们发现接受请求和响应请求在所有处理过程中都是类似的,而处理请求则会根据业务的变化而变化,这就是要求我们程序猿自己去搞的,所以我们把接收和响应抽取变成web服务器,(有一位伟大的程序员说过,能偷懒绝不自己写代码)。

再看看我们必要的tomcat内部的目录结构:

PS:这里除了webapps是重点之外,还要明确的是lib这个目录,我们经常会有一种误区,“你不是说tomcat是servlet容器(jsp容器)和web服务器的组合吗?为什么我写了JDBC的代码,他还可以认识。”我要说“老铁,那是因为我们在安装tomcat学习得时候,还有一个步骤就是把jdbc.jar包放到lib下了’所以我们可以正常使用,只不过是我们只是放入了一次,使用多次罢了。

之上提到的4中关键技术,是易班这个小项目所必须了解的,不是针对这个项目中涉及的技术,这四种技术可以说在我们日常学习开发web应用程序时都要涉及,之后可能在工作中随着越来越多框架的使用,我们甚至不用了解它时怎么做的,但是还是那句话,对我这种菜逼来说总结总结也是一种收获。

4.项目功能

1 .用户登录

用户登录界面展示

仔细想想还是不打算把这块详细介绍了,比较有亮点的就是这里有一个页面重定向的过程,用到了过滤器,在下面的闪光点有介绍,暂且往下面看。

2 .学生信息管理

1.建库建表

学生管理作为这个项目的核心要点,作为一个信息管理平台,我们应该保证的是可以查询所有学生的信息,包括姓名,学号,身份证号(可选),班级,毕业年份,专业,邮箱,如果有必要我们还以加上创建这个学生信息时的时间。所以我这个项目,就优先考虑了这些学生信息,如果还有什么需要我们还可以再往里边添加,就涉及到修改数据库表的部分。

我在做这个项目的时候,因为有做博客小项目的经验(只是简单的项目没有复杂的前端设计等等,但是重在了解步骤)
第一件事:我们应该考虑如何建库建表,建库好说,那么建表呢,建表需要考虑我这个项目中所有的关系,比如用户表,学生和班级的关系,学生和寝室的关系,是一对一还是一对多,这都是我们要在建表过程中所考虑的。还是先给一个结论,再讲我是为什么要这么设计?

1.用户表比较简单,也没有什么管理,它内部维护的是用户的数据和其他没有太大的关系,我不做仔细介绍。

2.就是学生表和班级表,可以从图的管理我们很明显的看到,学生和班级的关系是,一个学生只有一个班级,一个班级可以对应多个学生。联系实际也比较容易想到。

3.第三就是数据字典和数据字典标签了,我先说为什么这么设计,原因是应用在了一些通用的下拉菜单的选项上。

比如在插入一个新增一个学生信息的时候,通过下来菜单提供的选项,选择某个班级,那么这个班级对应的年分,专业也就确定了。数据字典表和数据字典标签表是一对多的关系。例如在班级表中毕业年份和专业。在设计上也是通过数据字典表和数据字典标签表来保存,两张表都是key和Value的形式,字典表是父节点,字段标签表是子节点,下拉菜单通过父节点的key查询出所有关联的子节点,再使用子节点的key和value进行下拉菜单的选项的初始化。

下拉菜单的背后是:页面元素——接口请求——接口响应。

2.功能

功能也就是我们经常提到的前后端约定好的接口,我实现的是学生管理相关的业务,所以我主要实现了和该业务相关的后端接口,说一些题外话,这就是我理解的web’开发,什么是web开发?

第一:开发一个项目就是首先明确做那个业务,这个业务要实现那些功能?
第二:前后端开发人员商量好,具体那个功能对应那个前端接口,前端需要那些数据,什么格式的数据,请求发到后端之后,后端按照前端需要的格式返回需要的数据。
第三:对这些数据进行排版上的优化,呈现一个漂亮的页面。
这三点就是我理解的web开发。

那么从这个页面显示来看,我做的主要就是学生管理业务下的,针对学生信息进行增加,修改,删除,查询功能,查询往往是被忽视的,因为从登录页面直接跳转到了查询全部学生信息的页面,其实这个也是需要实现的功能。

不论开发这四大功能的哪一个,流程都是一样得,需要知道前端到底发过来一个什么类型得请求。是GET 还是POST,约定好得接口是什么?知道这些后代码编写查询数据,处理格式,返回响应。一个功能开发完毕。用两个功能接口举例。

第一个:查询学生信息功能
前端接口:GET student/query?
需要我们响应的格式:

可以看到响应包含显示是否请求成功的success字段、状态码code字段、信息message字段、总数total字段,剩下的就是一个data字段,data里边包含了每个学生对象的信息。那么基本思路就是返回上述信息,其中data数据部分用List来保存每个学生信息,再返回。

实际对应的功能页面和浏览器抓包:

可以看到我们响应返回了正确的数据格式,而且前端不需要的数据格式我们并没有设置,是默认值null。那么后端是怎么做到的呢?

我们需要一个servlet,并且路径匹配我们前端约定。
标红的地方是需要理解的,常规我们要实现一个web开发中,我们需要每次在servlet的web.xml中手动配置,这是真特么烦,所以servlet 3.0为我们提供了更为简便的方法,就是注解形式。。这也是JDK 1.5版本之后,JAVA提供的一种新的数据类型(Annotation),中文是注解或者标注。这种方式解决了xml配置文件繁琐冗余的缺点。

需要强调的是这个注解只能使用在继承了HttpServlet类之上,属于类级别的注解。

接着要考虑的就是我们怎么去返回需要且格式正确的数据。数据的具体处理先不做介绍,先看如何得到这个data。

/*** 学生表*/
@Getter//使用lombook插件自动生成的get  set  tostring  方法
@Setter
@ToString
public class Student {private Integer id;/*** 姓名*/private String studentName;/*** 学号*/private String studentNo;/*** 身份证号*/private String idCard;/*** 邮箱*/private String studentEmail;/*** 班级id*/private Integer classesId;/*** 创建时间*/private Date createTime;//班级属性private  Classes classes;//和前端名字对应,否则会造成一些bug
}

值得强调的是我们Student 类中包含一个 Classes类 ,也就是学生类中包含了一个班级类。那么有必要再把Classes类里边的属性做一个展开。

/*** 班级表*/
@Getter
@Setter
@ToString
public class Classes extends DictionaryTag {private Integer id;/*** 班级名称*/private String classesName;/*** 毕业年份,数据字典000001*/private String classesGraduateYear;/**专业,数据字典000002*/private String classesMajor;/*** 备注*/private String classesDesc;/*** 创建时间*/private Date createTime;
}

这是我们的基础类,当查询出每个学生的信息时候,先得到一个List,再创键一个学生对象,再把这个对象放入Lsit中,重复这个步骤直到所有学生信息放入完毕。

这些解决完之后到了真正要执行JDBC操作的环节了。

public class StudentDAO {public static List<Student> query(Page p) {Connection c=null;PreparedStatement ps=null;ResultSet rs=null;List<Student> list=new ArrayList<>();try {//1.获取数据库连接c= DBUtil.getConnection();//工具类以及该封装好的方法//sql语句预编译StringBuilder sql= new StringBuilder("select s.id," +"       s.student_name," +"       s.student_no," +"       s.id_card," +"       s.student_email," +"       s.classes_id," +"       s.create_time," +"       c.id cid," +"       c.classes_name," +"       c.classes_graduate_year," +"       c.classes_major," +"       c.classes_desc" +"  from student s" +"         join classes c on s.classes_id = c.id");if (p.getSearchText()!=null&&p.getSearchText().trim().length()>0){sql.append(" where s.student_name like ?");//模糊查询}if (p.getSortOrder()!=null&&p.getSortOrder().trim().length()>0){String s=p.getSortOrder();sql.append("  order by s.create_time  "+s);//不能用占位符操作,字符串替换的时候会带上单引号  也就是order  by  xxx  ‘asc’//但是这种l注方式存在sq入的风险}//1.1  获取查询总数量:使用以上sql可以复用,可以使用子查询StringBuilder countsql=new StringBuilder("select count(0) count from (");countsql.append(sql);countsql.append(")tmp");ps=c.prepareStatement(countsql.toString());if (p.getSearchText()!=null&&p.getSearchText().trim().length()>0){ps.setString(1,"%"+p.getSearchText()+"%");}rs=ps.executeQuery();while (rs.next()){int count=rs.getInt("count");//TODOThreadLocalHolder.getTOTAL().set(count);//设置total变量到当前线程中的ThreadLocalMap数据结构中保存}//1.2  获取分页数据sql.append(" limit ?,?");//在实际应用中,如果查询一张表的所有字段,sql语句也不要写成*,两张表如果有一样的字段,比如上面的id   一定要用别名区别//2.创建操作命令对象ps=c.prepareStatement(sql.toString());int index =1;if (p.getSearchText() != null&&p.getSearchText().trim().length()>0){ps.setString(index++,"%"+p.getSearchText()+"%");}ps.setInt(index++,(p.getPageNumber()-1)*p.getPageSize());//设置索引ps.setInt(index++,p.getPageSize());//3.执行查询操作rs=ps.executeQuery();//4.处理查询结果集while (rs.next()){Student student=new Student();//设置属性,通过结果集获取来设置student.setId(rs.getInt("id"));student.setStudentName(rs.getString("student_name"));student.setStudentNo(rs.getString("student_no"));student.setIdCard(rs.getString("id_card"));student.setStudentEmail(rs.getString("student_email"));student.setClassesId(rs.getInt("classes_id"));student.setCreateTime(new Date(rs.getTimestamp("create_time").getTime()));Classes  classes = new Classes();student.setClasses(classes);classes.setId(rs.getInt("cid"));classes.setClassesName(rs.getString("classes_name"));classes.setClassesGraduateYear(rs.getString("classes_graduate_year"));classes.setClassesMajor(rs.getString("classes_major"));classes.setClassesDesc(rs.getString("classes_desc"));// classes.setCreateTime(new Date(rs.getTimestamp("cct").getTime()));list.add(student);}return list;} catch (Exception e) {throw  new RuntimeException("查询学生列表出错",e);//如果不把这个具体的异常信息传入参数,那么异常信息就会丢失,出现问题时不方便定位} finally {//5.释放资源DBUtil.close(c,ps,rs);//封装得释放资源的方法}}}

先不管传入的分页参数p,分页是怎么回事,一会再介绍。整体浏览下来,主要还是一个连接,查询,处理,添加,释放资源的过程。
1.获取到了数据库连接。
2.对sql进行拼接,因为请求分页的过程中sql是动态变化的,所以我们采取了StringBuilder。接着就是一系列针对分页进行的操作。
3.创建操作命令对象,并且把写好的sql查询语句作为参数。
4.执行查询操作
5.处理查询结果集,这个时侯通过一个循环,先创建一个学生对象,获取结果集中id、student_name、student_no等等字段,设置学生的属性,设置完毕后添加到List列表中,所有学生对象被添加完后,返回list。调用 DBUtil工具类的关闭资源方法。

基本上所有的功能都是类似的操作,只是再sql的拼接上有所不同。但是略微有点区别的是:修改和删除功能是没有结果集的,也就是说我们执行sql后,不必要执行后边的数据分析过程。

 public static void updata(Student s) {Connection c=null;PreparedStatement ps=null;//没有结果集try {//1.获取数据库连接c= DBUtil.getConnection();//工具类以及该封装好的方法//sql语句预编译String sql="update  student set  student_name=?,student_no=?,id_card=?,student_email=?,classes_id=?  where id=?";//根据学生id进行修改//2.创建操作命令对象ps=c.prepareStatement(sql);ps.setString(1,s.getStudentName());ps.setString(2,s.getStudentNo());ps.setString(3,s.getIdCard());ps.setString(4,s.getStudentEmail());ps.setInt(5,s.getClassesId());ps.setInt(6,s.getId());//3.执行sql语句int num=ps.executeUpdate();//返回值是成功插入了几行数据,如果插入不成功也就说不等于1就算失败,这个方法没有实现} catch (Exception e) {throw  new RuntimeException("修改学生信息出错",e);//如果不把这个具体的异常信息传入参数,那么异常信息就会丢失,出现问题时不方便定位} finally {//5.释放资源DBUtil.close(c,ps);//封装得释放资源的方法}}public static void delete(String[] ids) {//通过id确定要删除的学生。Connection c=null;PreparedStatement ps=null;//没有结果集try {//1.获取数据库连接c= DBUtil.getConnection();//工具类以及该封装好的方法//sql语句预编译StringBuilder sql=new StringBuilder("delete from student where  id in (");for (int i = 0; i <ids.length ; i++) {if (i!=0) {sql.append(",");}sql.append("?");}sql.append(")");//2.创建操作命令对象ps=c.prepareStatement(sql.toString());for (int i = 0; i <ids.length ; i++) {ps.setInt(i+1,Integer.parseInt(ids[i]));//jdbc设置占位符从1开始}//3.执行sql语句int num=ps.executeUpdate();//返回值是成功插入了几行数据,如果插入不成功也就说不等于1就算失败,这个方法没有实现} catch (Exception e) {throw  new RuntimeException("删除学生信息出错",e);//如果不把这个具体的异常信息传入参数,那么异常信息就会丢失,出现问题时不方便定位} finally {//5.释放资源DBUtil.close(c,ps);//封装得释放资源的方法}}

第二个:插入一个学生信息功能

还是按照我们开发的步骤
前端接口:POST student/add
我们可以看到前端要求我们传来的数据格式应该是json格式。那么什么是json格式?
这是请求关于csdnURL可以看到我们的请求体都是key=value&key=value的形式,而json格式是{“key”:“value”}。所以我们要加一个处理数据格式的json工具类,当然了作为一个比较懒的程序员。jackson框架已经为我提供好了json类,只需要调用方法就可以了。同样的我这里也写成了一个工具类,增加代码复用。

public class JSONUtil {//jackson框架提供处理json类private  static final ObjectMapper M=new ObjectMapper();static {//设置序列化和反序列化的日期格式M.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));}//1.反序列化json字符串为java对象//1.1.因为不能通过http.HttpServletRequest对象.getParameter();获取我们url和请求体,// k1=v1&k2=v2格式的数据。可以通过key获取value。不能使用这种格式是因为json是{"k1":"v1"}的格式。//1.2.getInputStream();获取请求体,作为输入流,不知道流里是什么格式。需要自己编写代码public static <T> T read(InputStream is,Class<T> clazz){//把输入流的数据转为类class中含有属性的json格式。try {return M.readValue(is,clazz);} catch (IOException e) {throw new RuntimeException("JSON反序列化失败,传入的数据格式和class类型不匹配",e);}}//2.序列化java对象为json字符串public static String write(Object o){try {return M.writerWithDefaultPrettyPrinter().writeValueAsString(o);} catch (JsonProcessingException e) {throw new RuntimeException("JSON序列化失败",e);}}
}

需要返回响应:

servlet部分


看见插入学生信息的时候,调用doPost方法,并且需要对数据进行json序列化。于是好像又要弄清楚什么时dopost和什么时doget?

1.它们生成的方式不一样
get方式有三种。1.直接再url地址栏中输入url,2.超链接 3.表单中的方法属性为get时或者默认时(默认也是get)
post只有一种就是表单中方法属性是post。

2.数据传送时,get方式把表单数据存在url后边,也就是get方法提交HTTP时没有请求体。post放表单数据放到请求体中,以实体的方式发送到服务器。

3.服务器获取数据方式,get方法服务器通过request.QueryString方法获取。post方法使用request.Form获取。

4.get方式数据长度有限制,因为是参数传递,并且在地址栏中,一般大小不超过2kb。post方式因为是实体所以适合大规模进行传输。

5.安全性方面,get方式安全性较差,因为直接将数据显示在地址栏中,浏览器有缓冲,可能记录用户信息。post安全性高,因为post提交数据采用HTTP post机制,用户是看不见的。

6.刷新之后get不会有任何提示,而post会询问你是否要重新提交数据。

所以我们通用的总结是:做数据查询时,采用get方式,做数据修改,添加时用post方式。

实际功能页面部分

实际响应

后端插入的JDBC操作其实并没有什么区别

public static void insert(Student s) {//插入接口Connection c=null;PreparedStatement ps=null;//没有结果集try {//1.获取数据库连接c= DBUtil.getConnection();//工具类以及该封装好的方法//sql语句预编译String sql=" insert into  student(student_name,student_no,id_card,student_email,classes_id) values (?,?,?,?,?)";//2.创建操作命令对象ps=c.prepareStatement(sql);ps.setString(1,s.getStudentName());ps.setString(2,s.getStudentNo());ps.setString(3,s.getIdCard());ps.setString(4,s.getStudentEmail());ps.setInt(5,s.getClassesId());//3.执行sql语句int num=ps.executeUpdate();//返回值是成功插入了几行数据,如果插入不成功也就说不等于1就算失败,这个方法没有实现} catch (Exception e) {throw  new RuntimeException("插入学生信息出错",e);//如果不把这个具体的异常信息传入参数,那么异常信息就会丢失,出现问题时不方便定位} finally {//5.释放资源DBUtil.close(c,ps);//封装得释放资源的方法}}

通过这两个功能基本上映射了易班这个项目中所有的功能接口,开发时不仅要实现后端的代码,更重要的是要注意前后端的约定,不论是在接口的约定上,还是在数据格式上这些都是容易出错的地方,一旦出错,往往就有可能导致json异常,或者页面显示那个字段为空。也就是bug,而这类bug往往特别隐蔽,尤其是字段为空bug。别问我是怎么知道的,都是泪啊。

5.项目中的闪光点

1.页面重定向(Filter过滤器)

作为一个管理页面,如果直接访问非登录页面或者资源都有可能导致信息的泄露,不安全事件的发生。那么其实在设计的时候就简单的使用了过滤器,实现了如果访问的是非登录页面那么跳转到登录页面,如果是资源那么返回的是报错信息。比如我在未登录的情况下直接访问http://localhost:8080/sis/student/query?searchText=&sortOrder=asc&pageSize=7&pageNumber=1&_=1597471367451这个接口那么就会有报错信息。

如果是访问静态页面
那么就会跳转到登录页面,因为是动态的所以没办法演示。

过滤器作为javaWeb三大组件之一(Servlet,Filter,Listener),它的主要作用是过滤请求的,而不是处理请求。它就像一个门卫,所有想要进入某个门(对应的路径)的客人(请求servlet),都会被核查身份,如果身份正确正常招待(正常调用servlet方法)。
如果是手动配置,需要在xml文件中添加对应的路径
值得说明的是@WebFilter("/*")代表模糊匹配包下的所有servlet路径。还可以使用注解的形式

再看具体后端实现过程:

@WebFilter("/*")//代表模糊匹配,所有路径都会匹配到
public class LoginFilter  implements Filter {   //继承接口  重写方法@Overridepublic void init(FilterConfig filterConfig) throws ServletException {//初始化生命周期方法}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {//过滤filter的方法实质是客户端请求通过doFilter过滤器,然后才匹配servlet资源,响应也是也通过doFilter过滤器再返回客户端//后端接口:只校验除登录接口外的其他接口,没有登录时不允许访问//前端资源:只校验/public/page/main.html首页,其他都放行,否则出现问题HttpServletRequest req =(HttpServletRequest) servletRequest;HttpServletResponse res=(HttpServletResponse) servletResponse;String url=req.getServletPath();HttpSession session = req.getSession(false);//没有session的时候返回一个nullif (session == null) {//首页重定向到登录页面,如果后端接口,返回错误的json数据// req.setCharacterEncoding("UTF-8");// res.setCharacterEncoding("UTF-8");//首页重定向到登陆页面if ("/public/page/main.html".equals(url)) {res.setContentType("test/html; charset=UTF-8");String schema=req.getScheme();//httpString  host=req.getServerName();//服务器ipint port=req.getServerPort();//端口号String ctx=req.getContextPath();//项目部署路径,应用上下文sis//   res.sendRedirect("public/index.html");  //会出问题???String basePath=schema+"://"+host+":"+port+ctx;res.sendRedirect(basePath+"/public/index.html");return;} else if ((!(url.startsWith("/public/")) && !(url.startsWith("/static/")) && !("/user/login".equals(url)))){//请求后端非登录接口,未登录的请求返回401状态码res.setContentType("application/json");PrintWriter pw=res.getWriter();Response r=new Response();//new 一个我们自己写的Response类错误信息r.setCode("ERR401");r.setMessage("不允许访问");res.setStatus(401);pw.println(JSONUtil.write(r));pw.flush();return;}}filterChain.doFilter(servletRequest,servletResponse);}@Overridepublic void destroy() {//销毁生命周期方法}
}

最后还要对Filter做一些简单说明,首先它是单例的,init()创建方法,在服务器启动的时候就会被执行,也是在服务器关闭时执行destory()销毁方法。每次执行的时候都会执行我们重写的doFilter()方法,filterChain 接口的 doFilter 方法用于通知 Web 容器把请求交给 Filter 链中的下一个 Filter 去处理,如果当前调用此方法的 Filter 对象是Filter 链中的最后一个 Filter,那么将把请求交给目标 Servlet 程序去处理。Filter是责任链设计模式,可以多次过滤,但是说白了就是我们只有这么一次过滤。

2.数据分页

还有一个就是数据分页功能,可以这么说过滤器是web开发中我们现成使用就可以了,但是分页操作需要根据业务的不同来进行设计。为什么要有分页?其实就是如果数据比较多单页面无法显示所有内容,需要多次分页显示。“易班”这个平台设计分页的只有在查询所有学生信息时,需要分页,其他的插入,修改不必要。这个功能本质上基于JDBC 的limit s,n ( 索引位置,数量)。

分页的实现:

实现分页的有两种选择:
(1)一次查询出数据库中的所有记录,然后在每页中显示指定的记录。

(2)对数据库进行多次查询,每次只获得本页的数据并显示

二者效果上没有比较明显的区别,实际上1方案在执行的时候无疑会加大服务器内存的负载,降低速度。

现在一点点来看我们的分页功能是如何实现的,也就是索引位置怎么计算,直接给出结论:索引位置=(当前页码-1)*每页的数量。于是就要思考这个每页数量和当前页码是怎么来的有两种方式:

这里我们设计是采用第一种方式也就是告知我们当前页码和每页的数量。除了这些够吗?显然不仅实现了分页功能还返回给页面了一个业务数据也就是数据总数(不是页码数量而是数据数量)。响应如图

那么我们还要实现这个功能。但是就引入了一个问题(total属性如何设置并返回,下文介绍)那么总结下来和分页业务相关的有两个:
一.实现分页
二.返回总数

我们一步一步来看。
还是先看请求进来之后我们首先干了什么,如果是查询所有学生信息的请求,进入到继承了模板方法AbstractBaseServlet类的子类StudentQueryServlet那么要执行模板方法和重写的process方法,执行process方法后返回了一个结果 p 再返回给模板方法。

模板方法局部代码,模板方法的整个代码我已经再servlet中介绍过。这里挑出来重要的部分。

可见最后的返回值p返回给了Object类型的o中,然后把o添加到Data中。Data经过json工具序列化后作为响应返回。

那么等等!!!

不看这段 《 r.setTotal(ThreadLocalHolder.getTOTAL().get());//不管是否分页操作,都获取当前线程中的Total字段 》代码的前提下,思考total字段应该怎么办呢?返回的是如下图的 List类型的数据,如图

没有total字段,所以一定要想办法加上,但是除了这个请求其他请求又没有total字段,除了对代码进行大幅度修改,这里采用了 ThreadLocal这种方法。继续请求流程。看看Page类中的属性和方法。


好像没有什么特别的地方,除了分页的属性之外,就是从请求中获取对应的参数,该进行类型的转换用Integer转换。再把JDBC处理过程再来出来细看整个流程直到返回查询结果。

public static List<Student> query(Page p) {Connection c=null;PreparedStatement ps=null;ResultSet rs=null;List<Student> list=new ArrayList<>();try {//1.获取数据库连接c= DBUtil.getConnection();//工具类以及该封装好的方法//sql语句预编译StringBuilder sql= new StringBuilder("select s.id," +"       s.student_name," +"       s.student_no," +"       s.id_card," +"       s.student_email," +"       s.classes_id," +"       s.create_time," +"       c.id cid," +"       c.classes_name," +"       c.classes_graduate_year," +"       c.classes_major," +"       c.classes_desc" +"  from student s" +"         join classes c on s.classes_id = c.id");if (p.getSearchText()!=null&&p.getSearchText().trim().length()>0){//条件查询字段不为空并且长度大于0sql.append(" where s.student_name like ?");//模糊查询}if (p.getSortOrder()!=null&&p.getSortOrder().trim().length()>0){String s=p.getSortOrder();sql.append("  order by s.create_time  "+s);//按照什么进行排序,升序asc或者降序desc//不能用占位符操作,字符串替换的时候会带上单引号  也就是order  by  xxx  ‘asc’//但是这种l注方式存在sq入的风险}//1.1  获取查询总数量:使用以上sql可以复用,可以使用子查询StringBuilder countsql=new StringBuilder("select count(0) count from (");countsql.append(sql);countsql.append(")tmp");ps=c.prepareStatement(countsql.toString());//这一步只是获取了数据的总数if (p.getSearchText()!=null&&p.getSearchText().trim().length()>0){ps.setString(1,"%"+p.getSearchText()+"%");//替换占位符,条件复制一下,一定要加模糊匹配的%   而且是包含操作  前后都要加}rs=ps.executeQuery();while (rs.next()){int count=rs.getInt("count");//TODOThreadLocalHolder.getTOTAL().set(count);//设置total变量到当前线程中的ThreadLocalMap数据结构中保存}//1.2  获取分页数据sql.append(" limit ?,?");//在实际应用中,如果查询一张表的所有字段,sql语句也不要写成*,两张表如果有一样的字段,比如上面的id   一定要用别名区别//2.创建操作命令对象ps=c.prepareStatement(sql.toString());int index =1;//为什么要用index?if (p.getSearchText() != null&&p.getSearchText().trim().length()>0){ps.setString(index++,"%"+p.getSearchText()+"%");}ps.setInt(index++,(p.getPageNumber()-1)*p.getPageSize());//设置索引ps.setInt(index++,p.getPageSize());//3.执行查询操作rs=ps.executeQuery();//4.处理查询结果集while (rs.next()){Student student=new Student();//设置属性,通过结果集获取来设置student.setId(rs.getInt("id"));student.setStudentName(rs.getString("student_name"));student.setStudentNo(rs.getString("student_no"));student.setIdCard(rs.getString("id_card"));student.setStudentEmail(rs.getString("student_email"));student.setClassesId(rs.getInt("classes_id"));student.setCreateTime(new Date(rs.getTimestamp("create_time").getTime()));Classes  classes = new Classes();student.setClasses(classes);classes.setId(rs.getInt("cid"));classes.setClassesName(rs.getString("classes_name"));classes.setClassesGraduateYear(rs.getString("classes_graduate_year"));classes.setClassesMajor(rs.getString("classes_major"));classes.setClassesDesc(rs.getString("classes_desc"));// classes.setCreateTime(new Date(rs.getTimestamp("cct").getTime()));list.add(student);}return list;} catch (Exception e) {throw  new RuntimeException("查询学生列表出错",e);//如果不把这个具体的异常信息传入参数,那么异常信息就会丢失,出现问题时不方便定位} finally {//5.释放资源DBUtil.close(c,ps,rs);//封装得释放资源的方法}}

一、获取总数
在 //1.获取数据库连接部分,是常规的sql语句的预编译。

这里要用 StringBuilder对sql语句实现拼接,也就是获得sql语句查询的条件,条件的来源是页面请求的数据
比如这里的 升序,每页数量为7, 当前页码为1

再就是如何获取总量的问题,在查询分页数据的同时获取总量显然是不对的,因为limit会限制数据个数,所以要把查询总量放在查询分页之前。
这里的代码除了查询总量sql的拼接,值得一提的是

意义就是把查询出来重命名的count数量 获取并设置到当前的线程中保存

详细代码;
ThreadLocal并不是直译的本地线程,更直接来说ThreadLocal并不是一个线程,一个线程的局部变量。使用ThreadLocal维护变量时,ThreadLocal会给每个使用该变量的线程一个独立的变量副本,每个线程可以独立使用自己的副本,而不影响别的线程。说白了就是把count这个变量绑定到使用它的这个线程上,这个线程的生命周期内都可以获取这个变量,其他线程不能获取。经过这么操作之后,当请求获取total字段时,如果不是查询所有学生信息接口,那么调用的total信息就是null,而查询学生信息接口返回的就是对应的查询出来的total值。

PS:一定要在线程结束前,删除这个变量,否则造成内存泄漏,而且要在Response中添加total属性。

二、分页

在查询sql执行完毕处理完结果后,就是分页操作。值得强调的也就使用index的原因?
可以看到这两条if语句,也就是说查询条件和排序条件并不是必要的,如果没有,在设置占位符就可能出现问题。比如说索引在设置占位符的时候没有查询条件和排序条件那么设置占位符的位置就时1,2.如果有就是3,4。所以index自增就可以解决。

6.总结和展望

1.总结

“易班”项目,是我模仿真正项目开发步骤,实现的一个小项目,可能从业务来说并不算是很复杂,说白了就是数据库的增删查改。但是从中重点学习到了一个项目从构想到落地的过程,可能缺少了前端的设计,这也是我需要进步的。还有就是如何去灵活的使用学到的方法,现在所学的都是基础,可能后期应用到各种框架,可能代码连个main函数都没有,能理解自己到底为什么这么用?怎么用?出现bug在哪?怎么改?

2.展望

总的来说并没有比较漂亮的完成了这个项目,很多功能还没有实现,只是提出了一种预想和解决办法,比如班级管理和用户管理虽然和学生管理功能类似,但是每个功能都有自己需要注意的地方,这是未完成的,完成了的功能不完美的包括sql注入方面,都没有做的很好,这也是下一步需要改进的方向,总体下来感觉自己确实有所收获,也希望自己能够继续进步,加油。

7.开源代码

GitHub源码地址:https://github.com/XiaoYao-code/stu-info-sys.

“易班”学生管理平台小项目 万字总结 泪目!!!相关推荐

  1. XXX管理平台系统——项目总结(over)

    XXX 管理平台项目总结 前言 经过前后一年的工作,项目总算进入付款流程了,项目也总算告以段落了,二期我是不会继续再主导了,毕竟家庭才是第一位的,否则违背了我回来的初衷. 不 知道算是成功还是失败,从 ...

  2. XXX管理平台系统——项目教训

    XXX管理平台系统项目教训 前言 闲来无事聊一下自己的教训吧,经验也是在教训中不断成长的. 技术 方面 之前对硬件和网络缺乏基本的选型概念,以及对整个系统的整体和技术方案把握有所欠缺,导致整个系统架构 ...

  3. java校园足球管理系统_基于jsp的校园足球管理平台-JavaEE实现校园足球管理平台 - java项目源码...

    基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的校园足球管理平台, 该项目可用各类java课程设计大作业中, 校园足球管理平台的系统架构分为前后台两部分, 最终实 ...

  4. JavaWeb购物平台小项目

    JavaWeb购物平台小项目 最近也快到期末了,web程序设计的课程也迎来了第一次大作业,内容是要做一个购物平台,用户能够添加购物车,购买商品,留言,反馈等,而管理员可以添加商品,浏览反馈,查看账务等 ...

  5. 计算机实战项目、毕业设计、课程设计之 含论文+辩论PPT+源码等]微信小程序ssm竞赛管理平台小程序+后台管理系统

    <微信小程序竞赛管理平台+后台管理系统|前后分离VUE>该项目含有源码.论文等资料.配套开发软件.软件安装教程.项目发布教程等 本系统包含微信小程序前台和Java做的后台管理系统,该后台采 ...

  6. 自助任务平台小项目感想

    这周一直在做一个项目:某某平台.老板从某个站上看到的.然后就决定抄这个项目.主要想用在某某电商身上.想服务于那些商家的淘宝刷单. 在这个项目中,又接触到一点关于产品的东西--实名认证方式: 互联网常见 ...

  7. 学校借力OA系统,构建学生信息一体化管理平台

    对大型教育集团而言,旗下有多所不同类型的学校,管理模式.收费标准不同,需要通过信息化手段帮助管理者解决各类管理难题. 为了规范学生管理.推动学校各部门协同管理,泛微以内外协同的办公平台为基础,为学校搭 ...

  8. 易班总是显示服务器出错,易班

    站长1名 工作职责:按照新媒体与易班建设科要求,在指导老师的引导下全面统筹负责工作站的建设和管理工作. 常务副站长1名 工作职责:分管综合办公部.宣传策划部.网络技术部,重点做好内部建设.功能开发.项 ...

  9. 基于 Java Web 的毕业设计选题管理平台--选题报告与需求规格说明书

    一.选题报告 1.团队名称---指南者团队 2.团队成员: 孔潭活:2015034643032 何德新:2015034643017 吴淑瑶:2015034643018 苏咏梅:201503464302 ...

  10. 【计算机毕业设计】民宿管理平台

    一.系统截图(需要演示视频可以私聊) 摘  要 随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势:对于民宿管理平台系统当然也不能排除在外,随着网络技术的不断成熟, ...

最新文章

  1. Linux命令:MySQL系列之五--SELECT单表查询、多表查询升级及删除,插入
  2. mybatis实现批量更新
  3. JS垃圾回收——和其他语言一样,JavaScript 的 GC 策略也无法避免一个问题:GC 时,停止响应其他操作,这是为了安全考虑...
  4. XMemcached使用示例--转
  5. CentOS SSH安装与配置
  6. 修改EIGRP 路径cost 值,以及分析和实现等价与非等价负载均衡
  7. 内向的人可以做产品经理吗?
  8. c语言中0xof423什么意思,C语言指针 百思不得其解的一个问题
  9. 冯偌依曼计算机的基本原理是,03级计算机专《计算机组成原理》试卷A.doc
  10. python api调用 验证码_Python调用创蓝253短信验证码API文档
  11. 世界互联网大会:华三发安全平台天机
  12. python中使用Opencv进行人脸检测
  13. 路径规划之 A* 算法 1
  14. 简单的 OpenGL 纹理贴图不起作用?
  15. Java JavaEE JavaSE JavaME JavaWEB 之间的区别与联系
  16. cad查看_CAD查看:Geometric Glovius Pro v5.1.0
  17. 环形电流计算公式_辨析!环形差模电感饱和电流的计算公式是什么?
  18. Android开发中的Java包的定义
  19. 英语 —— 辅音浊化
  20. 微星B450mMortar迫击炮+AMD速龙3000GE安装Windows7 SP1并使用UEFI+Nvme启动

热门文章

  1. html5绘制圆形,Canvas绘制圆形
  2. 如何在苹果手机上进行自动化测试!!!!!可以尝试配置一下
  3. 思科模拟器路由表怎么看_Cisco路由配置教程 Cisco路由器静态路由与默认路由的配置方法图解...
  4. matlab simulink仿真实现电力电子的整流电路
  5. “离职同事在工作群抢红包被踢”:学会退群,是职场人的基本修养
  6. 量子纠缠计算机里的灵魂意识,量子纠缠理论告诉我们,灵魂真的存在于你我身体?...
  7. labview编程小技巧
  8. Python之温度换算
  9. 配置vscode解决code runner乱码
  10. 怎么设置代理服务器IP上网