目录
前言
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
无框架开发后台服务,经历造轮子的痛苦
通过《JavaEE--从文件上传、下载入门Java web》入门,依赖《JavaEE--Java Web基础知识》完成Java web知识的学习。现在我们来实战一下,尝试不依赖任何框架,从零实现一个后台服务,经历一番纯手工造轮子的痛苦。
需求梳理:
- 带验证码的用户登录;
- 实现对用户信息的增删改查;
- 实现分页查询和条件查询;
- 实现模拟银行账户转账操作;
- 实现调用接口返回页面以及返回对象信息;
需求分析:
- 带验证码的用户登录
需要实现全局拦截,用户登录才放行,未登录则跳转login.jsp;可选实现方案:使用Filter拦截器,使用forword转发请求(想在请求中存入提示信息,所以使用转发请求,不用重定向);
验证码功能:浏览器端请求CheckCodeServlet,获取验证码,CheckCodeServlet将生成的验证码存入session;用户登录时携带验证码参数,后台校验是否和session中一致,一致则通过,同时立即清除session中的验证码,保持验证码的刷新。可选实现方案:使用session实现数据存储(用户会话技术,不用担心存在多用户互相影响的问题)
- 实现对用户信息的增删改查
用户信息保存在数据库中(Mysql8.0),用户信息的增删改查,就是对数据库用户信息表的增删改查;可选实现方案:java 操作数据库,jdbc
- 实现分页查询和条件查询
获取所有用户信息,列表展示;要求分页,否则一次加载数据过多,可能导致前端页面内存溢出,也影响后台接口执行效率;支持按照某些字段查询,条件查询;可选方案:jdbc
- 实现模拟银行账户转账操作
模拟银行两个账户之间的转账,A账户向B账户转账100元,那A账户就减少100元,B账户增加100元。要求不管发生什么异常,都不能存在金额问题(要么转账成功,要么转账失败,两个账户的金额要始终正确)。可选方案:这个需求存在多次操作数据库,为了确保发生异常的时候可以恢复,必须使用事务,利用事务回滚
- 实现调用接口返回页面以及返回对象信息
浏览器访问servlet后,servlet根据处理结果返回对象信息或者返回一个html页面;可选实现方案:servlet返回具体对象信息,可以通过response写回即可;servlet返回html页面,有两个方案:1.文件下载(读取html文件流,写回浏览器)2.转发/重定向请求到具体的html页面
编码实现:
按照上面的需求分析,我们来一一编码实现:
使用Session存储验证码,保证一次会话范围内共享:
/**
* 绘制验证码,生成验证码图片jpg,并返回给浏览器
* 绘制完成,将验证码存入session,key:checkcode_session
*/
@WebServlet("/CheckCodeServlet")
public class CheckCodeServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//暂存随机验证码字符
StringBuilder strTemp = new StringBuilder("");
int width = 80;
int height = 30;
//1.创建图片对象
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
//2.图片内容填充,绘制二维码
Graphics g = image.getGraphics();
//填充背景色
g.setColor(Color.pink);
g.fillRect(0,0,width,height);
//画边框
g.setColor(Color.blue);
g.drawRect(0,0,width-1,height-1);
//画验证码
String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i =0;i<4;i++){
String checkCodeChar = str.charAt(new Random().nextInt(str.length()))+"";
//验证码字符
strTemp.append(checkCodeChar);
g.drawString(checkCodeChar,width/5*(i+1),height/2);
}
//将验证码存储到session中
request.getSession().setAttribute("checkcode_session",strTemp.toString());
//画干扰线
g.setColor(Color.green);
Random random = new Random();
for (int i=0;i<6;i++){
g.drawLine(random.nextInt(width),random.nextInt(height),random.nextInt(width),random.nextInt(height));
}
//3.输出图片
ImageIO.write(image,"jpg",response.getOutputStream());
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
}
使用Filter拦截器实现全局拦截
/**
* 全局登录拦截器(路径 /* 匹配所有路径)
* 拦截除静态资源、login.jsp、login(LoginServlet)以及验证码CheckCodeServlet之外的所有请求
* 验证登录信息,登录过则放行,否则转发请求到login.jsp,让用户先登录
*/
@WebFilter("/*")
public class LoginFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
//强制类型转换,为了拿到uri/url
HttpServletRequest request = (HttpServletRequest) req;
//获取资源路径
String uri = request.getRequestURI();
//判断是否包含登陆相关资源路径
if (uri.contains("/login.jsp")||uri.contains("login") || uri.contains("/css/")
|| uri.contains("/js/") || uri.contains("/fonts/") || uri.contains("CheckCodeServlet")){
//放行
chain.doFilter(req, resp);
} else {
//不包含,则验证用户是否已经登录
//注意与LoginServlet中保存的session一致
Object user = request.getSession().getAttribute("user");
if (user != null){
//说明登陆过了,放行
chain.doFilter(req, resp);
} else {
//没有登陆就试图访问系统,拦截,并跳转登录页面
request.setAttribute("msg_error","您尚未登陆,请先登录");
request.getRequestDispatcher("/login.jsp").forward(request,resp);
}
}
}
@Override
public void destroy() {}
}
JDBC增删改查、分页、条件查询
/**
* 数据库工具类
*/
public class JDBCUtil {
private static final String driverClassName = "com.mysql.cj.jdbc.Driver";
private static final String url = "jdbc:mysql://127.0.0.1:3306/mydb?useSSL=true&characterEncoding=utf-8&serverTimezone=GMT";
private static final String username = "root";
private static final String password = "***";
static {
try {
Class.forName(driverClassName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection(){
Connection connection = null;
try {
connection = DriverManager.getConnection(url,username,password);
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
public static void release(Connection conn, Statement stat, ResultSet rs){
try {
if (null != rs){
rs.close();
}
if (null != stat){
stat.close();
}
if (null != conn){
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 用户信息实体类,对应数据库表user_info
*/
public class UserInfo implements Serializable {
private static final long serialVersionUID = -6620605799090612569L;
private Integer id;
private String username;
private String password;
private String address;
private Integer age;
//省略getter和setter
}
/**
* 用户信息表数据库操作类
* 1.根据主键id查询
* 2.保存用户信息
* 3.根据主键id删除用户
* 4.根据主键id修改用户密码
* 5.分页查询+条件查询
*/
public class UserInfoDao {
/**
* 根据主键id查询
* @param id
* @return
*/
public UserInfo queryByPrimarykey(Integer id){
Connection connection = JDBCUtil.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
String sql = "select * from user_info where id = ?";
try {
ps = connection.prepareStatement(sql);
ps.setInt(1,id);
rs = ps.executeQuery();
if (rs.next()){
UserInfo userInfo = new UserInfo();
userInfo.setUsername(rs.getString("username"));
userInfo.setPassword(rs.getString("password"));
return userInfo;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(connection,ps,rs);
}
return null;
}
/**
* 保存用户
* @param userInfo
* @return
*/
public boolean save(UserInfo userInfo){
Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = null;
boolean result = false;
String sql = "INSERT INTO user_info (username,password) values (?,?)";
try {
ps = conn.prepareStatement(sql);
ps.setString(1,userInfo.getUsername());
ps.setString(2,userInfo.getPassword());
result = ps.execute();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(conn,ps,null);
}
return result;
}
/**
* 删除用户
* @param id
* @return
*/
public boolean delete(Integer id){
Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = null;
boolean result = false;
String sql = "delete from user_info where id = ?";
try {
ps = conn.prepareStatement(sql);
ps.setInt(1,id);
result = ps.execute();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(conn,ps,null);
}
return result;
}
/**
* 更新用户密码
* @param id
* @param password
* @return
*/
public int update(Integer id,String password){
Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = null;
int count = 0;
String sql = "update user_info set password = ? where id = ?";
try {
ps = conn.prepareStatement(sql);
ps.setString(1,password);
ps.setInt(2,id);
count = ps.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(conn,ps,null);
}
return count;
}
/**
* 根据用户名和密码查询,用于账户登录
* @param username
* @param password
* @return
*/
public UserInfo queryByUsernameAndPassword(String username,String password){
Connection connection = JDBCUtil.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
String sql = "select * from user_info where username = ? and password = ?";
try {
ps = connection.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
rs = ps.executeQuery();
if (rs.next()) {
UserInfo userInfo = new UserInfo();
userInfo.setUsername(username);
userInfo.setPassword(password);
return userInfo;
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(connection,ps,rs);
}
return null;
}
/**
* 分页查询,支持条件查询
* @param _currentPage
* @param _rows
* @param conditions
* @return
*/
public PageBean<UserInfo> findUserByPage(String _currentPage, String _rows, Map<String, String[]> conditions) {
int currentPage = Integer.parseInt(_currentPage);
int rows = Integer.parseInt(_rows);
int totalCount = findTotalCount(conditions);
//分页数
int totalPage = (totalCount % rows == 0) ? (totalCount / rows) : (totalCount / rows + 1);
//如果当前页《=0,则认为是第一页
if (currentPage<=0){
currentPage = 1;
}
//如果当前页》= totalpage,则认为是最后一页
if (currentPage >= totalPage){
currentPage = totalPage;
}
PageBean<UserInfo> pb = new PageBean<>();
pb.setCurrentPage(currentPage);
pb.setRows(rows);
pb.setTotalCount(totalCount);
pb.setTotalPage(totalPage);
int start = (currentPage-1)*rows;
pb.setUsers(queryUserByPage(start,rows,conditions));
return pb;
}
/**
* 条件查询,分页查询合并
* @param start
* @param rows
* @param conditions
* @return
*/
private List<UserInfo> queryUserByPage(int start, int rows, Map<String, String[]> conditions) {
String sql = "select * from user_info where 1=1 ";
//遍历map,添加条件
StringBuilder sb = new StringBuilder(sql);
List<Object> params = new ArrayList<>();
joinSQL(conditions, sb, params);
//添加分页查询和参数
sb.append(" limit ?,?");
params.add(start);
params.add(rows);
Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(sb.toString());
for (int i = 1; i <= params.size(); i++) {
ps.setObject(i,params.get(i-1));
}
rs = ps.executeQuery();
ArrayList<UserInfo> users = new ArrayList<>();
while (rs.next()){
UserInfo user = new UserInfo();
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
users.add(user);
}
return users;
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(conn,ps,rs);
}
return null;
}
/**
* 条件查询记录总数
* @param conditions
* @return
*/
private int findTotalCount(Map<String,String[]> conditions){
Connection conn = JDBCUtil.getConnection();
PreparedStatement ps = null;
ResultSet rs = null;
String sql = "select count(*) as count from user_info where 1=1 ";
StringBuilder sb = new StringBuilder(sql);
List<Object> params = new ArrayList<>();
joinSQL(conditions,sb,params);
System.out.println(sb.toString());
try {
ps = conn.prepareStatement(sb.toString());
for (int i = 1; i <= params.size(); i++) {
ps.setObject(i,params.get(i-1));
}
rs = ps.executeQuery();
if (rs.next()){
return rs.getInt("count");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtil.release(conn,ps,rs);
}
return 0;
}
//根据条件集,动态拼接sql语句
private void joinSQL(Map<String, String[]> conditions, StringBuilder sb, List<Object> params) {
for (String key : conditions.keySet()) {
//仅符合设定条件的参数才可以添加
if ("username".equals(key) || "address".equals(key) || "email".equals(key)) {
String value = conditions.get(key)[0];
//判断是否有值,有值的条件才会添加
if (value != null && !"".equals(value.trim())) {
sb.append(" and " + key + " like ? ");
params.add("%" + value + "%");//条件值
}
}
}
}
}
使用事务包裹多次数据库操作,支持发生异常时回滚
/**
* 事务管理类
*/
public class TransactionManager {
private ThreadLocal<Connection> tl = new ThreadLocal<>();
public TransactionManager() {
if (null == tl.get()){
tl.set(JDBCUtil.getConnection());
}
}
public Connection getConnection(){
return tl.get();
}
public void beginTransaction() throws SQLException {
tl.get().setAutoCommit(false);
}
public void commit() throws SQLException {
tl.get().commit();
}
public void rollback() throws SQLException {
tl.get().rollback();
}
public void release() throws SQLException {
tl.get().close();
tl.remove();
}
}
/**
* 银行账户数据库操作类
* 使用事务,支持回滚
*/
public class AccountDao {
public boolean transferAccount(Integer from,Integer to,double money){
PreparedStatement ps = null;
ResultSet rs = null;
boolean result = false;
//使用事务
TransactionManager manager = new TransactionManager();
Connection conn = manager.getConnection();
//第一步,查询账户是否余额充足
String sql = "select money from bank_account where id = ?";
try {
//开始事务
manager.beginTransaction();
ps = conn.prepareStatement(sql);
ps.setInt(1,from);
rs = ps.executeQuery();
double oldMoney = 0;
if (rs.next()){
oldMoney = rs.getDouble("money");
if (money > oldMoney){
System.out.println("账户余额不足");
return false;
}
} else {
System.out.println("查无此账户");
return false;
}
//第二步,修改转出账户金额,减掉转出的钱
sql = "update bank_account set money = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1,oldMoney - money);
ps.setInt(2,from);
int update = ps.executeUpdate();
if (1 == update){
} else {
System.out.println("转出金额失败");
return false;
}
//手动制造异常,测试事务回滚
//int j = 1/0;
//第三步,修改转入账户金额,加上转出的钱
sql = "select money from bank_account where id = ?";
ps = conn.prepareStatement(sql);
ps.setInt(1,to);
rs = ps.executeQuery();
if (rs.next()){
oldMoney = rs.getDouble("money");
}
sql = "update bank_account set money = ? where id = ?";
ps = conn.prepareStatement(sql);
ps.setDouble(1,oldMoney + money);
ps.setInt(2,to);
update = ps.executeUpdate();
if (1 == update){
result = true;
} else {
System.out.println("转入金额失败");
result = false;
}
//提交事务
manager.commit();
} catch (Exception e) {
e.printStackTrace();
//回滚
try {
System.out.println("发生异常,回滚");
manager.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
} finally {
try {
manager.release();
} catch (SQLException e) {
e.printStackTrace();
}
JDBCUtil.release(conn,ps,rs);
}
return result;
}
}
使用转发实现返回页面,response写回对象信息
@WebServlet("/htmlApi")
public class HtmlServlet extends HttpServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getParameter("path");
if (null == path && path.contains(".html")){
//使用转发实现页面返回
req.getRequestDispatcher(path).forward(req,resp);
} else {
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().write("这是响应的具体信息!");
}
}
}
问题总结
经历过上面的几个具体需求实现,很明显有很多地方让我们实现得很痛苦,整个代码存在很多问题:高度耦合,很多重复代码等等。现在我们来一一分析下:
代码高度耦合,类实例缺少管理
为了实现用户信息表的增删改查,我们编写了一个UserInfoDao类,实现了一系列的增删改查方法。每一个Servlet需要用到的时候,都是先new UserInfoDao()创建一个Dao对象实例,然后调用对象方法。显然,这里对UserInfoDao对象实例是缺乏管理的,所有用到的地方都会先创建一个,存在代码耦合;其次每次创建一个对象实例都需要消耗cpu的处理时间片,每个对象实例都会占用一定的内存;再就是可能存在忘记释放无用对象造成内存泄漏。
这里优雅的实现应该是创建一个dao对象实例,全局共享,要用的时候去全局共享中取就好。既可以减少代码耦合,也可以全局管理dao对象实例,全生命周期,减少内存占用,避免内存泄漏。这就是spring框架的两大核心功能之一:IOC,关于IOC的具体实现可以参考《Java基础篇--反射和注解》--(实战)模拟spring框架,简单实现IOC。
数据库操作实现复杂,存在大量重复编码
上面的UserInfoDao类,实现了对user_info表的增删改查、以及分页、条件查询等,可以看到每个方法实现都大致包含几个步骤:
1.获取数据库connection连接
2.编写sql语句
3.获取preparestatement对象实例;
4.设置条件参数
5.执行具体操作
6.解析ResultSet结果,封装为所需要的对象
7.释放rs、ps和conn对象等资源
显然,每个方法都存在相同的几个步骤(1,3,7),我们多次重复实现这些代码;其次不管什么操作,都需要我们自己编写sql语句,要求我们不仅要会java,还必须熟悉sql;另外每次查询返回的都是ResultSet对象,需要我们进行结果解析和封装;最主要的,当前实体类UserInfo和数据库表user_info之间是没有任何关联的,所有的都是依赖sql语句和结果封装。正因为存在这么多缺点,所以才导致Dao编码异常复杂,一个简单的分页查询,就编写了100多行代码;一个条件查询,需要多次拼接sql字串,而且非常容易出错;等等诸多痛苦相信在之前的实现过程中一定深有体会。为了解决这些问题,java大佬们封装了数据库操作框架也叫对象关系映射框架,目前主流的主要是两个:Hibernate和Mybatis,分别对应着后台开发经典架构SSH中的H和SSM中的M。关于这两大框架的功能特点和区别,详见《JavaEE--数据库框架篇》。
事务管理高耦合,高度代码重复
上例我们模拟了一个银行转账的操作,编写了一个AccountDao实现具体的转账逻辑。由于转账存在多次操作数据库:先要查询转出账户是否金额充足,然后要修改转出账户金额(减去转出的金额),再就是查询出转入账户的原有金额,拿原有金额加上转出金额,计算得出最后金额后,修改回转入账户。整个过程不能发生任何异常,否则就可能存在金额不一致问题,比如我们设置的:
//手动制造异常,测试事务回滚
int j = 1/0;
当异常发生时,如果没有事务回滚,则会发生转出账户已经扣掉了100元,但是转入账户并没有收到钱,这100元也就凭空消失了,显然这肯定是不允许发生的。为了保证转账的系列数据库操作准确无误,当异常发生时也能回退到原来的状态,所以加上了事务管理。具体步骤是:
- 实现事务管理工具类TransactionManager;
- 改写需要事务管理的方法,在方法起始处创建TransactionManager对象实例,并调用方法beginTransaction()开启事务;
- 对整个方法进行try-catch包裹,捕获方法抛出的所有异常,确保当任何异常发生的时候,都可以触发回滚,在catch方法体中加上rollback(),执行回滚;
- 在原方法尾加上commit(),提交事务,保证正常逻辑的数据库操作执行;
- 在finally方法体中释放事务管理相关资源。
整个实现过程:事务管理工具类是自己造轮子,对整个转账方法的改写是重复代码,所有的需要加上事务管理的方法都是同样的改写方式,而且当我们想加上事务的时候,需要去修改Dao,很不方便。正确的实现方式应该是:使用代理对需要事务管理的方法进行动态增强。关于动态代理,如果还不熟悉,可以回顾《Java基础篇--设计模式》--代理模式。这正是spring框架的两大核心功能之一:AOP。主要的实现步骤是:通过IOC创建对象实例(比如Dao)的同时,扫描其方法上是否有事务注解(@transaction),如果有,则创建动态代理,对该方法进行事务包裹,动态增强该方法。大致代码实现如下:(代码中用就是我们造的轮子TransactionManager)
/**
* 用于创建service的代理对象工厂
*/
@Component
public class BeanFactory {
@Autowired
private IAccountService accountService;
@Autowired
private TransactionManager txManager;
/**
* 获取代理对象
* @return
*/
public IAccountService getAccountService() {
Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces()
, new InvocationHandler() {
/**
* 添加事务支持
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object rtValue = null;
try {
//01.开始事务
txManager.benginTransaction();
rtValue = method.invoke(accountService, args);
//02.提交事务
txManager.commit();
return rtValue;
} catch (Exception e) {
//03.回滚事务
txManager.rollback();
throw new RuntimeException(e);
} finally {
//04.释放连接
txManager.release();
}
}
});
return accountService;
}
}
Servlet返回是字串还是页面都不方便
在上例通过HtmlServlet体验了如何返回html或者返回json字串。返回html页面,我们知道其实就是文件下载,这里我们省了个懒,利用Tomcat容器的特性使用转发来直接返回页面资源;返回json字串(后台返回的时候不管是具体对象,还是字符串,都是转换为json字串的形式再传输),就是通过response写回信息即可,需要注意的是要先设置好response响应头的格式(contentType)。
显然这里也是可以更加优雅的实现,减少重复代码:1.可以在容器初始化设定封装HttpServletResponse对象时需要设置的ContentType,保证所有的response都是预设了ContentType,不用在每个servlet中都设置一下;2.通过注解,表明当前servlet是返回页面,还是返回字串。对于返回页面的,自动通过视图映射找到对应的页面资源,返回给浏览器;对于返回字串信息的,自动通过response写回。视图解析器、视图映射、区分结果返回等功能正是spring mvc的核心功能。
经历过了自己造轮子,繁复的数据库操作实现,重复的事务控制代码,手动if判断来达到视图映射等等。可以看到不使用任何框架直接来开发后台应用,还是很繁琐的,而且还不能保证实现的效果(俗称出力不讨好,事倍功半)。实际的后台服务开发还是要从经典SSH或者SSM开始,SSH(Spring SpringMVC Hibernate) SSM(Spring SpringMVC Mybatis)。下一篇将具体讲解使用SSM开发后台服务,体验一下畅快的后台开发,让你爱上编码,爱上这种感觉!
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!