Java 轮子
HTTP 组件库
JDK HttpURLConnection
使用 java.net.HttpURLConnection 发起 HTTP 请求最大的优点是不需要引入额外的依赖,但是使用起来非常繁琐,也缺乏连接池管理、域名机械控制等特性支持。
简单的 POST 请求实例
apachepublic class HttpUrlConnectionDemo {
public static void main(String[] args) throws IOException {
String urlString = "https://httpbin.org/post";
String bodyString = "xxx";
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
os.write(bodyString.getBytes("utf-8"));
os.flush();
os.close();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
System.out.println("响应内容:" + sb.toString());
} else {
System.out.println("响应码:" + conn.getResponseCode());
}
}
}Apache HttpClient
不过 HttpURLConnection 不支持 HTTP/2.0,为了解决这个问题,Java 9 的时候官方的标准库增加了一个更高级别的 HttpClient,再发起 POST 请求就显得高大上多了,不仅支持异步,还支持顺滑的链式调用。
public class HttpClientDemo {
public static void main(String[] args) throws URISyntaxException {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI("https://postman-echo.com/post"))
.headers("Content-Type", "text/plain;charset=UTF-8")
.POST(HttpRequest.BodyPublishers.ofString("二哥牛逼"))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();
}
}Apache HttpComponents HttpClient
基本依赖项:
<!--HttpCore 提供了低级别的 HTTP 传输和协议处理功能,HttpClient 依赖于它。-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.15</version>
</dependency>
<!--HttpClient 这是核心库,用于发送 HTTP 请求和接收响应。-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>简单的 POST 请求实例
public class HttpComponentsDemo {
public static void main(String[] args) throws IOException, IOException, ParseException {
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost("https://httpbin.org/post");
List<NameValuePair> nvps = new ArrayList<>();
nvps.add(new BasicNameValuePair("name", "二哥"));
httpPost.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8")));
try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
System.out.println(response2.getStatusLine() + " " + EntityUtils.toString(response2.getEntity()));
}
}
}
}OkHttp
OkHttp 是一个执行效率比较高的 HTTP 客户端:
基本依赖项:
<!--okhttp3-->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>简单的 POST 请求实例
public class OkHttpPostDemo {
public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
OkHttpClient client = new OkHttpClient();
String post(String url, String json) throws IOException {
RequestBody body = RequestBody.create(json, JSON);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
try (Response response = client.newCall(request).execute()) {
return response.body().string();
}
}
public static void main(String[] args) throws IOException {
OkHttpPostDemo example = new OkHttpPostDemo();
String json = "{'name':'二哥'}";
String response = example.post("https://httpbin.org/post", json);
System.out.println(response);
}
}单元测试框架
Junit
单元测试可以确保你编写的代码是符合软件需求和遵循开发规范的。单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次能够达到代码覆盖率 100% 的测试,是整个软件测试过程的基础和前提。可以这么说,单元测试的性价比是最好的。

最初的测试,要测试 fact() 方法正确性,你在 main() 方法中编写了一段测试代码。
public class Main {
public static long fact(long n) {
long r = 1;
for (long i = 1; i <= n; i++) {
r = r * i;
}
return r;
}
public static void main(String[] args) {
if (fact(3) == 6) {
System.out.println("通过");
} else {
System.out.println("失败");
}
}
}缺点:
- 测试代码没有和源代码分开。
- 不够灵活,很难编写一组通用的测试代码。
- 无法自动打印出预期和实际的结果,没办法比对。
使用 JUnit 可以非常简单地组织测试代码,并随时运行它们,还能给出准确的测试报告,让你在最短的时间内发现自己编写的代码到底哪里出了问题。
按下 alt+insert 键,菜单选择 test

IDEA 会自动在 测试代码目录 的当前类所在的包下生成一个类名带 Test(惯例)的测试类。


