前言
Maven能很好的解决引入jar包问题,
Tomcat热部署可以让你少点几次鼠标,
一个好的工具类(虽然我也写的不咋地)能够在各个类似项目里复用,
掌握Manager工具可以让你对服务器的运行情况有更多了解,
如果想要更加便捷地开发,可以了解一下这些框架
Mybatis(不用考虑DAO层的实现)
Mabatis-plus(在Mybatis的基础上,不用自己写简单的SQL),
Bootstrap(不用自己写简单的HTML),
Log-4j(不用自己sout打印日志),
SSM(Spring,Spring MVC,Mybatis,一般的项目用这三个就够了)
SpringBoot(SSM的简化版,比SSM写项目方便一点)
Maven构建工具
这个工具可以让你不用再去自己复制jar包再放入自己的项目,而是可以通过网络进行下载jar包。
在最初使用web框架时,你会发现每当你创建一个新的项目时,为了连接数据库,你都要把数据库驱动jar包放在WEB-INF的lib文件夹下,比较麻烦。
所以你可以将项目变成Maven工具构建的项目,以test2项目为例,
你只需要在这个test目录下新建一个pom.xml文件
(这里我的web根目录就叫web而不是webapp,这个是可以通过Fact或者工件设置那里自己改的,不要在意)
新建后pom.xml文件,点击进去,右键添加为Maven项目
然后右边会出现这个,Unknown就是刚刚添加为Maven项目的那一个。
复制这段进去,记得改把工件名和组名改成自己的
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 组名,加个企业名或者啥的,反正就是自己的包名 -->
<groupId>com.budiu</groupId>
<!-- 工件名 -->
<artifactId>test2</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 这个JSTL之后应该会学的 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
<scope>compile</scope>
</dependency>
<!--这里的provided是指服务器自己本身就已经有这个jar包了,不需要打包进入部署单元(如war文件)-->
<!-- 引入这个纯粹是为了在写Servlet类是不用自己手动在模块里再去Tomcat安装目录下再去手动引入这个包了,不引入IDEA会提示无法解析该类,这是IDEA的问题,不是Tomcat的问题 -->
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp/jsp-api -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<!-- 这个可能要根据自己的数据库进行版本变动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>compile</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
重新加载项目
自己就把名字变好了,然后就可以把lib文件夹删除掉了
之后就可以在这个pom.xml文件填写依赖坐标,而不是自己去网上下载到本地再去引用。
Maven这个工具以后要专门去学的,不过全部内容也就两个小时,常用的内容十几分钟就看完了。
Tomcat服务器热部署与热加载
每次进行代码改动后,每次都得进行更新类和资源,然后再去浏览器刷新网页,这也是个麻烦的事。
虽然热部署的范围比热加载大,但是开发环境下,大多更喜欢用热加载,因为范围更小,对性能消耗也会变小。
在生产环境下,热部署更常见。
我的建议是直接热部署得了,我们写的那些作业体量根本还不需要考虑服务器负载性能那种程度。
热加载与热部署的区别
此处参考博文
腾讯云社区 热加载与热部署有什么区别?
腾讯云社区 热部署热加载原理解析
优快云 Tomcat热加载/热部署
一篇文章彻底搞懂Tomcat热部署与热加载,这篇思路最清晰,但是不太好读懂
热加载(Hot Swap):
定义:热加载是指在Tomcat服务器不停止运行情况下,重新加载Java类文件和资源文件
执行主体:热加载的执行主体是Context,它表示单个Web应用程序。
热部署(Hot Deployment):
定义:热部署是指在Tomcat服务器在不停止运行情况下,重新部署整个Web应用程序
执行主体:热部署的执行主体是Host,它表示Tomcat服务器上的主机环境,负责管理多个Web应用程序。
热加载
开启热加载后会有一个后台线程周期性(每隔10秒)监测WEB-INF/classes和WEB-INF/lib里面的文件内容是否变动,如果内容变动,Tomcat会清空类加载缓存,重新进行类加载和资源加载。
热部署
开启热部署后,host会监控自己的war包是否被删除,有没有新的war包进入,然后调用内部API进行部署。
热加载配置方法
这是最简单的热加载,IDEA里就能配置,只需要在切出IDE时,把它变为更新类和资源。
日志出现这种,就说明热加载成功了。
热部署配置方法
热部署有三种主流方式,第一种是Jrebel插件,可以在IDEA插件里去下载,不过是付费插件,但是网上也有破解之法,第二种也需要用到Maven工具,是一个Maven插件,第三种就是更改Tomcat本身的配置。
这里只说最简单的一种:
- 首先先把上面实现热加载的IDEA设置弄好,也就是切换出IDEA时选择更新类文件于资源操作。
其实这里原理没弄懂,个人猜测是IDEA的项目并不是直接部署在Tomcat里的,必须将项目构建后,才会放入Tomcat的Manager进行部署。如果更改代码,不重新构建,Tomcat是无法监测到文件发生变化的,所以需要这个设置去重新构建
在这里我试过假如不执行第一步操作的话,那就需要点一这个构建工件,才能达到相同效果
- 找到Tomcat的context.xml文件: 这个文件通常位于$CATALINA_BASE/conf目录下(CATALINA_BASE是Tomcat的安装根目录)。
- 编辑context.xml文件: 打开context.xml文件,找到标签,在Context加上这个属性
<Context reloadable="true">
reloadable="true"属性指示Tomcat在类文件更改时重新加载Web应用程序,然后重启一下Tomcat就好。如果失败了,可以重启一下电脑。
这里看起来和热加载好像没什么变化,但你可以试试改变Servlet的路径,如果只是热加载,你网址栏输入更改后的路径将会得到一个404报错,但是如果是热部署,那就会返回这个Servlet处理后的结果。
数据库连接工具
基础工具类实现
老师提供的数据库连接的模板文件已经很好了,但是我偷了个懒,写得简单点(更简单的代价肯定就是不够全面,无法应对所有情况,但是应付作业肯定还是轻轻松松的)。
我最初写的MYSQL工具类,就只有两个静态方法,一个获取连接,一个关闭连接。
package com.budiu.util;
import java.sql.*;
/**
* 连接MySQL数据库的工具类
*
* @author 罗不丢
* @since 2024/11/10
*/
public class MySQLUtil {
static String driverClass="com.mysql.cj.jdbc.Driver";
static String userName="*******";
static String password="*******";
static String url="jdbc:mysql://sh-cynosdbmysql-grp-jmg83f6i.sql.tencentcdb.com:22552/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC";
private static Connection connection=null;
public static Connection getConnection() {
try {
Class.forName(driverClass);
return DriverManager.getConnection(url,userName,password);
} catch (SQLException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public static void closeConnection() {
if (connection!=null){
try {
connection.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
不知道你们能在里面发现几个问题
首先是我用static来修饰Connection,而且我的方法没有进行同步操作,那么说明在多人使用的情况下,这个Connection很有可能会出现异常。
然后是我的硬编码(也就是我的数据库驱动名称,用户名等都写在类里,正常情况下是需要写在yml或者properties配置文件里的,不过这里我也懒得去额外再去写配置文件了,毕竟是个懒人)。
还有closeConnection方法中虽然尝试关闭连接,但是没有提供关闭ResultSet和Statement的方法,这还是可能会导致内存泄漏。
工具类优化
但是吧,如果我不用静态变量connection的话,每个人进行一次读取数据的访问我就得创建一个数据库连接对象,其实很多时候一些连接对象是闲置的,还可以反复利用,所以就有了数据库连接池这个概念来进行性能优化。
java里比较常见的数据库连接池技术有c3po和Druid
这里只展示德鲁伊数据库连接池的基本使用。
先进行Maven引用,把这个依赖放到pom.xml文件里,不会Maven那就只有先下载jar到本地再引用了
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
德鲁伊数据库连接池工具类,那个sqlLog方法是为了打印填写好参数的SQL
public class DruidMySQLUtil {
private static DruidDataSource dataSource;
static {
dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://sh-cynosdbmysql-grp-jmg83f6i.sql.tencentcdb.com:22552/test?characterEncoding=utf8&useSSL=false&serverTimezone=UTC");
dataSource.setUsername("*******");
dataSource.setPassword("*******");
// 配置初始化大小、最小、最大
dataSource.setInitialSize(1);
dataSource.setMinIdle(1);
dataSource.setMaxActive(20);
// 配置获取连接等待超时的时间
dataSource.setMaxWait(60000);
// 配置间隔多久进行一次检测,检测需要关闭的空闲连接
dataSource.setTimeBetweenEvictionRunsMillis(60000);
// 配置一个连接在池中最小生存的时间
dataSource.setMinEvictableIdleTimeMillis(300000);
//用于检查数据库连接是否仍然有效
dataSource.setValidationQuery("SELECT 'x'");
//连接池会定期检查空闲的连接是否仍然有效。如果验证失败,连接会被从池中移除并重新创建。
dataSource.setTestWhileIdle(true);
//表示在从连接池借用连接时不会进行验证。如果设置为true,每次从连接池获取连接时都会执行validationQuery来验证连接的有效性。
dataSource.setTestOnBorrow(false);
//属性设置为false时,表示在将连接返回到连接池时不会进行验证。
dataSource.setTestOnReturn(false);
// 打开PSCache,并且指定每个连接上PSCache的大小
dataSource.setPoolPreparedStatements(true);
dataSource.setMaxOpenPreparedStatements(20);
try {
dataSource.init();
} catch (SQLException e) {
throw new RuntimeException("初始化数据连接池失败",e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void close(ResultSet rs) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException("结果集资源关闭失败", e);
}
}
}
public static void close(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException("连接资源关闭失败", e);
}
}
}
public static void close(PreparedStatement pstmt) {
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
throw new RuntimeException("预处理语句资源关闭失败", e);
}
}
}
public static void close(Statement statement){
if(statement!=null){
try{
statement.close();
}catch (SQLException e){
throw new RuntimeException("statement资源关闭失败", e);
}
}
}
// 关闭所有资源
public static void closeAll(Connection conn, PreparedStatement pstmt, ResultSet rs) {
close(rs);
close(pstmt);
close(conn);
}
public static void closeAll(Connection conn, PreparedStatement pstmt, ResultSet rs,Statement statement) {
close(rs);
close(pstmt);
close(conn);
close(statement);
}
public static void closeAll(Connection conn, ResultSet rs,Statement statement){
close(rs);
close(conn);
close(statement);
}
public static void closeAll(Connection conn, PreparedStatement prestatement){
close(conn);
close(prestatement);
}
/**
* @Description 日志记录
* @Params
* @param preparedStatement 已经填充好参数的预处理语句
* @Return void
* @Author 罗不丢
* @Date 2024/11/15
*/
public static void sqlLog(PreparedStatement preparedStatement){
if (preparedStatement!=null) {
String executedSql = preparedStatement.toString(); // 包含实际的参数值
System.out.println("执行的SQL语句: " + executedSql); // 打印填充参数后的SQL语句
}else{
System.out.println("预处理语句为空");
}
}
public static void sqlLog(Statement statement){
if (statement!=null) {
String executedSql = statement.toString();
System.out.println("执行的SQL语句: " + executedSql);
}else{
System.out.println("语句为空");
}
}
// Tomcat较高版本会有内存泄漏警告,目前还没找到较好的解决办法
public static void closeDataSource() {
try {
while(DriverManager.getDrivers().hasMoreElements()) {
DriverManager.deregisterDriver(DriverManager.getDrivers().nextElement());
}
} catch (SQLException e) {
throw new RuntimeException("JDBC驱动取消注册过程异常",e);
}
// 尝试停止MySQL的AbandonedConnectionCleanupThread
System.out.println("尝试关闭报错线程");
AbandonedConnectionCleanupThread.checkedShutdown();
if (dataSource != null) {
dataSource.close();
dataSource = null;
}
}
}
额外说明
说一下最后的closeDataSource(),这个方法是为了解决这个日志警告。
Tomcat 高版本能够检测到内存泄漏情况,但我还没弄懂这个线程没有正常关闭的原因。
注册了JDBC驱动程序 [com.alibaba.druid.proxy.DruidDriver],
但在Web应用程序停止时无法注销它。 为防止内存泄漏,JDBC驱动程序已被强制取消注册。
16-Nov-2024 10:59:37.848 警告 [RMI TCP Connection(4)-127.0.0.1] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesJdbc Web应用程序 [test3_war_exploded] 注册了JDBC驱动程序 [com.mysql.cj.jdbc.Driver],
但在Web应用程序停止时无法注销它。 为防止内存泄漏,JDBC驱动程序已被强制取消注册。
16-Nov-2024 10:59:38.284 信息 [mysql-cj-abandoned-connection-cleanup] org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading 非法访问:此Web应用程序实例已停止。无法加载[]。为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
java.lang.IllegalStateException: 非法访问:
此Web应用程序实例已停止。无法加载[]。
为了调试以及终止导致非法访问的线程,将抛出以下堆栈跟踪。
at org.apache.catalina.loader.WebappClassLoaderBase.checkStateForResourceLoading(WebappClassLoaderBase.java:1432)
at org.apache.catalina.loader.WebappClassLoaderBase.getResource(WebappClassLoaderBase.java:1057)
at com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.checkThreadContextClassLoader(AbandonedConnectionCleanupThread.java:123)
at com.mysql.cj.jdbc.AbandonedConnectionCleanupThread.run(AbandonedConnectionCleanupThread.java:90)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
需要再创建一个监听器
监听到web程序快关闭时,手动去取消注册驱动和终止报错线程。
import com.budiu.util.DruidMySQLUtil;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
@WebListener
public class DataSourceServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
//销毁数据源
System.out.println("正在销毁数据库连接池资源");
DruidMySQLUtil.closeDataSource();
}
}
工具类用法
具体用法类似这样
@Override
public boolean insertOne(Emp emp) {
String sql="insert into emp(empno,ename,hiredate,job,sal,comm) values (?,?,?,?,?,?)";
PreparedStatement preparedStatement=null;
Connection connection=null;
try {
//获取连接
connection=DruidMySQLUtil.getConnection();
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1,emp.getNo());
preparedStatement.setString(2, emp.getName());
preparedStatement.setDate(3, (Date) emp.getHireDate());
preparedStatement.setString(4, emp.getJob());
preparedStatement.setDouble(5,emp.getSalary());
preparedStatement.setString(6,emp.getComm());
//打印SQL语句
DruidMySQLUtil.sqlLog(preparedStatement);
//进行sql语句执行
return preparedStatement.execute();
} catch (SQLException e) {
throw new RuntimeException(e);
}finally {
//关闭资源
DruidMySQLUtil.closeAll(connection,preparedStatement);
}
}
Tomcat Manager
Tomcat其实还提供了一个管理器,本人觉得一般般,但是后期远程部署啥的应该会有点用吧。
当你启动Tomcat管理器后,可以通过访问
localhost:8080/manager/html
这里还需要进行用户登录,Tomcat默认是不会允许你登录的,你得先去配置文件改一下
在conf目录的tomcat-user.xml里去掉注释,然后随便改个密码
登录后
除了主页比较有用外,它还能给你粗略看一下服务器的运行情况,可以监测自己的服务有没有正常运行
自制Servlet了解web程序情况
Tomcat的Manager并只能理解web程序的整体情况,所以需要自己写几个Servlet去了解web程序的具体情况,可以直接复制粘贴到自己的Servlet目录就能用。
了解web根目录下的内容
可以根据这个Servlet看自己的jar是否正常导入
import java.io.File;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/listFiles")
public class ListFilesServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
File webRoot = new File(getServletContext().getRealPath("/"));
response.setContentType("text/html");
java.io.PrintWriter out = response.getWriter();
out.println("<html><head>");
out.println("<style>");
out.println(".folder { cursor: pointer; }");
out.println(".hidden { display: none; }");
out.println("</style>");
out.println("<script>");
out.println("function toggleFolder(folderId) {");
out.println(" var folder = document.getElementById(folderId);");
out.println(" if (folder.classList.contains('hidden')) {");
out.println(" folder.classList.remove('hidden');");
out.println(" } else {");
out.println(" folder.classList.add('hidden');");
out.println(" }");
out.println("}");
out.println("</script>");
out.println("</head><body>");
out.println("<h1>Files in Web Root</h1>");
out.println("<ul>");
listFilesAndDirs(webRoot, out, false);
out.println("</ul>");
out.println("</body></html>");
}
private void listFilesAndDirs(File dir, java.io.PrintWriter out, boolean isWebInf) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
String folderId = "folder" + file.getName().hashCode();
// 默认展开Web-INF目录
if (file.getName().equals("WEB-INF") || isWebInf) {
out.println("<li><strong class='folder' οnclick='toggleFolder(\"" + folderId + "\")'>" + file.getName() + "</strong> (directory)<ul id='" + folderId + "'"+">");
listFilesAndDirs(file, out, true); // Recursive call with isWebInf set to true
out.println("</ul></li>");
} else {
// Only show the directory name without expanding it
out.println("<li>" + file.getName() + " (directory)</li>");
}
} else {
// Only list files if we're inside WEB-INF
if (isWebInf) {
out.println("<li>" + file.getName() + "</li>");
}
}
}
}
}
}
访问这个Sevlet返回的结果像这样,这些树形结构是可以点击展开的,但我们一般只关注WEB-INF的目录下有没有什么少的部分
查看Servelt名称和路由信息
/**
* 遍历web程序里的Servlet和它的路由信息
*
* @author 罗不丢
* @since 2024/11/15
*/
@WebServlet("/listServlets")
public class ListServletsServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 获取ServletContext对象
ServletContext context = getServletContext();
// 获取应用的上下文路径
String contextPath = context.getContextPath();
// 获取所有已注册的Servlet
Map<String, ? extends ServletRegistration> servlets = context.getServletRegistrations();
// 设置响应类型为HTML
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 输出HTML页面的头部
out.println("<html><head><title>Servlet List</title>");
out.println("<style>body { font-size: 28px; }</style>"); // 设置字体大小为18px
out.println("</head><body>");
out.println("<h2>注意部分链接可能需要表单提交参数才能访问成功,这里仅作信息展示</h2>");
// 遍历所有Servlet并生成相应的URL
out.println("<ul>");
for (Map.Entry<String, ? extends ServletRegistration> entry : servlets.entrySet()) {
String servletName = entry.getKey();
ServletRegistration servlet = entry.getValue();
for (String urlPattern : servlet.getMappings()) {
// 构建完整的URL,包括上下文路径
String fullUrl = contextPath + urlPattern;
out.println("<li><a href=\"" + fullUrl + "\">" +"Servlet名称: "+servletName + " - 路由信息: " + fullUrl + "</a></li>");
}
}
out.println("</ul>");
// 输出HTML页面的尾部
out.println("</body></html>");
}
}
返回的结果类似这样
总结
以后的框架可能会将这些基础代码封装再封装,我们只需要调用某个方法就能完成一堆现在繁琐的操作。所以也不用担心以后的代码有多难写,代码肯定是越写越简单的,这也是面向对象编程的意义所在。