MVC

本文详细阐述了转账系统的实现流程,包括需求分析、系统搭建、MVC架构的应用、事务处理及数据库连接池优化等关键步骤。通过逐步升级转账版本,介绍了如何引入分层开发、事务机制、代理模式和动态代理,最终加入druid数据库连接池提高系统性能。

转账系统实现

转账系统需求分析

(1)验证转出账号是否存在
(2)验证转入账号是否存在
(3)通过转出账号取得转出账号余额,看看钱够不够
(4)更新转出账号余额(扣钱)
(5)通过转入账号取得转入账号余额
(6)更新转入账号余额(加钱)
以上6个步骤有一个共同点,就是都是一次与数据库的交互.
以上6步,就是与数据库交互了6次.

转账系统搭建

(1)建表
(2)建工程(版本号0)
a.导入jar包
b.创建前端页面
c.创建后端包结构
d.创建controller
e.由controller来接收参数
(3)转账功能实现(负面教材)

转账版本1 抽取dao层

mvc版本0写不下去了,是因为我们将所有的代码都写在了servlet,servlet压力过大,我们现在急需为servlet进行减压工作.

controller(表现层):接收请求,接收参数,处理业务逻辑,为浏览器做出响应.
dao(持久层): 与数据库做交互

dao层的创建步骤
(1)创建包结构,命名为dao
(2)在dao包下,创建dao层的接口, 命名为domain+Dao
(3)在dao层接口中,写dao层的抽象方法,方法是以完成业务逻辑的子操作为单位,每一个方法中,就是一个jdbc的操作.
(4)在dao包下,创建子包,命名为impl
(5)impl包下,创建接口的实现类,命名为 接口名+Impl
(6)该实现类,实现接口,重写方法,完成方法体
(7)上一层在完成业务逻辑的过程中,只要遇到与数据库做交互,调用dao层的方法就可以了.

MVC

(1)MVC概述
MVC是一种基于java web开发的设计思想,以模型\视图\控制器的调用理念,形成了我们在实际项目开发中的一种固定的分层开发模式.(也是一种设计模式,但不是GoF23种之一,GoF是基于面向对象的设计模式)
M:模型 model
V:视图 view
C:控制器 controller
在这里插入图片描述
(2)MVC的两种模式
model1模式:
开发中,所有的组件使用jsp来完成,不存在servlet.
最大的好处是开发方便.
最大的缺点是不便于维护.
model1模式仅适用于微小型项目的开发.
这种微小型项目在市场上早已不存在了
model2模式:
在这里插入图片描述
(3)MVC相关面试题
a. 什么是MVC?
MVC是一种基于java web开发的设计思想,以模型\视图\控制器的调用理念,形成了我们在实际项目开发中的一种固定的分层开发模式.
基于MVC思想是怎么分的层?
模型层:service dao
视图层:jsp,easy ui,jquery ui,freemarker,extjs,bootstrap…
控制层:servlet

b.开发中如何分层?
controller:控制层(表现层)
service:业务层
dao:持久层

c. 分层开发的好处是?
(1)分层开发后, 使得开发中的组件分工明确,各司其职,使得我们的代码更具有可读性(读谁,找谁),可复用性(避开了重复代码),可维护性(维护那一层,只需要修改该层,其它层不用动).尽量的达到了高内聚(每一个组件做它专业做的事情)低耦合(比如有四个都是好使得模块,维护其中一个模块,影响另一个模块的使用,这就是耦合性太强)的目的(只有高内聚才能低耦合).
(2)分层开发后,更有利于分工协作开发.

注意:
我们未来的实际项目开发,就是以分层的形式来进行开发.
一定是上层调用下层,不可能是下层调用上层(不存在这种调用关系)
不要同层之间做调用.
不要跨层调用.

分工明确后:
controller:接收请求,接收参数,为浏览器做出响应
service:处理业务逻辑
dao:与数据库做交互(jdbc)

转账版本2 加入MVC思想进行分层开发