IDEA 会提示导入依赖包。
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>Junit 要求 @Test 注解,带有 @Test 的方法识别为测试方法。在测试方法内部,可以使用 assertEquals() 对期望的值和实际的值进行比对。
@Test
void fact() {
assertEquals(1, Main.fact(1));
assertEquals(2, Main.fact(2));
assertEquals(6, Main.fact(3));
assertEquals(100, Main.fact(5));
}瞻前顾后
勾选 setUp() 和 tearDown()
@BeforeEach
void setUp() {
}
@AfterEach
void tearDown() {
}@BeforeEach 的 setUp() 方法会在运行每个 @Test 方法之前运行;@AfterEach 的 tearDown() 方法会在运行每个 @Test 方法之后运行。
与之对应的还有 @BeforeAll 和 @AfterAll,与 @BeforeEach 和 @AfterEach 不同的是,All 通常用来初始化和销毁静态变量。
异常测试
对于 Java 程序来说,异常处理也非常的重要。对于可能抛出的异常进行测试,本身也是测试的一个重要环节。
@Test
void factIllegalArgument() {
assertThrows(IllegalArgumentException.class, new Executable() {
@Override
public void execute() throws Throwable {
Factorial.fact(-2);
}
});
}忽略测试
有时候,由于某些原因,某些方法产生了 bug,需要一段时间去修复,在修复之前,该方法对应的测试用例一直是以失败告终的,为了避免这种情况,我为你提供了 @Disabled 注解。
class DisabledTestsDemo {
@Disabled("该测试用例不再执行,直到编号为 43 的 bug 修复掉")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}条件测试
有时候,需要在某些条件下运行测试方法,有些条件下不运行测试方法。针对这场使用场景,Junit 提供了条件测试。
不同的操作系统,可能需要不同的测试用例
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}不同的 Java 运行环境,可能也需要不同的测试用例。
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
// ...
}JSON 解析库
阿里巴巴 fastjson
依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>toJSONString() 方法用来序列化,parseObject() 方法用来反序列化。
class People {
private int age;
private String name;
private Date birthday;
public static void main(String[] args) {
People people = new People();
people.setAge(0);
people.setName("丁丁");
people.setBirthday(new Date());
String json = JSON.toJSONString(people);
System.out.println(json);
}
}
// {"age":18,"birthday":1776433586134,"name":"沉默王二"}@JSONField 注解,name 用来指定字段的名称,format 用来指定日期格式,serialize 和 deserialize 用来指定是否序列化和反序列化。
public class People {
@JSONField(name = "Years of life")
private int age;
@JSONField(serialize = false,deserialize = true)
private String name;
@JSONField(format = "yyyy年MM月dd日")
private Date birthday;
public static void main(String[] args) {
People people = new People();
people.setAge(0);
people.setName("丁丁");
people.setBirthday(new Date());
String json = JSON.toJSONString(people);
System.out.println(json);
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "丁丁");
jsonObject.put("age", 0);
jsonObject.put("birthday", new Date());
String json2 = JSON.toJSONString(jsonObject);
People people2 = JSON.parseObject(json2, People.class);
System.out.println(people2);
}
}
//{"Years of life":0,"birthday":"2026年04月17日"}
//People{age=0, name='丁丁', birthday=Fri Apr 17 21:53:54 CST 2026}为了满足更多个性化的需求, SerializerFeature 类中定义了很多特性,你可以在调用 toJSONString() 方法的时候进行指定。
- PrettyFormat,让 JSON 格式打印得更漂亮一些
- WriteClassName,输出类名
- UseSingleQuotes,key 使用单引号
- WriteNullListAsEmpty,List 为空则输出 []
- WriteNullStringAsEmpty,String 为空则输出“”
Google Gson
Spring Boot 默认 Jackson
日志框架
在项目开发中,日志十分的重要,不管是记录运行情况还是定位线上问题,都离不开对日志的分析。日志记录了系统行为的时间、地点、状态等相关信息,能够帮助我们了解并监控系统状态,在发生错误或者接近某种危险状态时能够及时提醒我们处理,同时在系统产生问题时,能够帮助我们快速的定位、诊断并解决问题。
在本地环境下,使用 System.out.println() 打印日志是没问题的,可以在控制台看到信息。但如果是在生产环境下的话,控制台打印出的信息并没有保存到日志文件中,只能即时查看。所以就需要更高级的日志记录 API(比如 Log4j 和 java.util.logging)。
错误的日志记录方式会影响性能
因为日志记录的次数越多,意味着执行文件 IO 操作的次数就越多,这也就意味着会影响到程序的性能
Log4j
java.util.logging 属于原生的日志 API,Log4j 属于第三方类库,建议使用 Log4j,因为 Log4j 更好用。Log4j 不需要重新启动 Java 程序就可以调整日志的记录级别,非常灵活。Log4j 可以通过 log4j.properties 文件来配置 Log4j 的日志级别、输出环境、日志文件的记录方式。
Log4j 还是线程安全的,可以在多线程的环境下放心使用。
基于 Log4j 的日志级别
- DEBUG 的级别最低,当需要打印调试信息的话,就用这个级别,不建议在生产环境下使用。
- INFO 的级别高一些,当一些重要的信息需要打印的时候,就用这个。
- WARN,用来记录一些警告类的信息,比如说客户端和服务端的连接断开了,数据库连接丢失了。
- ERROR 比 WARN 的级别更高,用来记录错误或者异常的信息。
- FATAL,当程序出现致命错误的时候使用,这意味着程序可能非正常中止了。
- OFF,最高级别,意味着所有消息都不会输出了。
java.util.logging 的使用方式:
package com.itwanger;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
public class JavaUtilLoggingDemo {
public static void main(String[] args) throws IOException {
Logger logger = Logger.getLogger("test");
FileHandler fileHandler = new FileHandler("javautillog.txt");
fileHandler.setFormatter(new SimpleFormatter());
logger.addHandler(fileHandler);
logger.info("细小的信息");
}
}程序运行后会在 target 目录下生成一个名叫 javautillog.txt 的文件

