前言
快一周了,第二篇博客才写完。写第一篇的时候感觉挺麻烦、挺累的,因为总想把细节讲清楚。在写第二篇博文的时候,才慢慢觉得没有那么累了,我想,以后会习惯上写博客了吧。
这一篇讲Java后台,先介绍整体的class文件结构吧。
本篇讲用户,所以我们先只介绍user部分。后台的代码分层开发,运行需要经过Servlet控制层、Service业务层、Dao持久层,所以我们也按这个顺序讲解。
JavaBean
先介绍JavaBean吧。User很简单,只有四个属性,分别对应用户Id、用户名、用户密码、用户入学年份。
package com.rclv.domain;
public class User {
private String uid; //用户Id
private String uname; //用户名
private String ugrade; //用户入学年份
private String upassword; //用户密码
// 省略get、set方法
}
Servlet
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。
Servlet主要完成以下任务:读取客户端(浏览器)发送的显式的数据、隐式的 HTTP 请求数据;处理数据并生成结果,这个过程可能需要访问数据库;发送显式的数据、隐式的 HTTP 响应到客户端(浏览器)。
简单地说,在此项目中,Servlet的作用是:接受Web浏览器的请求,并可能访问数据库处理数据,最终发送响应到浏览器端。上一篇中head.jsp中的库存链接就是一个用Servlet处理的浏览器请求。
Servlet生命周期:首先调用 init () 方法进行初始化,然后调用service() 方法来处理客户端的请求,最后调用 destroy() 方法终止(结束),并由JVM 的垃圾回收器回收。
package com.rclv.test;
public class ServletTest extends HttpServlet{
// 1.Servlet初始化
public void init(ServletConfig config) throws ServletException {
}
// 2.1调用service方法,若service没有被重写,则service会把请求转发到doGet或doPost方法;若service被重写,则不进行转发,并完成service方法。
public void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
}
// 2.2doGet方法。
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
// 2.3doPost方法
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
}
// 3.Servlet销毁
public void destroy() {
}
}
service()方法是Servlet的核心。每当客户端发出请求时,Servlet对象的service() 方法就要被调用,并递给Servlet一个ServletRequest对象和一个ServletResponse对象作为service()方法的参数。
默认情况下,service()方法转发http请求到doGet或doPost方法中。如果重写了该方法,则默认操作被覆盖,不再进行转发操作,而是执行service()方法中的内容。
所以一般情况下,需要重写doGet和doPost方法。但是这样一个Servlet类只能处理一个请求,而且每次都要重写doGet和doPost方法,如果一个项目需要很多请求方法,就会非常麻烦。而实际开发中,我们在完成某一类功能时,如果让一个Servlet可以处理多个请求方法,就会方便很多,提高了代码的复用性。
BaseServlet
前面说过,重写service()方法则不再进行转发操作,Servlet直接执行该方法体内的代码。所以可以重写service()方法,来提高代码的复用性。下面我们通过重写HttpServlet继承类的service()方法,并利用反射机制实现对多种方法的调用。
每个继承HttpServlet的类都会继承其service()方法,所以我们需要抽取一个继承HttpServlet类的基类BaseServlet,重写该类的service()方法即可。前面展示的文件结构中,总共有四个Servlet类,其他三个则都是继承BaseServlet的应用类。
package com.rclv.web.servlet;
/**
* 通用的servlet
*/
public class BaseServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
//1.获取子类 创建子类或者调用子类的时候 this代表的是子类对象
@SuppressWarnings("rawtypes")
Class clazz = this.getClass();
// 2.获取请求的方法
String m = request.getParameter("method");
// 3.获取方法对象
Method method = clazz.getMethod(m, HttpServletRequest.class, HttpServletResponse.class);
// 4.方法执行,返回值为请求转发的路径
String s = (String) method.invoke(this, request, response);
// 5.判断s是否为空,否则完成请求转发
if(s!=null) {
request.getRequestDispatcher(s).forward(request, response);
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException();
}
}
}
新建BaseServlet类,继承HttpServlet,重写service方法。
第一步。try-catch语句捕获异常,通过this.getClass()获取当前运行时类的Class对象。我们应用BaseServlet是用其子类,所以此Class对象将是BaseServlet子类的Class对象。
此篇讲用户,UserServlet是主要的Servlet,它继承了BaseServlet。所以,当运行UserServlet时,this.getClass()返回的就是UserServlet类的Class对象。下面对于BaseServlet的代码说明,我们都将以其子类UserServlet的视角讲解。
第二步。获取前台传来的method属性,即获取将要执行的UserServlet中的方法。
第三步。获取方法对象,即获取UserServlet中将要执行的方法对象。class.getMethod()方法的第一个参数是要获取的方法的方法名,后面的参数是该方法的形参类型。
第四步。方法执行,并返回一个String类型的返回值。method.invoke()方法的第一个参数为当前类UserServlet的实例,后面两个参数对应执行方法中的参数。
第五步。请求转发。第四步中的返回值,作为请求转发的路径,此路径是相对路径即可。因为在getRequestDispatcher(url)方法中封装了ServletContext.getRealPath()以获得相应的项目根路径,再通过字符串相加,从而可以获得一个完整的路径。
UserServlet
UserServlet需要完成三个功能,用户注册、用户登陆、用户退出。UserServlet的访问路径为/user。
(1)用户注册
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=loginUI">登录</a></li>
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=registUI">注册</a></li>
看前台的注册链接,是一个UserServlet的访问,并带了一个method=registUI键值。根据前面讲的BaseServlet类中service()方法对子类方法的获取和执行,被访问的UserServlet将执行registUI方法。
registUI方法只有一个返回值,为转发路径。之后将执行service()中的请求转发功能,跳转到/jsp/register.jsp页面。
<!-- 注册表单 -->
<form id="formId" action="${pageContext.request.contextPath }/user?method=regist" method="post">
<table>
<tr>
<th>用户名:</th>
<th><input type="text" name="uname"/></th>
</tr>
<tr>
<th>密码:</th>
<th><input type="password" name="upassword"/></th>
</tr>
<tr>
<th>入学年份:</th>
<th><input type="text" name="ugrade"/></th>
</tr>
<tr>
<th></th>
<th align="right"><input type="submit" value="提交"/></th>
</tr>
</table>
</form>
register.jsp页面主要是个简单的注册表单,需要填写用户名、用户密码、入学年份。表单的action发送路径仍是UserServlet请求,提交方式为post,提交表单之后,将会访问UserServlet类,并执行其中的regist方法。
public String regist(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1.新建一个User对象,用BeanUtils.populate()方法,将request.getParameterMap()从注册表单获得的 所有key-value的值映射到user中对应的属性。
User user = new User();
BeanUtils.populate(user, request.getParameterMap());
// 2.用UUIDUtils.getId()方法给user设置uid属性,用MD5Utils.md5()方法加密user的upassword属性。
user.setUid(UUIDUtils.getId());
user.setUpassword(MD5Utils.md5(user.getUpassword()));
// 3.用BeanFactory.getBean()获取一个UserService对象us,并调用us.regist(user)方法进入service层,参数为user。
UserService us = (UserService) BeanFactory.getBean("UserService");
us.regist(user);
// 4.设置request域中的msg属性值,返回请求转发路径,完成注册功能。
request.setAttribute("msg", "注册成功");
return "/jsp/msg.jsp";
}
regist方法。
第一步。新建一个User对象,用BeanUtils.populate()方法,将request.getParameterMap()从注册表单获得的 所有key-value的值映射到user中对应的属性。
第二步。用UUIDUtils.getId()方法给user设置uid属性。用MD5Utils.md5()方法加密user的upassword属性。
package com.rclv.utils;
import java.util.UUID;
public class UUIDUtils {
/**
* 随机生成id
* @return
*/
public static String getId(){
return UUID.randomUUID().toString().replace("-", "").toUpperCase();
}
}
package com.rclv.utils;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Utils {
public static String md5(String plainText) {
byte[] secretBytes = null;
try {
secretBytes = MessageDigest.getInstance("md5").digest(
plainText.getBytes());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("找不到md5算法");
}
String md5code = new BigInteger(1, secretBytes).toString(16);
for (int i = 0; i < 32 - md5code.length(); i++) {
md5code = "0" + md5code;
}
return md5code;
}
}
第三步。用BeanFactory.getBean()获取一个UserService对象us,并调用us.regist(user)方法进入service层,参数为user。
第四步。设置request域中的msg属性值,返回请求转发路径,完成注册功能。
BeanFactory
在说service层之前,我们先来讲一下BeanFactory。
public class BeanFactory {
public static Object getBean(String id) {
try {
// 1.通过类加载器获取ClassPath下的beans.xml文件资源,并用SAXReader解析该文件,返回一个Document类型的对象。
Document doc = new SAXReader().read(BeanFactory.class.getClassLoader().getResourceAsStream("beans.xml"));
// 2.通过Xpath语法选取节点元素返回一个Element类型的元素对象。selectNodes()方法的参数格式:节点名[@属性名='属性值'],//代表文档中所有匹配选择的当前节点。
Element ele = (Element) doc.selectSingleNode("//bean[@id='"+id+"']");
// 3.返回元素属性值
String value = ele.attributeValue("class");
// 4.通过反射机制返回一个value对应的类实例。
return Class.forName(value).newInstance();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
第一步。通过类加载器获取ClassPath路径下的beans.xml文件资源,并用SAXReader解析该文件,返回一个Document类型的对象。
第二步。通过Xpath语法选取节点元素返回一个Element类型的元素对象。selectNodes()方法的参数格式:节点名[@属性名='属性值']。
第三部。返回元素的class属性值。
第四步。通过反射机制,用Class.forName(value).newInstance()方法返回一个value对应的类实例。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean id="AgentsService" class="com.rclv.service.impl.AgentsServiceImpl"/>
<bean id="UserService" class="com.rclv.service.impl.UserServiceImpl"/>
<bean id="AgentsDao" class="com.rclv.dao.impl.AgentsDaoImpl"/>
<bean id="UserDao" class="com.rclv.dao.impl.UserDaoImpl"/>
</beans>
从beans.xml中可以看到,bean标签中class属性对应的都是具体的接口实现类,id属性被用来查询获取元素节点。
我们再回到UserServlet中。
UserService us = (UserService) BeanFactory.getBean("UserService");
us.regist(user);
BeanFactory.getBean(“UserService”)此时传入的参数是UserService,此时将返回beans.xml中对应的UserServiceImpl的接口实现类。则UserService接口类创建的实例us就是该接口的实现类,返回类型需要强转为UserService类。
这种方法就是面向切面编程的思想。在开发中,如果UserService有多个实现类,并且在多个类中都创建了UserService的某一个实现类,当项目有需求需要替换该实现类时,就需要在每个调用类中都修改相应代码,就会非常麻烦。而如果用BeanFactory这种工厂模式编程的话,只需要修改当前BeanFactory.getBean("")方法中的参数在bean.xml中对应的class属性即可。修改之后,该方法将返回当前接口的其他实现类。
UserService
UserService是接口类,UserServiceImpl是其实现类。
package com.rclv.service;
import com.rclv.domain.User;
public interface UserService {
void regist(User user) throws Exception;
User login(String uname, String upassword) throws Exception;
}
package com.rclv.service.impl;
import com.rclv.dao.UserDao;
import com.rclv.domain.User;
import com.rclv.service.UserService;
import com.rclv.utils.BeanFactory;
public class UserServiceImpl implements UserService {
@Override
public void regist(User user) throws Exception {
UserDao ud = (UserDao) BeanFactory.getBean("UserDao");
ud.regist(user);
}
@Override
public User login(String uname, String upassword) throws Exception {
UserDao ud = (UserDao) BeanFactory.getBean("UserDao");
return ud.getByNameAndPassword(uname, upassword);
}
}
继续看注册方法吧。Service层的代码很简单,先由BeanFactory.getBean(“Dao”)方法返回一个UserDao的实现类ud,然后调用ud.regist(user)方法进入dao层,参数为上一层传来的user。
这里看似service层没有完成什么功能,只是返回了一个UserDao实现类ud,并调用ud的方法进入了dao层。其实是因为我们这里需要完成的功能比较简单,service层是用来完成业务逻辑的,带些逻辑的比如,通过验证用户名是否存在来决定是否调用dao层方法来完成数据库查询操作。
在下一篇的药品管理AgentsService模块中,将会出现在一个方法内调用AgentsDao层两个不同的方法完成不同的数据库操作,从而体现service层的作用。
UserDao
最后是持久层。
package com.rclv.dao;
import com.rclv.domain.User;
public interface UserDao {
void regist(User user) throws Exception;
User getByNameAndPassword(String uname, String upassword) throws Exception;
}
package com.rclv.dao.impl;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import com.rclv.dao.UserDao;
import com.rclv.domain.User;
import com.rclv.utils.DataSourceUtils;
public class UserDaoImpl implements UserDao {
// 数据库添加user
@Override
public void regist(User user) throws Exception {
// 1.Dbutils工具类操作数据库,c3p0连接池作为queryrunner构造器参数。
QueryRunner qr = new QueryRunner(DataSourceUtils.getDataSource());
// 2.设置sql查询语句。
String sql = "insert into user values(?,?,?,?)";
// 3.用qr.update()方法完成数据库添加操作,第一个参数为sql,后面几个参数为user表中要添加的字段值,分别对应sql中的占位符。
qr.update(sql, user.getUid(), user.getUname(), user.getUgrade(),
user.getUpassword());
}
// 数据库查询user
@Override
public User getByNameAndPassword(String uname, String upassword) throws Exception {
QueryRunner qr = new QueryRunner(DataSourceUtils.getDataSource());
String sql = "select * from user where uname = ? and upassword = ? limit 1";
return qr.query(sql, new BeanHandler<>(User.class), uname, upassword);
}
}
UserDao是接口,UserDaoImpl是其实现类。
regist方法中,用Dbutils工具类结合c3p0连接池完成数据库操作。
第一步。用构造器创建QueryRunner类对象qr,由DataSourceUtils返回的c3p0数据库连接池做参数,实现对数据库的链接。
第二步。设置sql查询语句。
第三步。用qr.update()方法完成数据库添加操作,第一个参数为sql,后面几个参数为user表中要添加的字段值,分别对应sql中的占位符。
至此,完成数据库添加操作,完成用户注册功能。
c3p0数据库连接池
c3p0是一种JDBC数据库连接池。使用连接池的原因是,数据库的每次使用都需要发送请求、验证用户,执行完数据库操作再断开连接,这样非常消耗时间和资源。而数据库连接池是在系统初始化的时候,就建立好的空闲连接;当需要进行数据库连接的时候,只需从连接池中取出一个连接对象,使用完之后,不用关闭连接,而是将其放回连接池中。
数据库连接池使得资源可重复使用,减少了系统的相应时间,提高了应用程序的性能。
c3p0的配置非常简单。在src目录下创建一个c3p0.properties文件,配置内容如下。
c3p0.driverClass=com.mysql.jdbc.Driver
c3p0.jdbcUrl=jdbc:mysql://localhost:3306/cupboard
c3p0.user=root
c3p0.password=root
(2)用户登陆
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=loginUI">登录</a></li>
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=registUI">注册</a></li>
和用户注册一样,前台登陆链接访问UserServlet的loginUI方法,返回请求转发路径,跳转到login.jsp登陆页面。
public String registUI(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return "/jsp/register.jsp";
}
<!-- 登录表单 -->
<form id="formId" action="${pageContext.request.contextPath }/user?method=login" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="uname"/></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="upassword"/></td>
</tr>
<tr>
<td colspan="2" align="center"><input type="submit" value="提交" /></td>
</tr>
</table>
</form>
login.jsp是一个非常简单的登陆表单,action发送请求访问UserServlet类的login方法。
public String login(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1.获取前台参数,并对upssword进行加密
String uname = request.getParameter("uname");
String upassword = request.getParameter("upassword");
upassword = MD5Utils.md5(upassword);
// 2.返回一个UserService实例,并调用login方法进入service层
UserService us = (UserService) BeanFactory.getBean("UserService");
User user = us.login(uname, upassword);
// 3.判断user是否查询返回成功。若不成功,则重新跳转到login.jsp登陆页面
if (user == null) {
request.setAttribute("msg", "登录失败");
return "/jsp/login.jsp";
}
// 4.若user返回成功,则在session域中添加user对象,并重定向到首页。
request.getSession().setAttribute("user", user);
response.sendRedirect(request.getContextPath() + "/");
return null;
}
控制层:login(HttpServletRequest request, HttpServletResponse response)方法。
第一步。用request.getParameter()方法获得前台传来的uname和upassword属性,并对upassword进行md5加密。
第二步。调用BeanFactory.getBean("UserService")方法返回UserService实现类us,并调用us.login(uname, upassword)方法进入service层,完成查询业务后返回一个user对象。
第三步。判断user是否返回成功。若user==null返回不成功,则设置request域的msg属性,并return转发路径,重新跳转到登陆页面。
第四步。user返回成功,则将该user对象设置给session域的user属性,并用response.sendRedirect(request.getContextPath() + "/")方法重定向到首页。request.getContextPath()获得的是项目的根路径,则根据web.xml中的<welcome-file-list>配置
默认打开首页。
业务层:User UserService.login(String uname, String upassword)方法。
和之前一样,BeanFactory工厂返回一个UserDao对象ud,ud调用ud.getByNameAndPassword(uname, upassword)方法进入dao层。
持久层:User getByNameAndPassword(String uname, String upassword)方法。
前两步。Dbutils连接数据库,创建QueryRunner对象qr,创建查询语句sql,条件查询user表中uname、upassword和方法传入参数都对应相同的一个数据。
第三步。执行query查询语句,封装结果集参数 new BeanHandler<>(User.class)表示查询返回单个JavaBean(User)对象。
至此,完成用户登录功能。
我们可以再回到前端head.jsp。
<!-- 登录和注册 -->
<c:if test="${empty user }">
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=loginUI">登录</a></li>
<li style="position: relative; left:70%"><a href="${pageContext.request.contextPath }/user?method=registUI">注册</a></li>
</c:if>
<!-- 用户名和退出登录 -->
<c:if test="${not empty user }">
<li style="position: relative; left:68%"><a href="javascript:void(0);">${user.uname }_${user.ugrade }:您好</a></li>
<li style="position: relative; left:68%"><a href="${pageContext.request.contextPath }/user?method=logout">退出</a></li>
</c:if>
如果后台登陆成功,就会返回一个session域的user对象。head.jsp中会判断当前页面中是否有user对象,若有则El显示用户名和用户入学年份;若没有则显示登陆按钮。
(1)用户退出
public String logout(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 使用request。getSession().invalidate()方法销毁当前session域中的所有属性
request.getSession().invalidate();
response.sendRedirect(request.getContextPath());
return null;
}
用户退出的功能就更简单了,登陆是往session域中添加一个user对象,退出则是删除这个user对象。使用getSession().invalidate()方法,可以销毁当前session域中的所有属性,达到删除session中user对象的作用。
销毁session之后,重定向返回首页,完成用户退出功能。