service层的创建步骤
(1)创建包结构,命名为service
(2)在service包下,创建业务层接口,命名为 domain+Service
(3)在接口中完成业务方法
业务方法,是以业务为单位.比如我们现在的转账操作,就一个业务方法,这个方法就是为了完成转账业务的.
方法中的参数:一般情况,都是控制层接收的参数.
方法中的返回值:如果是一个以添加/修改/删除为核心的业务,就不需要有返回值,如果是一个以查询为目的的业务,就需要有返回值.
对于转账,核心业务是更新操作,所以不需要有返回值.
(4)在service包下,创建子包,命名为impl
(5)在impl包下,创建接口的实现类,命名为 接口名+Impl
实现接口,重写方法,完成方法体.
(6)改造控制层,实现业务逻辑,调用service层对象就可以了.

转账版本3 加入事务机制

对于我们转账这件事情来说,最开始的6步,如果其中有一步发生问题,我们都需要让整个操作无效,要么一起成功,要么一起失败,所以我们需要加入事务。
问题是:我们的事务应该加入到哪里呢?
按照以前的做法,事务都是加在jdbc中,先是关闭自动提交,在最后提交事务,在catch中回滚事务,那么我们现在应该在dao层的三个方法中分别加上以上的条件 ,这样做相当于我们每调用一次dao层方法,都是一次事务从开启到结束的过程,在service中我们为了完成转账操作,一共6步,调用了6次dao层,也就是说我们把转账这一件事情分为了6个事务,但我们真正想做的是,只有转账这一个事务,将这6步子操作放在一个事务中,因为转账就是一件事情,这个过程不能发生任何差错,要么一起成功,要么一起失败。
因为转账的子操作都是在service中写的,所以我们需要在service中加入业务,也就是说,我们最终想要实现的是,在这6步子操作前开启事务,6步子操作后提交事务,6步子操作执行过程中发生异常后回滚事务,但是开启事务需要connection对象,但是connection属于的是jdbc代码,我们应该放到dao层中,在该版本中,为了完成事务操作,把connection加到service中,在以下版本中进行修复。
总结以上,我们现在实现的是,在service中设置了开启事务,提交事务,回滚事务,我们执行转账的子操作放在了开启事务和提交事务的中间,但是如果仅仅这样做,还会出现问题,因为我们在service中使用Connection对象开启了一个事务,但是我们在执行每一步子操作的时候,调用dao层方法,在dao层方法中又创建了一个新的Connection对象,也就是说,dao层的方法并没有受到事务的控制,因为他们用的是自己新创建的Connection对象,而我们的期望是用我们在service中创建的Connection对象,解决方法是,把我们在service中创建的Connection对象,当做参数传到dao层的方法,让dao层使用我们service中的Connection对象。
做完以上步骤后,我们发现还存在一个问题,那就是在执行完转账操作后,后台出现异常了,为什么会出现异常呢?这是因为,我们在service层中创建的Connection对象,在第一次执行子操作,验证转出账号是否存在时,就把Connection对象给关闭了,导致我们在Connection关闭后,还进行子操作,所以报异常,我们希望的是Connection对象的关闭交给service层处理,在执行子操作的过程中不关闭,所以我们将dao层方法中关闭Connection对象方法的参数设为null。
现在,我们在测试转账成功的操作时,后台不会出现问题了,那么在测试转账失败时呢?会不会出现问题呢?
会出现问题,经过测试发现我们故意写错验证账号的sql语句,执行转账操作后台虽然会报异常,但是前台还是会显示转账成功,这是因为,在我们执行第一步验证转出账号是否存在,执行到dao层的执行sql语句时,出现异常了,然而这个异常被dao层的catch捕获了,然后接着执行,继续执行service中的方法,所以才会出现转账成功的提示,我们想要的是,在执行子操作的过程中,发生的异常由service层中的catch捕获,这样既可以在service的catch中回滚事务了,现在有两种方式处理dao层中发生的异常,一种是在dao层直接抛出异常,交由service层的catch处理,但是这样做会导致异常不在捕获了,而是在执行完有错误的sql语句时,直接抛出,后面的finally语句的关闭资源的语句也不会执行,导致ResultSet和PrepareStatement资源无法关闭,第二种方式是在dao层中帮我们处理完异常后,我们再将异常抛给service层,如果此时抛的是throw new SQLException,会报错,因为SQLException是一个编译时异常,所以我们可以抛一个运行时异常,throw new RuntimeException(),但是这样的话,还会出现问题,在service中,执行完第一步子操作后,直接跳到finally中,因为我们抛出的是一个RuntimeException,但是我们处理的是一个SQLException,导致不匹配,所以直接跳到了finally,我们可以将处理的SQLException改为Exception。这样就OK了