Log4j 依赖
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>在 resources 目录下创建 log4j.properties 文件
### 设置###
log4j.rootLogger = debug,stdout,D,E
### 输出信息到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=debug.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = debug.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
### 输出ERROR 级别以上的日志到=error.log ###
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File =error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n配置根 Logger
log4j.rootLogger = [ level ] , appenderName, appenderName, …level 就是日志的优先级,从高到低依次是 ERROR、WARN、INFO、DEBUG。如果这里定义的是 INFO,那么低级别的 DEBUG 日志信息将不会打印出来。
appenderName 就是指把日志信息输出到什么地方,可以指定多个地方,当前的配置文件中有 3 个地方,分别是 stdout、D、E。
配置日志输出的目的地
log4j.appender.appenderName = fully.qualified.name.of.appender.class log4j.appender.appenderName.option1 = value1 … log4j.appender.appenderName.option = valueNLog4j 提供的目的地有下面 5 种:
- org.apache.log4j.ConsoleAppender:控制台
- org.apache.log4j.FileAppender:文件
- org.apache.log4j.DailyRollingFileAppender:每天产生一个文件
- org.apache.log4j.RollingFileAppender:文件大小超过阈值时产生一个新文件
- org.apache.log4j.WriterAppender:将日志信息以流格式发送到任意指定的地方
配置日志信息的格式
log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class log4j.appender.appenderName.layout.option1 = value1 … log4j.appender.appenderName.layout.option = valueNLog4j 提供的格式有下面 4 种:
- org.apache.log4j.HTMLLayout:HTML 表格
- org.apache.log4j.PatternLayout:自定义
- org.apache.log4j.SimpleLayout:包含日志信息的级别和信息字符串
- org.apache.log4j.TTCCLayout:包含日志产生的时间、线程、类别等等信息
自定义格式的参数如下所示:
- %m:输出代码中指定的消息
- %p:输出优先级
- %r:输出应用启动到输出该日志信息时花费的毫秒数
- %c:输出所在类的全名
- %t:输出该日志所在的线程名
- %n:输出一个回车换行符
- %d:输出日志的时间点
- %l:输出日志的发生位置,包括类名、线程名、方法名、代码行数,比如:
method:com.itwanger.Log4jDemo.main(Log4jDemo.java:14)
示例
package com.itwanger;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
public class Log4jDemo {
private static final Logger logger = LogManager.getLogger(Log4jDemo.class);
public static void main(String[] args) {
// 记录debug级别的信息
logger.debug("debug.");
// 记录info级别的信息
logger.info("info.");
// 记录error级别的信息
logger.error("error.");
}
}程序运行后会在 target 目录下生成两个文件


