[Java优选系列第1弹]如何优化Java三层架构开发效率?三个实用技巧分享

2 篇文章 0 订阅

想和你们分享我眼里的代码世界🗺️  优选系列持续更新中💫

一直在等你,你终于来啦💖

                          绿色代表解释说明                黄色代表重点                红色代表精髓

        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中的三层架构?揭开你的面纱🤨

(一)什么是Java中的三层架构?

        Java三层架构是一种基于MVC(Model-View-Controller)模式的软件开发模式,它将应用程序分为三个层次:

        1️⃣表示层:也称为视图层或用户界面层,它负责与用户交互,显示数据和接收输入,也就是企业开发中对应的Servlet层

        2️⃣业务逻辑层:也称为服务层或控制器层,它负责处理用户的请求,执行业务逻辑和规则,调用数据访问层的方法,也就是企业开发中对应的Service层

        3️⃣数据访问层:也称为持久层或模型层,它负责与数据库交互,执行增删改查等操作,返回数据给业务逻辑层,也就是企业开发中对应的Dao层

每个代码需求都来源于生(压)活(力),来个图感受一下三层架构的冲击:

服务员:只管接待客人;

厨师:只管做客人点的菜;

采购员:只管按客人点菜的要求采购食材;

        他们各负其职,服务员不用了解厨师如何做菜,不用了解采购员如何采购食材;厨师不用知道服务员接待了哪位客人,不用知道采购员如何采购食材;同样,采购员不用知道服务员接待了哪位客人,不用知道厨师如何做菜。

(二)为什么要使用Java中的三层架构?

使用三层架构的目的:解耦!!!

同样拿上面饭店的例子来讲:

(1)服务员(Servlet层)请假——另找服务员;厨师(Service层)辞职——招聘另一个厨师;采购员(Dao层)辞职——招聘另一个采购员; (2)顾客反映:

🔸你们店服务态度不好——服务员的问题。开除服务员;

🔸你们店菜里有虫子——厨师的问题。换厨师;

任何一层发生变化都不会影响到另外一层!

(三)三层架构有何优势?

经过分析,可以清除的得到三层架构的优势:

🔸结构清晰、耦合度低

🔸可维护性高,可扩展性高

🔸利于开发任务同步进行, 容易适应需求变化

二、Dao层:采用Druid数据库连接池技术+DButils包+通用BasicDao🌈

Dao层是买菜的对吧,我不想每次买个菜都要跑去菜市场咋办?好的,美团你给我送过来!这里菜市场就是数据库,美团就是数据库连接池+DButils包+通用BasicDao!

(一)什么是数据库连接池

        数据库连接池技术是一种提高数据库访问性能和效率的方法,它可以实现对数据库连接的重用和管理,避免了频繁地创建和关闭连接所带来的开销和延迟。大白话:数据库连接池就是创建了一个池,池里面已经建立了很多与数据库的连接,Dao并不是去直接和数据库连接了,而是去数据库连接池拿一个连接。

        还是不懂?灵魂画家要出手了!

(二)为什么要使用Druid数据库连接池

使用数据库连接池技术的好处有以下几点:

🔸资源重用:通过连接池,可以复用已经建立的数据库连接,减少了创建新连接的时间和资源消耗。这样可以提高系统的响应速度,同时也节省了内存空间和网络带宽。连接池中与数据库之间建立的连接不会主动断开,断开的是Dao层与数据库连接池之间的连接。

🔸连接管理:通过连接池,可以统一地分配、监控和回收数据库连接,避免了连接泄露、超时、异常等问题。这样可以保证系统的稳定性和安全性,同时也方便了故障排查和性能分析。

🔸连接配置:通过连接池,可以灵活地设置连接池的大小、超时时间、最大等待时间等参数,以适应不同的业务需求和负载情况。这样可以优化系统的吞吐量和资源利用率,同时也提高了系统的可扩展性。有配置文件设置这些属性。

🔸防止崩溃:当多个Dao层需要与数据库建立连接,不至于让数据库崩溃,造成数据泄露等问题。如果数据库连接池的连接也被使用完,那么就必须在队列中等待连接被释放~

当然有许多的数据库连接池技术,Druid是较为高效的一个,因此采用Druid数据库连接池。

 (三)怎么使用Druid数据库连接池

1️⃣Druid数据库连接池当然是已经写好的啦,作为优秀的API调用大师,我们需要导入jar包,如下:

2️⃣同时自己导入或编写配置文件如下:

标红处写自己的数据库,标黄处写自己的数据库密码

到这一步,其实我们没有减少很多代码!只是提高了连接数据库的稳定性。

 (四)为什么使用DButils包

3️⃣见面知意,这依然需要我们导入包,导入的包如下:

我们主要使用包中的QueryRunner类。QueryRunner类是DButils包中的一个核心类,它提供了一些简化和封装了JDBC操作的方法,可以让我们更方便地执行SQL语句,处理结果集,管理数据库连接等。

(五)为什么使用BasicDao

传统的Dao层,一个类就对应一个Dao类来操作数据库,但是数据库的操作无非就是增删改查,既然是相同的,只有Sql语句不一样,那么为什么不把相同的部分抽离出来,将Sql语句作为参数传入。使用泛型编写BasicDao,让其他Dao层继承它实现复用,同时其他的Dao扩展自己的业务。这样大大减少了代码冗余。

(六)Druid数据库连接池+DButils+通用BasicDao实例

完整的代码如下,可以把这个类作为工具类拿去自己使用,因为它是万能的!

🔴连接数据库的代码如下:

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<T> {
    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<T> queryMulti(String sql, Class<T> clazz,Object... parameters){
        Connection connection = JdbcByDruid.getConnection();
        try {
                                          //这个参数表示返回一个对象列表
            return qr.query(connection,sql,new BeanListHandler<T>(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<T> clazz,Object... parameters){
        Connection connection = JdbcByDruid.getConnection();
        try {
                                           //这个参数表示返回一个对象
            return qr.query(connection,sql,new BeanHandler<T>(clazz),parameters);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }finally {
            JdbcByDruid.close(connection);
        }
    }
}

出错了,你数据库的配置文件改好了嘛🤯

三、Service层采用动态代理+ThreadLocal实现事务🌠

一个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="读者" ";

因此,实现事务是必须的。实现事务就是保证数据整体上不会发生丢失,错误。

(二)什么是动态代理?——Stop先搞清什么是静态代理!

        静态代理是一种设计模式,它可以让一个类(代理类)代表另一个类(被代理类)去执行一些操作,同时可以在执行前后添加一些额外的功能。静态代理的特点是,代理类和被代理类在编译时就已经确定了,它们都需要实现一个共同的接口。

一个简单的代码例子是,假设有一个接口叫做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对象是一种特殊的变量,它可以为每个线程提供一个独立的副本,从而实现线程间的数据隔离。可以简单的理解为ThreadLocal是可以存放一个任意类型的数据的变量,供不同的类使用。

它的常用方法只有两个:get()、set(),分别对应放入数据和取出数据。

我们使用ThreadLocal对象的目的就是获取当前Connection连接,保证当前连接中执行的所有方法都是事务的。

(五)动态代理+ThreadLocal实现事务实例

实现的基本思路:

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<Connection> 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语句要么一起执行,要么都不执行,实现了事务~

四、Servlet层采用统一路径反射生成方法💥

基本思路是:

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("删除用户成功");
    }

    //可以定义其他的方法,用于处理不同的请求
}

人生如火车,历经的所有都是风景,一边回味一边珍惜

                                以上就是本文的全部内容啦,确定不来个点赞👍和收藏💖嘛~ 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Gavana.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值