转账版本4 加入ThreadLocal

java.lang.ThreadLocal是一种基于当前线程安全的存取机制.
set:存值
get:取值
remove:移除值
当我们将值存放到ThreadLocal对象中后,只要当前线程存在,则里面的值就还在.
为什么要使用ThreadLocal?
我们上面的转账版本3存在一个缺陷,我们每调用一次dao层中方法,需要将service中Connection对象作为参数传递,非常麻烦,所以我么使用ThreadLocal
在这里插入图片描述
当我们在service中创建出conn,将conn存入到ThreadLocal中,以后在service调用dao层方法时,不用传递参数,dao层直接去ThradLocal中取即可,加入ThreadLocal后的DBUtil

public class DBUtil {
	private static String url = "jdbc:mysql://localhost:3306/student";
	private static String user = "root";
	private static String password = "123";
	private static String className = "com.mysql.jdbc.Driver";
	private static ThreadLocal<Connection> t = new ThreadLocal<>();
	
	static {
		try {
			Class.forName(className);
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	/*
	 * 创建/取得连接
	 * 当service调用该方法的时候,t对象里面没有连接,该方法为创建一根连接,将连接存放到t对象中
	 * 当dao层调用该方法的时候,t对象里面已经有连接了,该方法为将连接直接从t中取出返回
	 */
	public static Connection getConn() throws SQLException {
		Connection conn = t.get();
		if (conn == null) {
			conn = DriverManager.getConnection(url, user, password);
			t.set(conn);
		}
		return conn;
	}
	
	public static void dbClose(ResultSet rs, PreparedStatement pstat, Connection conn) throws SQLException {
		if (rs != null) {
			rs.close();
		}
		if (pstat != null) {
			pstat.close();
		}
		if (conn != null) {
			conn.close();
			/*
			 * 为什么要将ThreadLocal中的值移除掉呢?ThreadLocal
			 * 不是随着线程的销毁而销毁了吗?
			 * 因为tomcat自带线程池,我们使用的线程都是从线程池中取得的,线程用完
			 * 并没有销毁,而是放入到了线程池中,所以我们需要销毁线程上存的值
			 */
			t.remove();
		}
	}
}

代理模式

(1)代理模式概述
GoF23种设计模式其中之一,指的是某一个java组件不方便做某些事情的处理时,可以交由代理类完成,叫做代理模式.
实际项目开发中,应用很广泛.

(2)代理模式的创建步骤
zs(真正要做这件事的人)送花(要做的事(业务逻辑))给ww,由ls(代理要做这件事的人)代替zs去完成.
1.创建业务接口,在业务接口中,写业务方法.
2.完成接口的真正的实现类(zs),实现接口,重写方法,完成方法体.
3.完成接口的代理实现类(ls),实现接口,重写方法.
a.写成员变量,类型为真正的实现类(zs)
b.写带有一个参数的构造方法,代替原来默认没有参数的构造方法,参数类型为真正的实现类(zs),由该参数为成员变量赋值.
c.代理实现类的业务方法由两部分内容来完成
第一部分内容:使用成员变量完成业务逻辑.
第二部分内容:业务逻辑的增强代码(真正的实现类不方便去做的事,业务逻辑的扩充).
4.在使用的过程中,先创建真正的实现类的对象,再创建代理实现类的对象.最后我们用就用代理实现类的对象.

(3)对比代理模式和装饰模式概念上的区别
两者在代码上相似,但完成的功能不同,装饰模式:比如一个人,穿衣服,戴各种装饰品,但这个人还是这个人,凸显出的是不断的装修自己,在代码上就是对功能的不断完善,代理模式:当我们写的某个组件要写的代码不方便在该组件中完成的时候,可以考虑使用代理模式,代替它做,指的是它不方便做这件事情的时候,才考虑代理模式

(4)代理模式在实际项目开发中的案例
我们给甲方做的系统,在甲方的服务器中已经运行了两年的时间,项目运行的一直很稳定,项目已经被证明是一个成熟的项目.
现在要求添加日志
以下以人员管理为例:

//这是人员管理的接口,实现增删改查操作
public interface UserService {
	public void save();
	public void delete();
	public void update();
	public void select();
}
//以下是该接口的实现类
public class UserServiceImpl implements UserService {
	@Override
	public void save() {
		System.out.println("添加日志开始");
		System.out.println("添加操作");
		System.out.println("添加日志结束");
	}
	@Override
	public void delete() {
		System.out.println("删除操作");
	}
	@Override
	public void update() {
		System.out.println("修改操作");
	}
	@Override
	public void select() {
		System.out.println("查询操作");
	}
}

我们以上述的操作来添加日志好吗?
不好,因为这已经是一个成熟的系统,我们修改了源代码
在这里插入图片描述
上图是一个上层调用下层的过程,现在客户提出需求,我需要在红色圈住的模块添加需求,但是需要修改的是它调用的最左边模块,此时,如果我们把最左边模块修改了,那么所有调用最左边模块的上层就可能出现问题,所以说一个成熟的系统,我们不要随意去修改源文件。

在实际项目开发中,23种设计模式基于6大基本开发原则,其中有一项是最重要的原则,为开闭原则.

开闭原则指的是,在一个已经成熟的系统的基础上,我们一定要做到,以源文件为单位,对添加开放,对修改关闭.
在以上叙述后,我们不方便在源文件UserServiceImpl上直接添加日志操作,我们可以用代理类来做它不方便做的事情

//UserServiceImpl的代理实现类,帮助UserServiceImpl在不修改源码的
//基础上,来实现添加日志的操作
public class UserServiceProxy implements UserService {
	private UserServiceImpl usi;
	public UserServiceProxy(UserServiceImpl usi) {
		this.usi = usi;
	}
	@Override
	public void save() {
		System.out.println("添加日志开始");
		usi.save();
		System.out.println("添加日志结束");
	}

	@Override
	public void delete() {
	}

	@Override
	public void update() {
	}

	@Override
	public void select() {
	}
}
//测试类
public class Test {
	public static void main(String[] args) {
		UserServiceImpl usi = new UserServiceImpl();
		UserServiceProxy usp = new UserServiceProxy(usi);
		usp.save();
	}
}
转账版本5 加入代理模式

我们在分层开发后,dao层代码不应该出现在service层中,在以上的版本中,在service层中,为了添加事务,出现了jdbc的代码,也就是说,在service层中,处理业务逻辑是应该做的事情,但是事务处理是不方便做的事情,相当于是业务逻辑的增强,所以将事务处理的代码抽取出去,交给代理类处理,在service只保留处理业务的代码。

转账版本6 加入动态代理

在加入了代理模式之后,由代理实现类来处理事务操作.未来的实际项目开发,所有的业务逻辑,都需要加入事务机制.所以我们以后每开发一个业务接口的真正的实现类,就必须要搭配的开发一个代理实现类来处理业务.
每开发一个zs,就得搭配的开发一个ls来处理事务,开发量倍增.我们现在急需一种方式,来对于事务操作的代理实现类进行一个通用的改造.
实现目的为,所有的zs,共用一个ls来处理事务.
我们现在要加入动态代理的机制.
动态代理基于原有的代理模式,结合反射机制,完成的能够实现通用代理业务的代码(不是23种设计模式)
动态代理类一般在util中创建

package com.jpg.util;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;

public class TransactionInvocationHandler implements InvocationHandler {
	private Object target; //写成Object是为了我们能够接收所有zs,通用性
	
	public TransactionInvocationHandler(Object target) {
		this.target = target;
	}
	
	/*
	 * 该方法为代理实现类的送花方法,即ls的送花方法
	 * ls送花方法组成:
	 * zs来完成业务逻辑
	 * 业务逻辑的增强(对于转账来说就是事务的处理)
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Connection conn = null;
		Object obj = null;
		
		try {
			conn = DBUtil.getConn();
			conn.setAutoCommit(false);
			
			/*
			 * zs完成的业务逻辑,应该用zs来调用方法,现在的zs就是target
			 * 方法就是method,因为zs是Object类型,所以我们只能用反射来调用方法
			 * 为什么方法的参数是args数组,因为有一些业务有返回值,有的没有,参数的
			 * 数量不确定,对于返回值也不确定,查询有返回值,更新没有返回值,这里用obj
			 * 接受返回值,如果没有返回值,obj就是null
			 * 
			 */
			
			obj = method.invoke(target, args);
			
			conn.commit();
		} catch (Exception e) {
			conn.rollback();
			e.printStackTrace();
		} finally {
			DBUtil.dbClose(null, null, conn);
		}
		return obj;
	}
	