技巧
对于 DEBUG 级别的日志来说,一定要使用下面的方式来记录:
javaif(logger.isDebugEnabled()){ logger.debug("DEBUG 是开启的"); }如果我们在打印日志信息的时候需要附带一个方法去获取参数值,就像下面这样:
javalogger.debug("用户名是:" + getName());假如
getName()方法需要耗费的时间长达 6 秒,那完了!尽管配置文件里的日志级别定义的是 INFO,getName()方法仍然会倔强地执行 6 秒,完事后再debug(),这就很崩了!明明 INFO 的时候debug()是不执行的,意味着getName()也不需要执行的慎重选择日志信息的打印级别,打印得太多,会影响到程序的性能。
使用 Log4j 而不是
System.out等使用 log4j.properties 文件来配置日志
在打印日志的时候带上类的全名和线程名,在多线程环境下,便于定位问题
打印日志信息的时候尽量要完整,
要对日志信息加以区分,把某一类的日志信息在输出的时候加上前缀,
不要在日志文件中打印敏感信息
Logback
Spring Boot 的默认日志框架使用的是 Logback。
SLF4J
SLF4J 是 Simple Logging Facade for Java 的缩写(for≈4),也就是简易的日志门面,以外观模式(Facade pattern,一种设计模式,为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用)实现,支持 java.util.logging、Log4J 和 Logback。
Log4j2
Apache Log4j2 是一个开源的日志记录组件,使用非常的广泛。
在工程中以易用方便代替了 System.out 等打印语句,它是JAVA下最流行的日志输入工具。工Log4j2主要由几个重要的组件构成:
日志信息的优先级,日志信息的优先级从高到低有
TRACE < DEBUG < INFO < WARN < ERROR < FATALTRACE:追踪,是最低的日志级别,相当于追踪程序的执行
DEBUG:调试,一般在开发中,都将其设置为最低的日志级别
INFO:信息,输出重要的信息,使用较多
WARN:警告,输出警告的信息
ERROR:错误,输出错误信息
FATAL:严重错误
日志信息输出目的地:控制台 or 文件
日志信息输出格式:显示内容
引入依赖
SLF4J 的 API 编写日志代码,但底层由 Log4j2 来实际执行日志输出。
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>JDBC
Druid
Druid 是阿里巴巴开源的数据库连接池实现,提供了强大的性能监控和扩展功能。
引入依赖
<!-- JDBC 驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.31</version>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>连接池配置
# Druid连接池配置
druid.driverClassName=com.mysql.cj.jdbc.Driver
druid.url=jdbc:mysql://192.168.1.117:33306/tintin_test?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf-8
druid.username=root
druid.password=123456
# 初始连接数
druid.initialSize=5
# 最小空闲连接数
druid.minIdle=10
# 最大活跃连接数
druid.maxActive=20
# 获取连接超时时间(毫秒)
druid.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
druid.timeBetweenEvictionRunsMillis=60000
# 配置连接在池中的最小生存时间
druid.minEvictableIdleTimeMillis=300000
# 检测连接是否有效的SQL
druid.validationQuery=SELECT 1
# 是否开启空闲连接检测
druid.testWhileIdle=true
# 是否在获取连接时检测连接有效性
druid.testOnBorrow=false
# 是否在归还连接时检测连接有效性
druid.testOnReturn=false
# 是否缓存PreparedStatement
druid.poolPreparedStatements=true
# 缓存PreparedStatement的最大数量
druid.maxPoolPreparedStatementPerConnectionSize=20JDBC 工具类
package com.tintin.tintinheadlinesys.util;
/**
* JDBC工具类 - 基于Druid连接池
*/
public class JDBCUtils {
private static final Logger logger = LoggerFactory.getLogger(JDBCUtils.class);
private static DataSource dataSource = null;
private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// 静态代码块加载配置文件和初始化连接池
static {
try {
// 加载配置文件
Properties props = new Properties();
InputStream is = JDBCUtils.class.getClassLoader()
.getResourceAsStream("jdbc.properties");
if (is == null) {
throw new RuntimeException("未找到jdbc.properties配置文件");
}
props.load(is);
// 创建Druid数据源
dataSource = DruidDataSourceFactory.createDataSource(props);
logger.info("Druid连接池初始化成功");
} catch (Exception e) {
logger.error("Druid连接池初始化失败", e);
throw new ExceptionInInitializerError("数据库连接池初始化失败: " + e.getMessage());
}
}
/**
* 获取数据源
*/
public static DataSource getDataSource() {
return dataSource;
}
/**
* 获取数据库连接
*/
public static Connection getConnection() throws SQLException {
// 先从ThreadLocal中获取
Connection conn = threadLocal.get();
if (conn == null) {
// 从连接池获取连接
conn = dataSource.getConnection();
threadLocal.set(conn);
}
return conn;
}
/**
* 开启事务
*/
public static void beginTransaction() throws SQLException {
Connection conn = getConnection();
if (conn.getAutoCommit()) {
conn.setAutoCommit(false);
}
}
/**
* 提交事务
*/
public static void commitTransaction() throws SQLException {
Connection conn = threadLocal.get();
if (conn != null && !conn.getAutoCommit()) {
conn.commit();
}
}
/**
* 回滚事务
*/
public static void rollbackTransaction() throws SQLException {
Connection conn = threadLocal.get();
if (conn != null && !conn.getAutoCommit()) {
conn.rollback();
}
}
/**
* 关闭连接并移除ThreadLocal
*/
public static void closeConnection() {
Connection conn = threadLocal.get();
if (conn != null) {
try {
if (!conn.getAutoCommit()) {
conn.setAutoCommit(true);
}
conn.close();
} catch (SQLException e) {
logger.error("关闭连接失败", e);
} finally {
threadLocal.remove();
}
}
}
/**
* 关闭资源
*/
public static void closeResource(ResultSet rs, PreparedStatement pstmt, Connection conn) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
logger.error("关闭ResultSet失败", e);
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
logger.error("关闭PreparedStatement失败", e);
}
try {
if (conn != null && conn != threadLocal.get()) {
conn.close();
}
} catch (SQLException e) {
logger.error("关闭Connection失败", e);
}
}
/**
* 关闭资源(简化版,自动处理ThreadLocal连接)
*/
public static void closeResource(ResultSet rs, PreparedStatement pstmt) {
closeResource(rs, pstmt, null);
}
}通用 DAO 基类
/**
* 通用DAO基类
*/
public class BaseDAO<T> {
/**
* 通用的增删改方法
*/
public int update(String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
int rows = 0;
try {
conn = JDBCUtils.getConnection();
pstmt = conn.prepareStatement(sql);
// 设置参数
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
return pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(null, pstmt);
}
return rows;
}
/**
* 查询单个对象
*/
public T queryForObject(Class<T> clazz, String sql, Object... params) {
List<T> list = queryForList(clazz, sql, params);
return list.isEmpty() ? null : list.get(0);
}
/**
* 查询多个对象
*/
public List<T> queryForList(Class<T> clazz, String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
List<T> list = new ArrayList<>();
try {
conn = JDBCUtils.getConnection();
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
rs = pstmt.executeQuery();
// 获取结果集元数据
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
if (Map.class.isAssignableFrom(clazz)) {
Constructor<T> declaredConstructor;
T entity;
Map<String, Object> map;
try {
declaredConstructor = clazz.getDeclaredConstructor();
entity = declaredConstructor.newInstance();
map = (Map<String, Object>) entity;
} catch (NoSuchMethodException e) {
map = new HashMap<>();
entity = (T) map;
}
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
Object value = rs.getObject(i);
map.put(toCamelCase(columnName), value);
}
list.add(entity);
continue;
}
T entity = clazz.getDeclaredConstructor().newInstance();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
Object value = rs.getObject(i);
// 使用反射设置属性值
Field field = clazz.getDeclaredField(toCamelCase(columnName));
field.setAccessible(true);
field.set(entity, value);
}
list.add(entity);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(rs, pstmt);
}
return list;
}
/**
* 获取单个值(如count、sum等)
*/
public <E> E getSingleValue(String sql, Object... params) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConnection();
pstmt = conn.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
rs = pstmt.executeQuery();
if (rs.next()) {
return (E) rs.getObject(1);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JDBCUtils.closeResource(rs, pstmt);
}
return null;
}
/**
* 将数据库字段名转换为驼峰命名
*/
private String toCamelCase(String columnName) {
StringBuilder result = new StringBuilder();
String[] parts = columnName.split("_");
for (int i = 0; i < parts.length; i++) {
if (i == 0) {
result.append(parts[i]);
} else {
result.append(parts[i].substring(0, 1).toUpperCase())
.append(parts[i].substring(1).toLowerCase());
}
}
return result.toString();
}
}ORM 框架
对象关系映射(Object Relational Mapping,简称 ORM)是一种程序设计技术,用于在面向对象编程语言和关系数据库之间进行数据转换。ORM 的核心思想是通过创建一个“虚拟对象数据库”,使开发者可以使用面向对象的方式操作数据库,而不需要直接编写复杂的 SQL 语句。
加密
MessageDigest
MessageDigest 类是 Java 提供的一个安全特性,用于生成数据的哈希值,如 MD5 或 SHA 算法。这个类可以接受任意大小的数据输入,并产生一个固定长度的哈希值,通常被称为数据的“数字指纹”。
/**
* 加密工具类
* 提供MD5、SHA系列、Base64等常用加密方法
*/
public class EncryptUtils {
private static final String CHARSET_UTF8 = "UTF-8";
/*********************************** 加密 ************************************/
/**
* MD5加密(返回32位小写十六进制字符串)
* @param input 待加密字符串
* @return MD5加密后的字符串
*/
public static String md5(String input) {
return hash(input, "MD5");
}
/**
* 带盐值的MD5加密
* @param input 待加密字符串
* @param salt 盐值
* @return MD5加密后的字符串
*/
public static String md5WithSalt(String input, String salt) {
if (input == null) {
return null;
}
String toEncrypt = input + (salt == null ? "" : salt);
return md5(toEncrypt);
}
/**
* 双重MD5加密
* @param input 待加密字符串
* @return 双重MD5加密后的字符串
*/
public static String doubleMd5(String input) {
if (input == null) {
return null;
}
return md5(md5(input));
}
/**
* SHA-1加密(返回40位小写十六进制字符串)
* @param input 待加密字符串
* @return SHA-1加密后的字符串
*/
public static String sha1(String input) {
return hash(input, "SHA-1");
}
/**
* SHA-256加密(返回64位小写十六进制字符串)
* @param input 待加密字符串
* @return SHA-256加密后的字符串
*/
public static String sha256(String input) {
return hash(input, "SHA-256");
}
/**
* SHA-512加密(返回128位小写十六进制字符串)
* @param input 待加密字符串
* @return SHA-512加密后的字符串
*/
public static String sha512(String input) {
return hash(input, "SHA-512");
}
/**
* 通用哈希加密方法
* @param input 待加密字符串
* @param algorithm 算法名称(MD5、SHA-1、SHA-256等)
* @return 加密后的十六进制字符串
*/
private static String hash(String input, String algorithm) {
if (input == null || input.isEmpty()) {
return null;
}
try {
MessageDigest md = MessageDigest.getInstance(algorithm);
byte[] digest = md.digest(input.getBytes(CHARSET_UTF8));
return bytesToHex(digest);
} catch (Exception e) {
throw new RuntimeException(algorithm + "加密失败", e);
}
}
/**
* 字节数组转十六进制字符串
* @param bytes 字节数组
* @return 十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 0xFF);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/*********************************** Base64 ************************************/
/**
* Base64编码
* @param input 待编码字符串
* @return Base64编码后的字符串
*/
public static String base64Encode(String input) {
if (input == null) {
return null;
}
try {
return Base64.getEncoder().encodeToString(input.getBytes(CHARSET_UTF8));
} catch (Exception e) {
throw new RuntimeException("Base64编码失败", e);
}
}
/**
* Base64解码
* @param input Base64编码的字符串
* @return 解码后的字符串
*/
public static String base64Decode(String input) {
if (input == null) {
return null;
}
try {
byte[] decodedBytes = Base64.getDecoder().decode(input);
return new String(decodedBytes, CHARSET_UTF8);
} catch (Exception e) {
throw new RuntimeException("Base64解码失败", e);
}
}
/*********************************** 验证 ************************************/
/**
* 验证MD5
* @param plainText 原文
* @param md5Text MD5密文
* @return 是否匹配
*/
public static boolean verifyMd5(String plainText, String md5Text) {
return verify(plainText, md5Text, "MD5");
}
/**
* 验证SHA-256
* @param plainText 原文
* @param sha256Text SHA-256密文
* @return 是否匹配
*/
public static boolean verifySha256(String plainText, String sha256Text) {
return verify(plainText, sha256Text, "SHA-256");
}
/**
* 验证原文与加密后的密文是否匹配(支持MD5、SHA系列)
* @param plainText 原文
* @param encryptedText 加密后的密文
* @param algorithm 算法名称
* @return 是否匹配
*/
public static boolean verify(String plainText, String encryptedText, String algorithm) {
if (plainText == null || encryptedText == null) {
return false;
}
String encrypted = hash(plainText, algorithm);
return encrypted != null && encrypted.equals(encryptedText);
}
}Java 工具类库
Hutool
Hutool是一个小而全的 Java 工具类库,通过静态方法封装,降低相关 API 的学习成本,提高工作效率,使 Java 拥有函数式语言般的优雅,让 Java 语言也可以“甜甜的”。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-ai</artifactId>
<version>5.8.46</version>
</dependency>常用代码片段
根目录资源加载
public class LoadResourceDemo {
// 从根目录找 无需加 /
public InputStream getStreamByClassLoader(String path) {
return this.getClass().getClassLoader().getResourceAsStream(path);
}
public URL getURLByClassLoader(String path) {
return this.getClass().getClassLoader().getResource(path);
}
// 不加 / 表示从当前类所在的包找, 加 / 表示从根目录找
public InputStream getStreamByClass(String path) {
return this.getClass().getResourceAsStream(path);
}
public URL getURLByClass(String path) {
return this.getClass().getResource(path);
}
// 以加载properties文件为例
public static void main(String[] args) throws IOException {
LoadResourceDemo demo = new LoadResourceDemo();
InputStream inputStream = demo.getStreamByClassLoader("jdbc.properties");
Properties properties = new Properties();
properties.load(inputStream);
System.out.println(properties);
}
}Web 项目公共 JSON 响应结果类
package com.tintin.demoschedulesystem.common;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 统一 API 响应结果封装
* @param <T> 数据类型
*/
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
// 默认成功码
private static final int DEFAULT_SUCCESS_CODE = 200;
// 默认失败码
private static final int DEFAULT_ERROR_CODE = 500;
// 状态码
private int code;
// 提示消息
private String message;
// 返回数据
private T data;
/**
* 无参构造(JSON 序列化需要)
*/
public Result() {
}
/**
* 全参构造(私有,通过工厂方法调用)
*/
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
// ==================== 成功响应 ====================
public static <T> Result<T> success() {
return new Result<>(DEFAULT_SUCCESS_CODE, "success", null);
}
public static <T> Result<T> success(String message) {
return new Result<>(DEFAULT_SUCCESS_CODE, message, null);
}
public static <T> Result<T> success(T data) {
return new Result<>(DEFAULT_SUCCESS_CODE, "success", data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(DEFAULT_SUCCESS_CODE, message, data);
}
// ==================== 失败响应 ====================
public static <T> Result<T> error() {
return new Result<>(DEFAULT_ERROR_CODE, "error", null);
}
public static <T> Result<T> error(String message) {
return new Result<>(DEFAULT_ERROR_CODE, message, null);
}
public static <T> Result<T> error(T data) {
return new Result<>(DEFAULT_ERROR_CODE, "error", data);
}
public static <T> Result<T> error(String message, T data) {
return new Result<>(DEFAULT_ERROR_CODE, message, data);
}
// ==================== 链式设置(方便进一步定制) ====================
public Result<T> code(int code) {
this.code = code;
return this;
}
public Result<T> message(String message) {
this.message = message;
return this;
}
public Result<T> data(T data) {
this.data = data;
return this;
}
// ==================== Getter / Setter ====================
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "Result{" +
"code=" + code +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}Web 项目跨域处理过滤器
@WebFilter("/*")
public class CrossFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT,OPTIONS, DELETE, HEAD");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "access-control-allow-origin, authority, content-type, version-info, X-Requested-With");
// 如果是跨域预检请求,则直接在此响应200业务码
if(httpServletRequest.getMethod().equalsIgnoreCase("OPTIONS")){
WebUtils.writeJson(httpServletResponse,Result.success());
}else{
// 非预检请求,放行即可
filterChain.doFilter(servletRequest, servletResponse);
}
}
}在 MVC 框架中,只需要添加 @CrossFilter 注解即可
合并数组
private String[] insertArr(String[] arr, int index, List<String> list) {
String[] newArr = new String[arr.length + list.size()];
for (int j = 0; j < newArr.length; j++) {
if (j < index ) {
newArr[j] = arr[j];
} else if (j < list.size() + index) {
newArr[j] = list.get(j - index);
} else {
newArr[j] = arr[j - list.size()];
}
}
return newArr;
}数组分页
/**
* 按指定大小,分隔集合,将集合按规定个数分为n个部分
*/
public static <T> List<List<T>> splitList(List<T> list, int len) {
if (list == null || list.isEmpty() || len < 1) {
return Collections.emptyList();
}
List<List<T>> result = new ArrayList<>();
int size = list.size();
int count = (size + len - 1) / len;
for (int i = 0; i < count; i++) {
List<T> subList = list.subList(i * len, ((i + 1) * len > size ? size : len * (i + 1)));
result.add(subList);
}
return result;
}下划线<->驼峰
/** 下划线转驼峰 */
public static String lineToHump(String str) {
str = str.toLowerCase();
Matcher matcher = linePattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
}
matcher.appendTail(sb);
return sb.toString();
}
/** 驼峰转下划线,效率比上面高 */
public static String humpToLine2(String str) {
Matcher matcher = humpPattern.matcher(str);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
matcher.appendReplacement(sb, "_" + matcher.group(0).toLowerCase());
}
matcher.appendTail(sb);
return sb.toString();
}Spring 工具类封装
SpringDataRedis
设置缓存,并且可以解决缓存穿透、缓存击穿问题。
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
// 设置缓存
public void set(String key, Object object, Long time, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(object), time, timeUnit);
}
// 设置带逻辑过期的缓存
public void setWithLogicalExpire(String key, Object object, Long time, TimeUnit timeUnit) {
RedisData redisData = new RedisData();
redisData.setData(object);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
@Data
class RedisData {
private Object data;
private LocalDateTime expireTime;
}
// 解决缓存穿透的查询,通过 null 值表示无数据来解决
public <R, ID> R queryWithNullCache(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbCallback, Long time, TimeUnit timeUnit) {
// 查询缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 判断是否命中非空值
if (StringUtils.hasLength(json)) {
// 命中非空值则返回
return JSONUtil.toBean(json, type);
}
// 判断未命中
if (json != null) {
// 未命中,返回错误信息
return null;
}
// 命中空值
// 查询数据库
R r = dbCallback.apply(id);
// 判断数据库查询结果是否存在
if (r == null) {
// 数据库结果为空,把空字符串写入 redis
set(key, "", 5L, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 将查询结果写入 redis
set(key, r, time, timeUnit);
return r;
}
private boolean tryLock(String key) {
return stringRedisTemplate.opsForValue().setIfAbsent(key, "1");
}
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
// 解决缓存击穿的查询,通过逻辑过期解决
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbCallback, Long time, TimeUnit timeUnit) {
// 查询缓存
String key = keyPrefix + id;
String json = stringRedisTemplate.opsForValue().get(key);
// 未命中
if (!StringUtils.hasLength(json)) {
return null;
}
// 命中未过期数据
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
LocalDateTime expireTime = redisData.getExpireTime();
R r = (R) redisData.getData();
if (expireTime.isAfter(LocalDateTime.now())) {
return r;
}
// 命中已过期数据
String lockKey = keyPrefix + "lock:" + id;
boolean isLock = tryLock(lockKey);
if (isLock) {
// 开启重建缓存线程
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
R rr = dbCallback.apply(id);
setWithLogicalExpire(key, rr, time, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey);
}
});
}
// 返回旧数据
return r;
}
}开发问题
SSL证书验证
在Java中进行网络请求时出现"sun.security.validator.ValidatorException: PKIX path building failed"错误通常是由于SSL证书验证失败引起的。一般原因是:证书链不完整或证书不受信任
Java对SSL证书的信任链有严格的要求。即使URL在浏览器中可访问,但如果SSL证书不在Java的信任库中,Java程序仍然可能会出现证书验证错误,导致无法建立安全连接。
解决:证书添加Java信任库
keytool -import -alias aliDatahub -file "C:\Users\12822\Desktop\sny.crt" -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -storepass changeit