想和你们分享我眼里的代码世界️ 优选系列持续更新中
绿色代表解释说明 黄色代表重点 红色代表精髓
Java三层架构是一种常用的软件开发模式,它将应用程序分为表示层、业务逻辑层和数据访问层,实现了代码的分层和解耦。但是,如何提高Java三层架构的开发效率,让代码更加简洁、高效和安全呢?本文将为大家分享三个实用的技巧,包括使用Druid数据库连接池、BasicDao泛型类、动态代理和ThreadLocal实现事务、统一路径反射生成方法等。喜欢直接阅读代码的读者,可以直接跳转查看源码进行学习
目 录
一、Java中的三层架构?揭开你的面纱
(一)什么是Java中的三层架构?
(二)为什么要使用Java中的三层架构?
(三)三层架构有何优势?
二、Dao层:采用Druid数据库连接池技术+DButils包+通用BasicDao
(一)什么是数据库连接池
(二)为什么要使用Druid数据库连接池
(三)怎么使用Druid数据库连接池
(四)为什么使用DButils包
(五)为什么使用BasicDao
(六)Druid数据库连接池+DButils+通用BasicDao实例
三、Service层采用动态代理+ThreadLocal实现事务
(一)什么是事务?为什么实现事务
(二)什么是动态代理?——Stop先搞清什么是静态代理!
(三)什么是动态代理
(四)ThreadLocal对象是什么
(五)动态代理+ThreadLocal实现事务实例
四、Servlet层采用统一路径反射生成方法
Java三层架构是一种基于MVC(Model-View-Controller)模式的软件开发模式,它将应用程序分为三个层次:
1️⃣表示层:也称为视图层或用户界面层,它负责与用户交互,显示数据和接收输入,也就是企业开发中对应的Servlet层。
2️⃣业务逻辑层:也称为服务层或控制器层,它负责处理用户的请求,执行业务逻辑和规则,调用数据访问层的方法,也就是企业开发中对应的Service层。
3️⃣数据访问层:也称为持久层或模型层,它负责与数据库交互,执行增删改查等操作,返回数据给业务逻辑层,也就是企业开发中对应的Dao层。
每个代码需求都来源于生(压)活(力),来个图感受一下三层架构的冲击:
服务员:只管接待客人;
厨师:只管做客人点的菜;
采购员:只管按客人点菜的要求采购食材;
他们各负其职,服务员不用了解厨师如何做菜,不用了解采购员如何采购食材;厨师不用知道服务员接待了哪位客人,不用知道采购员如何采购食材;同样,采购员不用知道服务员接待了哪位客人,不用知道厨师如何做菜。
使用三层架构的目的:解耦!!!
同样拿上面饭店的例子来讲:
(1)服务员(Servlet层)请假——另找服务员;厨师(Service层)辞职——招聘另一个厨师;采购员(Dao层)辞职——招聘另一个采购员; (2)顾客反映:
你们店服务态度不好——服务员的问题。开除服务员;
你们店菜里有虫子——厨师的问题。换厨师;
任何一层发生变化都不会影响到另外一层!
经过分析,可以清除的得到三层架构的优势:
结构清晰、耦合度低
可维护性高,可扩展性高
利于开发任务同步进行, 容易适应需求变化
Dao层是买菜的对吧,我不想每次买个菜都要跑去菜市场咋办?好的,美团你给我送过来!这里菜市场就是数据库,美团就是数据库连接池+DButils包+通用BasicDao!
数据库连接池技术是一种提高数据库访问性能和效率的方法,它可以实现对数据库连接的重用和管理,避免了频繁地创建和关闭连接所带来的开销和延迟。大白话:数据库连接池就是创建了一个池,池里面已经建立了很多与数据库的连接,Dao并不是去直接和数据库连接了,而是去数据库连接池拿一个连接。
还是不懂?灵魂画家要出手了!
使用数据库连接池技术的好处有以下几点:
资源重用:通过连接池,可以复用已经建立的数据库连接,减少了创建新连接的时间和资源消耗。这样可以提高系统的响应速度,同时也节省了内存空间和网络带宽。连接池中与数据库之间建立的连接不会主动断开,断开的是Dao层与数据库连接池之间的连接。
连接管理:通过连接池,可以统一地分配、监控和回收数据库连接,避免了连接泄露、超时、异常等问题。这样可以保证系统的稳定性和安全性,同时也方便了故障排查和性能分析。
连接配置:通过连接池,可以灵活地设置连接池的大小、超时时间、最大等待时间等参数,以适应不同的业务需求和负载情况。这样可以优化系统的吞吐量和资源利用率,同时也提高了系统的可扩展性。有配置文件设置这些属性。
防止崩溃:当多个Dao层需要与数据库建立连接,不至于让数据库崩溃,造成数据泄露等问题。如果数据库连接池的连接也被使用完,那么就必须在队列中等待连接被释放~
当然有许多的数据库连接池技术,Druid是较为高效的一个,因此采用Druid数据库连接池。
1️⃣Druid数据库连接池当然是已经写好的啦,作为优秀的API调用大师,我们需要导入jar包,如下:
2️⃣同时自己导入或编写配置文件如下:
标红处写自己的数据库,标黄处写自己的数据库密码
到这一步,其实我们没有减少很多代码!只是提高了连接数据库的稳定性。
3️⃣见面知意,这依然需要我们导入包,导入的包如下:
我们主要使用包中的QueryRunner类。QueryRunner类是DButils包中的一个核心类,它提供了一些简化和封装了JDBC操作的方法,可以让我们更方便地执行SQL语句,处理结果集,管理数据库连接等。
传统的Dao层,一个类就对应一个Dao类来操作数据库,但是数据库的操作无非就是增删改查,既然是相同的,只有Sql语句不一样,那么为什么不把相同的部分抽离出来,将Sql语句作为参数传入。使用泛型编写BasicDao,让其他Dao层继承它实现复用,同时其他的Dao扩展自己的业务。这样大大减少了代码冗余。
完整的代码如下,可以把这个类作为工具类拿去自己使用,因为它是万能的!
连接数据库的代码如下:
package BankSystem.util;
import BankSystem.dao.BasicDao;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author 高垚淼
* @version 1.0
*/
public class JdbcByDruid {
private static Properties properties = null;
private static DataSource druidDataSourceFactory = null;
static{
//使用类加载器读取配置文件
properties = new Properties();
try {
properties.load(BasicDao.class.getClassLoader().getResourceAsStream("BankSystem//druid.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
//使用Druid数据库连接池,预先连接数据库
try {
druidDataSourceFactory = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//返回Connection对象,建立与数据库连接池的连接
public static Connection getConnection(){
try {
return druidDataSourceFactory.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
//仅需关闭Connection对象,QuryRunner中的方法已经关闭了resaultSet、preparedStatement
public static void close(Connection connection){
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
编写BasicDao,实现通用增删改查,让其他具体的Dao层继承该类即可,代码如下:
package BankSystem.dao;
import BankSystem.util.JdbcByDruid;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
/**
* @author 高垚淼
* @version 1.0
*/
//创建所有Dao的基本操作,使用泛型T减少代码量
@SuppressWarnings("all")
public class BasicDao {
private QueryRunner qr = new QueryRunner();;
/**
* 插入、删除、修改语句的通用操作
* @param sql 传入sql语句
* @param parameters 可变参数,设置sql中的?通配符
*/
public int update(String sql,Object... parameters){
Connection connection = JdbcByDruid.getConnection();
try {
return qr.update(connection,sql,parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
JdbcByDruid.close(connection);
}
}
/**
* 查询所有的通用操作
* @param sql 传入sql语句
* @param clazz 类的字节码文件对象,反射加载该类
* @param parameters 可变参数,设置sql中的?通配符
* @return 返回遍历封装T对象的List集合
*/
public List queryMulti(String sql, Class clazz,Object... parameters){
Connection connection = JdbcByDruid.getConnection();
try {
//这个参数表示返回一个对象列表
return qr.query(connection,sql,new BeanListHandler(clazz),parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
JdbcByDruid.close(connection);
}
}
/**
*
* @param sql 传入sql语句
* @param clazz 类的字节码文件对象,反射加载该类
* @param parameters 变参数,设置sql中的?通配符
* @return 返回单个对象
*/
public T querySingle(String sql, Class clazz,Object... parameters){
Connection connection = JdbcByDruid.getConnection();
try {
//这个参数表示返回一个对象
return qr.query(connection,sql,new BeanHandler(clazz),parameters);
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
JdbcByDruid.close(connection);
}
}
}
出错了,你数据库的配置文件改好了嘛
一个Service类中的方法有时需要执行多条sql语句
事务是指一组逻辑上相关的操作,它们要么全部成功,要么全部失败,不允许出现中间状态。在Java Web开发中,通常需要在Service层对数据库的操作进行事务管理,以保证数据的完整性和一致性。
我们知道Service层是厨师,他很忙的,我们不会只执行一条sql语句 。比如,数据库要更新转账信息,我V你50,我存款应该减少50,你存款应该增加50,这对应两条Sql语句,想想如果中途出了错,只执行了一条?那我就凭空少了50。就像如下情形:
String sql = "update users set money-=50 where name="小高" ";
//来咯来咯,模拟出bug
int a = 10/0;
//每个读者都必须V50
String sql2 = "update users set money+=50 where name="读者" ";
因此,实现事务是必须的。实现事务就是保证数据整体上不会发生丢失,错误。
静态代理是一种设计模式,它可以让一个类(代理类)代表另一个类(被代理类)去执行一些操作,同时可以在执行前后添加一些额外的功能。静态代理的特点是,代理类和被代理类在编译时就已经确定了,它们都需要实现一个共同的接口。
一个简单的代码例子是,假设有一个接口叫做Hello,它有一个sayHello方法:
//定义一个接口
public interface Hello {
//定义一个抽象方法
public void sayHello();
}
然后有一个实现类叫做HelloImpl,它实现了Hello接口,并在sayHello方法中打印一句话:
//定义一个实现类
public class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello, I am HelloImpl");
}
}
现在我们想要在sayHello方法执行前后添加一些日志信息,我们可以定义一个代理类叫做HelloProxy,它也实现了Hello接口,并持有一个HelloImpl的引用,在sayHello方法中调用HelloImpl的sayHello方法,并在前后添加日志信息:
//定义一个代理类
public class HelloProxy implements Hello {
//持有一个被代理对象的引用
private HelloImpl helloImpl;
//通过构造器传入被代理对象
public HelloProxy(HelloImpl helloImpl) {
this.helloImpl = helloImpl;
}
@Override
public void sayHello() {
//在执行前添加日志信息
System.out.println("Before say hello");
//调用被代理对象的方法
helloImpl.sayHello();
//在执行后添加日志信息
System.out.println("After say hello");
}
}
最后我们可以在测试类中创建一个HelloProxy对象,并调用它的sayHello方法,看看效果:
//定义一个测试类
public class Test {
public static void main(String[] args) {
//创建一个被代理对象
HelloImpl helloImpl = new HelloImpl();
//创建一个代理对象,并传入被代理对象
HelloProxy helloProxy = new HelloProxy(helloImpl);
//调用代理对象的方法
helloProxy.sayHello();
}
}
运行结果如下:
Before say hello
Hello, I am HelloImpl
After say hello
那么我们可以直观的看到,使用代理模式,可以为不同的Service对象的方法前后添加相应的方法,可以是日志,当然,也可以是控制事务的语句!但是,静态代理依旧不够灵活,难道每个Service我都需要去创建一个代理对象?No!能少些的代码,绝不多写!
动态代理也是一种设计模式,它可以让一个类(代理类)在运行时动态地生成并代表另一个类(被代理类)去执行一些操作,同时可以在执行前后添加一些额外的功能。动态代理的特点是,代理类和被代理类在运行时才确定,它们不需要实现一个共同的接口,而是通过反射机制来调用被代理类的方法。
一个简单的代码例子是,假设有一个接口叫做Hello,它有一个sayHello方法:
//定义一个接口
public interface Hello {
//定义一个抽象方法
public void sayHello();
}
然后有一个实现类叫做HelloImpl,它实现了Hello接口,并在sayHello方法中打印一句话:
//定义一个实现类
public class HelloImpl implements Hello {
@Override
public void sayHello() {
System.out.println("Hello, I am HelloImpl");
}
}
现在我们想要在sayHello方法执行前后添加一些日志信息,我们可以使用JDK提供的动态代理机制来实现。我们需要定义一个实现了InvocationHandler接口的类,它负责处理代理对象的方法调用,并在调用前后添加日志信息:
//定义一个处理器类
public class HelloHandler implements InvocationHandler {
//持有一个被代理对象的引用
private Object target;
//通过构造器传入被代理对象
public HelloHandler(Object target) {
this.target = target;
}
//重写invoke方法,该方法会在代理对象调用任何方法时都会执行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//在执行前添加日志信息
System.out.println("Before say hello");
//通过反射调用被代理对象的方法
Object result = method.invoke(target, args);
//在执行后添加日志信息
System.out.println("After say hello");
//返回结果
return result;
}
}
最后我们可以使用Proxy类的静态方法newProxyInstance来创建一个代理对象,并传入被代理对象和处理器对象:
//定义一个测试类
public class Test {
public static void main(String[] args) {
//创建一个被代理对象
HelloImpl helloImpl = new HelloImpl();
//创建一个处理器对象
HelloHandler helloHandler = new HelloHandler(helloImpl);
//创建一个代理对象,并传入被代理对象的类加载器、接口和处理器对象
Hello proxy = (Hello) Proxy.newProxyInstance(helloImpl.getClass().getClassLoader(), helloImpl.getClass().getInterfaces(), helloHandler);
//调用代理对象的方法
proxy.sayHello();
}
}
运行结果如下:
Before say hello
Hello, I am HelloImpl
After say hello
现在我多个Service类只需要调用这一个代理对象,就可以执行各自的方法。你会问?你代理是代理了,可是我也没看见你怎么控制事务的呀,别急,还差最后一个知识点。
ThreadLocal对象是一种特殊的变量,它可以为每个线程提供一个独立的副本,从而实现线程间的数据隔离。可以简单的理解为ThreadLocal是可以存放一个任意类型的数据的变量,供不同的类使用。
它的常用方法只有两个:get()、set(),分别对应放入数据和取出数据。
我们使用ThreadLocal对象的目的就是获取当前Connection连接,保证当前连接中执行的所有方法都是事务的。
实现的基本思路:
1️⃣在Service层,使用动态代理来创建代理对象,代理对象可以在调用真实对象的方法前后添加事务控制的逻辑,即开启事务、提交事务、回滚事务。
2️⃣在Service层,使用ThreadLocal对象来存储数据库连接,每个线程都有自己的ThreadLocal对象,从而实现线程间的数据隔离。这样,每个线程都可以使用自己的数据库连接和事务,而不会影响其他线程。
3️⃣在Dao层,从ThreadLocal对象中获取数据库连接,然后执行SQL语句。这样,Dao层就可以使用Service层传递过来的数据库连接和事务,而不需要自己创建或管理。
4️⃣这样在Servlet层调用Service层时,我们可以使用Service层的代理对象,并且保证了事务
为了一个知识点对应一个代码,没有采用上面的Druid数据库连接池技术,但是它们互不影响,稍改下连接方式即可,不要忘了~
下面给出动态代理+ThreadLocal实现事务的完整代码:
Servlet层调用代理对象:
//Servlet层中的XXXService都是通过动态代理产生的代理对象
BuildingService buildingService = (BuildingService) ProxyUtil.getProxy(new BuildingServiceImpl());
Service层中生成代理对象的代码:
//Service层中,getProxy()
public static Object getProxy(Object target) {
//InvocationHandler对象
InvocationHandler h = new InvocationHandler() {
/**
* proxy 代理对象,基本没用
* method 目标方法
* args 目标方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result=null;
Connection conn = null;
try{
//通过数据源获取连接
conn = DataUtil.getConnectionByDatasource();
System.out.println("代理对象获取的数据库连接:"+conn);
//设置不自动提交
conn.setAutoCommit(false);
//所这个数据库连接和当前线程进行绑定
DataUtil.tl.set(conn);
//执行目标方法
result = method.invoke(target, args);
//提交
conn.commit();
}catch (Exception e){
e.printStackTrace();
//回滚
conn.rollback();
}finally {
//关资源
conn.close();
}
return result;
}
};
//通过Proxy类中的newProxyInstance方法,可以生成一个代理对象
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), h);
//返回的对象也实现了target实现的类
return proxy;
}
Dao层中,获取v保证是一个线程:
//Dao层中
public static final ThreadLocal tl = new ThreadLocal<>();
//dao层获取数据库连接都是从ThreadLocal中获取
public static Connection getConnection(){
//通过ThrealLocal获取和当前线程绑定的数据库连接
Connection connection = tl.get();
System.out.println("dao层获取的连接:"+connection);
return connection;
}
public static Connection getConnectionByDatasource(){
try {
return ds.getConnection();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
特别注意:Dao层获得连接是从ThreadLocal中取出,ThreadLocal中的连接是从数据库连接池中取出。
最后的结果也是显示,Service中调用的sql语句要么一起执行,要么都不执行,实现了事务~
基本思路是:
1️⃣定义一个通用的Servlet类,作为所有请求的入口,根据请求的路径或参数来判断要执行哪个方法。
2️⃣在通用的Servlet类中,使用反射机制来动态地调用对应的方法,而不需要使用if-else或switch-case等条件判断语句。
4️⃣在每个具体的方法中,实现相应的业务逻辑,并返回结果给客户端。
Servlet层采用统一路径反射生成方法实例:
//定义一个通用的Servlet类
@WebServlet(/user/*)
public class BaseServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求的路径
String uri = req.getRequestURI();
//获取要执行的方法名,比如 /user/add
String methodName = uri.substring(uri.lastIndexOf('/') + 1);
try {
//获取当前类的字节码对象
Class clazz = this.getClass();
//获取当前类中指定名称的方法
Method method = clazz.getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
//执行方法
method.invoke(this, req, resp);
} catch (Exception e) {
e.printStackTrace();
}
}
//定义一个具体的方法,用于添加用户
public void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求参数
String username = req.getParameter("username");
String password = req.getParameter("password");
//调用Service层或Dao层进行业务处理,这里省略
//返回结果给客户端,这里简单地打印一句话
resp.getWriter().println("添加用户成功");
}
//定义一个具体的方法,用于删除用户
public void delete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取请求参数
String id = req.getParameter("id");
//调用Service层或Dao层进行业务处理,这里省略
//返回结果给客户端,这里简单地打印一句话
resp.getWriter().println("删除用户成功");
}
//可以定义其他的方法,用于处理不同的请求
}
以上就是本文的全部内容啦,确定不来个点赞和收藏嘛~