	/*
	 * 按照以前的方式,直接new TransactionInvocationHandler(target),相当于new一个ls,
	 * 将zs作为参数,然后直接拿着ls调用方法,但是TransactionInvocationHandler new出来的不是ls
	 * 仅仅是动态代理类提供的一个对象,以下为取得代理实现类的对象ls
	 */
	
	public Object getProxy() {
		/*
		 * 第二个参数为zs的接口,在造ls的过程中,为ls提供zs的接口,因为ls实现的接口
		 * 必须和zs保持一致
		 */
		return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
	}
}

controller层函数调用
//创建zs
TaccountServiceImpl tsi = new TaccountServiceImpl();
//tih仅仅是动态代理类提供的一个对象
TransactionInvocationHandler tih = new TransactionInvocationHandler(tsi);
//现在我们已经知道了tih.getProxy()就是代表ls,可以用ls实现的接口或类
//来接受它,但是我们现在没有类,只有接口,ls和zs都实现了TaccountService接口
//所以我们可以使用接口去接受它
TaccountService taccountService = (TaccountService) tih.getProxy();
//就是执行的invoke方法
taccountService.taccount(zcAccount, zrAccount, zzBalanceStr);

对controller获取ls的方法进行封装,在util中创建

package com.jpg.util;

public class ServiceFactory {
	
	//传zs取ls
	public static Object getService(Object service) {
		return new TransactionInvocationHandler(service).getProxy();
	}
}

数据库连接池

(1)数据库连接池的作用
方便的管理我们的连接Connection
加入了数据库连接池之后,在连接池中有N根初始连接.
当我们使用连接的时候,从池中直接取得连接就可以了,省掉了创建连接的时间,提高了效率.
当我们使用完连接
普通连接(以前用的)
conn.close():将连接直接销毁
从数据库连接池中取得的连接(未来用的)
conn.close():将连接回收到池中

(2)市面上常见的数据库连接池
dbcp
c3p0
druid:阿里巴巴

(3)数据库连接池的搭建步骤
a.导入jar包
b.导入properties属性文件
c.代码中加载属性文件
d.从工厂创建连接池
e.取得连接,从连接池中取得
f.关闭连接,将连接回收到池中

(4)druid连接池的属性文件解析
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mytest
username=root
password=root
initialSize=20 #初始化连接数
maxActive=300 #最大连接数
maxWait=60000 #获取连接时最大等待时间,单位毫秒
maxIdle=10 #最大空闲连接数 已失效(druid中已失效)
timeBetweenEvictionRunsMillis=60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
minEvictableIdleTimeMillis=300000 #配置一个连接在池中最小生存的时间,单位是毫秒

转账版本7 加入druid数据库连接池
package com.jpg.util;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;

public class DBUtil {
	private static ThreadLocal<Connection> t = new ThreadLocal<>();
	
	private static Properties prop = new Properties();
	//dds数据库连接池对象
	private static DruidDataSource dds = null;
	
	static{
		try {
			//javaweb项目读取属性文件的方法
			prop.load(Thread.currentThread().getContextClassLoader().getResourceAsStream ("db_server.properties"));
			//由工厂创建一个数据库连接池
			dds = (DruidDataSource) DruidDataSourceFactory.createDataSource(prop);
			}catch (Exception e) {
				e.printStackTrace();
		}
	}
	
	public static Connection getConn() throws SQLException {
		Connection conn = t.get();
		if (conn == null) {
			//getConnectin实现了java.sql.Connection
			conn = dds.getConnection();
			t.set(conn);
		}
		return conn;
	}
	
	public static void dbClose(ResultSet rs, PreparedStatement pstat, Connection conn) throws SQLException {
		if (rs != null) {
			rs.close();
		}
		if (pstat != null) {
			pstat.close();
		}
		if (conn != null) {
			//将连接回收到数据池中,不是销毁连接
			conn.close();
			t.remove();
		}
	}
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值