这是我参与更文挑战的第1天,活动详情查看: 更文挑战
本文正在参加「Java主题月 – Java 开发实战」,详情查看活动链接
1 前言
本文旨在解决各位热爱代码人士在Java Web的开发过程中遇到的各式各样共性问题
逃离网上相互抄袭的博客文
逃离似懂非懂, 人云亦云
用正确且合理的方式去处理我们在写代码时跳入的坑
2 内容
本次总结大致有如下模块, 后续会根据情况不断更新:
- Java技巧
- Spring Boot Web相关
- 业务中的MyBatis
- Maven打包
3 Java技巧
3.1 try-with-resources
resources, 即资源
try catch, 是Java中重要的语句之一
try-with-resources, 可以自动关闭所使用的资源
下面是在开发中经常会遇到的关于文件处理的逻辑:
public class FileUtil {
/**
* 从指针中保存文件
*
* @param filePathName 文件全路径名称
* @param pointer 指针
* @param length 指针数据长度
* @throws IOException IO异常
*/
public static void saveFile(String filePathName, Pointer pointer, int length) throws IOException {
FileOutputStream out;
try {
out = new FileOutputStream(filePathName);
out.write(pointer.getByteArray(0, length), 0, length);
} finally {
if (out != null) {
out.close();
}
}
}
}
复制代码
finally代码着实让人难受, 利用try-with-resources, 可以进行优化:
public class FileUtil {
/**
* 从指针中保存文件
*
* @param filePathName 文件全路径名称
* @param pointer 指针
* @param length 指针数据长度
* @throws IOException IO异常
*/
public static void saveFile(String filePathName, Pointer pointer, int length) throws IOException {
try (FileOutputStream out = new FileOutputStream(filePathName)) {
out.write(pointer.getByteArray(0, length), 0, length);
}
}
}
复制代码
深入思考, 我们创建的自定义资源可以使用try-with-resources吗?
只要我们去实现java.lang.AutoCloseable
或java.io.Closeable
就行
3.2 JDK8中接口的新特性
Java8中的接口新加入了default和static关键字:
public interface IPerson {
static String getName() {
return "person";
}
default Integer getSex() {
return 1;
}
String getAddress();
}
复制代码
其中的getName可以在接口级进行调用:
public class Test {
public void test() {
IPerson.getName();
}
}
复制代码
这两个特性改变了接口和抽象类的比较, 简单进行整理:
功能 | 抽象类 | 接口 |
---|---|---|
方法实现 | √ | √(但无法覆写equals/hashCode/toString) |
多继承 | × | √ |
变量 | 全部 | 只能声明static和final变量 |
3.3 枚举的比较
总共有三种比较方式:
- compareTo()
- equals()
- ==
compareTo()
compareTo返回的是枚举之间的差值, 结果为0时代表两个枚举相同, 示例代码如下(来源于W3Cschool):
enum Level {
LOW, MEDIUM, HIGH, URGENT;
}
public class Main {
public static void main(String[] args) {
Level s1 = Level.LOW;
Level s2 = Level.URGENT;
int diff = s1.compareTo(s2);
// diff = s1 - s2 = 0 - 3 = -3
System.out.println(diff);
}
}
复制代码
equals()
equals()与对象一致, 结果为true时代表两个枚举相同
但是为何呢?Java中new出来的两个完全相同的对象, 在不重写的前提下, equals()的结果为false
原因就在枚举的创建上, 我们并不是用new来实现的:
TestEnum one = TestEnum.ONE
TestEnum two = TestEnum.ONE
复制代码
==
==同上, 结果为true时代表两个枚举相同
3.4 Switch中的枚举
不推荐使用switch, 强烈建议使用if else或多态对其进行取代
假设有如下枚举:
public private enum Gender {
MEN, WOMEN, UNKOWN
}
复制代码
直接使用MEN/WOMEN/UNKOWN即可:
public class Test{
public static void main(Person person) {
switch (person) {
case MEN:
业务代码...
break;
case WOMEN:
业务代码...
break;
}
}
}
复制代码
3.5 路径分隔符
在Windows下, 分隔符为\, 但可以识别/
在Linux下, 分隔符为/
所以, 在处理路径问题时, 建议将分隔符统一为/, Java是一门跨平台的语言
在对路径进行处理时, replaceAll方法存在小坑
Java中的\为转义符, 所以\\不是\\, 而是\, \\\\是\\
以下为建议处理逻辑:
public class Test{
public void test(String filename) {
filename.replaceAll("\\", "/");
}
}
复制代码
3.6 ArrayList初始化的四种方法
总共有四种初始化方法:
- 双括号
- Arrays.asList
- stream
- List.of()
双括号
List<Integer> test = new ArrayList<Integer>(){{
add(1);
add(2);
}};
复制代码
Arrays.asList
这种方式存在小坑, 按如下方法生成的数组无法修改:
List<Integer> test = Arrays.asList(1, 2, 3);
复制代码
可以进行调整, 使其支持修改:
List<Integer> test = new ArrayList<>(Arrays.asList(1, 2, 3));
复制代码
stream:
List<Integer> test = Stream.of(1, 2, 3).collect(Collectors.toList());
复制代码
List.of()
JDK11方可使用(最早在JDK9中加入, 但推荐使用长期支持的JDK版本, 如8/11)
List<Integer> test = List.of(1, 2, 3)
复制代码
3.7 ArrayList去重
- contains
- stream
contains
List<Integer> test1 = new ArrayList<>(Arrays.asList(1, 2, 2, 3));
List<Integer> result = new ArrayList<>();
test1.forEach(o -> {
if (!result.contains(o)) {
result.add(o);
}
});
复制代码
如果数组内容为对象, 在调用contains方法时, 实际是使用其equals方法进行判断
请根据实际的业务需求去重写类的equals方法
stream
List<Integer> test1 = new ArrayList<>(Arrays.asList(1, 2, 2, 3));
result = test1.stream().distinct().collect(Collectors.toList());
复制代码
如果数组内容为对象, stream的distinct方法会先用hashCode进行判断, 结果为true时继续调用equals
请根据实际的业务需求去重写类的hashCode和equals两个方法
3.8 ArrayList的比较操作
- 并集
- 交集
- 交集的补集
- 差集
假设存在数学/英语两个兴趣班, 分别有许多学生报名参加:
List<Student> math = new ArrayList<>();
List<Student> english = new ArrayList<>();
复制代码
并集
即所有参加了课外班的学生
// 原生方法
math.removeAll(english);
math.addAll(english);
// stream
// 这里的distinct方法刚在上一节的去重中讲过
math.addAll(english);
List<Student> result = math.stream().distinct().collect(Collectors.toList());
// CollectionUtils
List<Student> result = (List<Student>) CollectionUtils.union(math, english);
复制代码
交集
参加了英语和数学两个课外班的学生
// 原生方法
math.retainAll(english);
//stream
List<Student> result = math.stream().filter(english::contains).collect(Collectors.toList());
// CollectionUtils
List<Student> result = (List<Student>) CollectionUtils.intersection(math, english);
复制代码
交集的补集
只参加了英语或只参加数学一个课外班的学生
// 原生方法
// 暂时没想出合适的, 先用最蠢的方法
List<Student> result = new ArrayList<>();
for (Student student : math) {
if (english.contains(student)) {
continue;
}
result.add(student);
}
for (Student student : english) {
if (math.contains(student)) {
continue;
}
result.add(student);
}
//stream
List<Student> temp = new ArrayList<>(math);
temp.addAll(english);
List<Student> result = temp.stream().distinct().collect(Collectors.toList());
result.removeAll(math.retainAll(english))
// CollectionUtils
List<Student> result = (List<Student>) CollectionUtils.disjunction(math, english);
复制代码
差集
只参加了英语课外班的学生
// 原生方法
english.removeAll(math)
//stream
List<Student> result = english.stream().filter(student -> !math.contains(student)).collect(Collectors.toList());
// CollectionUtils
List<Student> result = (List<Student>) CollectionUtils.subtract(math, english);
复制代码
3.9 ArrayList转HashMap
一般发生在与前端交互的视图实体处理情景, 假设我们有一个设置数据实体:
@Data
public class SettingEntity {
private String id;
private String code;
private String value;
}
复制代码
根据需求, 前端需要的视图实体如下, 其中的system/author对应SettingEntity中的code:
{
"system": "spring-boot-koala",
"author": "Houtaroy"
}
复制代码
利用stream将List<SettingEntity>进行转换:
public class SettingService {
public Map<String, String> getSettingVO(List<SettingEntity> settings) {
return settings.stream().collect(Collectors.toMap(SettingEntity::getCode, SettingEntity::getValue);
}
}
复制代码
如果需要对数据进行处理, 可以调整为:
public class SettingService {
public Map<Integer, Integer> getSettingVO(List<SettingEntity> settings) {
return settings.stream().collect(Collectors.toMap(setting -> Integer.parseInt(setting.getCode()), setting -> Integer.parseInt(setting.getValue())));
}
}
复制代码
3.10 HashMap便捷方法的使用
使用HashMap时经常遇到的几种业务逻辑:
- Map是否为空
- 判断key是否存在, 如果存在则获取, 不存在则新增
- 判断key是否存在, 如果存在则更新, 不存在则忽略
Map是否为空
使用isEmpty方法
public class Test {
private Map<String, Object> map;
public boolean test() {
return map.isEmpty();
}
}
复制代码
判断key是否存在, 如果存在则获取, 不存在则新增
putIfAbsent, 此方法会返回旧值, 如果旧值不存在则为null
public class CacheUtil {
private Map<String, Student> cache;
private Student getOrCreateByName(String studentName) {
Student student = Student.builder().name(studentName).build();
cache.putIfAbsent(studentName, student);
return student;
}
}
复制代码
computeIfAbsent, 此方法会返回新值:
public class CacheUtil {
private Map<String, Student> cache;
private Student getOrCreateByName(String studentName) {
String className = "test-"
return cache.computeIfAbsent(studentName, key -> className + "student-" + key);
}
}
复制代码
判断key是否存在, 如果存在则更新, 不存在则忽略
使用computeIfPresent
public class CacheUtil {
private Map<String, Student> cache;
private void updateCache(Student student) {
cache.computeIfPresent(student.getName(), (key, value) -> student);
}
}
复制代码
方法比较
内容 | putIfAbsent | computeIfAbsent | computeIfPresent |
---|---|---|---|
方法返回 | 旧值 | 新值 | 新值 |
key为null | 生成一个null的key | 生成一个null的key | 更新key为null的value |
value为null | 生成一个value为null的key | 不生成 | 如果旧值为null, 则不变, 如果旧值不为null, 则删除其对应的key |
3.11 获取泛型的Class
使用ParameterizedType获取T的类型
public abstract class BaseDeviceService<T extends Device, TD> extends BaseSearchableService<T, TD> {
private final Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
复制代码
4 Spring Boot Web相关
4.1 如何接收数组参数
- 创建VO类对其进行包装
- 直接接收
假设我们需要实现一个批量添加学生的接口
创建VO类对其进行包装
public class AssociateStudentVO {
private String className;
private List<Student> students;
}
public interface teacherApi {
@GetMapping(value = "/teacher/associateStudent")
void associateStudent(@RequestBody AssociateStudentVO associateStudentVO);
}
复制代码
直接接收
public interface teacherApi {
@GetMapping(value = "/teacher/associateStudent")
void associateStudent(@RequestBody List<Student> students);
}
复制代码
如果使用swagger进行接口测试, 参数注解设置allowMultiple = true
4.2 自定义配置
- @Value
- @ConfigurationProperties
@Value
这种方式适用于配置数量少或使用位置零散:
public class FileService {
@Value("${project-url}")
private String projectUrl;
}
复制代码
@ConfigurationProperties
根据不同配置建立不同的类, 更符合面向对象思想:
@Data
@ConfigurationProperties("file")
public class FileConfiguration {
private String tempPath;
private String projectFilePath;
private String projectUrl;
private List<String> projectFileType;
}
复制代码
IDEA警告处理
在maven中加入如下依赖并重新编译项目:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
复制代码
4.3 事务
在需要事务的方法或类上增加@Transactional注解实现事务功能
详细配置可以参考这篇文章
特殊配置
发生指定异常时才触发事务:
@Transactional(rollbackFor = {MyException.class})
public class MyService {
业务代码...
}
复制代码
配置只读模式, Hibernate会提供Session方面的一些优化手段:
@Transactional(readOnly=true)
复制代码
失效情景
- 数据库不支持事务
- 类没有被Spring管理
- 非public方法
- 没有通过Bean调用方法(例如在类中相互调用)
脏读/不可重复读/幻读
脏读: 读取到了还未提交事务中的无效数据, TransactionDefinition.ISOLATION_READ_COMMITTED可避免
不可重复读: 在一个事务中快速读取两次数据, 但此数据在另一操作中被修改, 出现两次数据不一致, TransactionDefinition.ISOLATION_REPEATABLE_READ可避免
幻读: 在处理全部数据时, 另一操作新增一条数据, 新增的数据不受处理全部的影响, TransactionDefinition.ISOLATION_SERIALIZABLE可避免, 但会造成性能问题
详细示例可以参考我的文章
4.4 定时任务
在启动类上增加@EnableScheduling注解
@EnableScheduling
@SpringBootApplication
public class Application {
/**
* main方法
*
* @param args 参数
*/
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复制代码
在方法上增加@Scheduled注解, 使其成为定时任务:
public class Task {
@Scheduled(cron="0/5 * * * * ? ")
public void run() {
// 任务逻辑
}
}
复制代码
@Scheduled具体参数可以参考这篇文章
特别讲解下需要注意的几点:
-
“0/5 * * * * ? “, 是指从整点开始每五秒中运行一次, 即每分钟的第5秒/第10秒, 以此类推, 并不是从启动项目开始
-
“fixedDelay=5000″配置含义为上次任务执行完毕5秒后启动下一个任务
-
fixedDelayString与fixedDelay相同, 但其内容支持SpEL表达式, 如
@Scheduled(fixedDelayString = "${task.interval}")
4.5 MQTT
针对MQTT的详细讲解参考我的文章: 在Spring Boot中使用MQTT
本内容只示例如何应用:
- Client客户端
- spring-boot-koala-starter-mqtt-auto
Client客户端
创建工厂类:
public class MqttFactory {
private static MqttProperties configuration;
private static MqttClient client;
/**
* 获取客户端实例
* 单例模式, 存在则返回, 不存在则初始化
*/
public static MqttClient getInstance() {
if (client == null) {
init();
}
return client;
}
/**
* 初始化客户端
*/
public static void init() {
try {
client = new MqttClient(configuration.getAddress(), "client-" + System.currentTimeMillis());
// MQTT配置对象
MqttConnectOptions options = new MqttConnectOptions();
options.setAutomaticReconnect(true);
if (!client.isConnected()) {
client.connect(options);
}
} catch (MqttException e) {
LOGGER.error(String.format("MQTT: 连接消息服务器[%s]失败", configuration.getAddress()));
}
}
}
复制代码
MQTT的具体配置可以查看MqttConnectOptions
创建工具类:
public class MqttUtil {
/**
* 发送消息
* @param topic 主题
* @param data 消息内容
*/
public static void send(String topic, Object data) {
// 获取客户端实例
MqttClient client = MqttFactory.getInstance();
ObjectMapper mapper = new ObjectMapper();
try {
// 转换消息为json字符串
String json = mapper.writeValueAsString(data);
client.publish(topic, new MqttMessage(json.getBytes(StandardCharsets.UTF_8)));
} catch (JsonProcessingException e) {
LOGGER.error(String.format("MQTT: 主题[%s]发送消息转换json失败", topic));
} catch (MqttException e) {
LOGGER.error(String.format("MQTT: 主题[%s]发送消息失败", topic));
}
}
/**
* 订阅主题
* @param topic 主题
* @param listener 消息监听处理器
*/
public static void subscribe(String topic, IMqttMessageListener listener) {
MqttClient client = MqttFactory.getInstance();
try {
client.subscribe(topic, listener);
} catch (MqttException e) {
LOGGER.error(String.format("MQTT: 订阅主题[%s]失败", topic));
}
}
}
复制代码
消息处理监听器IMqttMessageListener:
public class MessageListener implements IMqttMessageListener {
/**
* 处理消息
* @param topic 主题
* @param mqttMessage 消息
*/
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
LOGGER.info(String.format("MQTT: 订阅主题[%s]发来消息[%s]", topic, new String(mqttMessage.getPayload())));
}
//订阅主题test01, 使用MessageListener来处理它的消息
public static void main(String[] args) {
MqttUtil.subscribe("test01", new MessageListener());
}
}
复制代码
spring-boot-koala-starter-mqtt-auto
笔者基于Spring Integration编写的配置启动类, 使用方法请参考Github: spring-boot-koala
4.6 AOP与SpEL
对于AOP和SpEL的应用, 可以参考我的这篇文章Spring Boot中AOP与SpEL的应用
以记录业务日志为例, 详细讲解了为何以及如何使用二者实现需求, 且在Github提供了具体的项目代码
5 业务中的MyBatis
5.1 批量插入
在插入语句中使用collection标签
假设一个项目可以关联多个合同, 先在repository中传入数组参数:
public interface ProjectRepository {
/** * 批量新增项目与合同关系
*
* @param projectId 项目id
* @param contractIds 合同id数组
*/
void addProjectContractRelation(@Param("projectId") String projectId, @Param("contractIds") List<String> contractIds);
}
复制代码
修改projectMapper.xml:
<insert id="addProjectContractRelation">
insert into t_project_contract (project_id, contract_id) values
<!--collection的名字要和repository传入的参数名称(注解@Param中的)一致-->
<foreach collection="contractIds" item="id" index="index" separator=",">
(#{projectId}, #{id})
</foreach>
</insert>
复制代码
如果我希望在新增项目的同时一并添加项目与合同的关系, 而不是再调用一次addProjectContractRelation
方法呢?
使用include标签
先来修改下项目实体类, 创建对应的传输实体DTO:
@Data
public class ProjectDTO {
private String id;
private String name;
private List<String> contractIds;
}
复制代码
接下来修改xml即可:
<insert id="add" parameterType="cn.houtaroy.test.entities.ProjectDTO">
insert into t_project (id, name) values (#{id,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR});
<include refid="addProjectContractRelation"/>
</insert>
复制代码
5.2 批量查询与collection传参
批量查询
承接5.1中的业务示例, 一个项目可关联多个合同, 在查询项目时如何把所有关联的合同查询出来呢?
首先, 创建根据项目id查询合同的方法, ContractMapper.xml:
<select id="listByProjectId" resultType="cn.houtaroy.test.entities.ContractEntity">
select contract.id, contract.name
from t_contract contract left join t_project_contract projectContract on contract.id = projectContract.contract_id
where projectContract.project_id = #{projectId}
</select>
复制代码
接下来, 在ProjectMapper.xml中创建resultMap:
<resultMap id="ProjectResult" type="cn.houtaroy.test.entities.ProjectDTO">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="contracts"
ofType="cn.houtaroy.test.entities.ContractEntity"
javaType="java.util.ArrayList"
select="cn.houtaroy.test.repositories.ContractRepository.listByProjectId"
column="id">
</collection>
</resultMap>
复制代码
最后, 我们只需要在查询中返回刚才创建的resultMap即可, ProjectMapper.xml:
<select id="load" resultMap="ProjectResult">
select id, name from t_project
</select>
复制代码
collection传参
上面的批量查询中, 只传入了项目id一个参数, 如果我还需要传入项目类型呢?此时我们需要调整标签中column的内容:
<collection property="contracts"
ofType="cn.houtaroy.test.entities.ContractEntity"
javaType="java.util.ArrayList"
select="cn.houtaroy.test.repositories.ContractRepository.listByProjectId"
column="{id=id, projectType=type}">
</collection>
复制代码
即{属性名称1=列名1, 属性名称2=列名2…}, 可以理解为按对象形式进行传参
5.3 判断参数类型
假设有商城搜索的需求, 具体内容如下:
1.用户挑选机械键盘, 支持根据类型进行筛选
2.前端传递类型条件, 此条件可能为字符串, 可能为数组
无论字符串还是数组, 查询参数的含义都是”机械键盘类型”, 从设计角度讲, 使用同一方法同一参数来处理同一概念是理论正确的
不多说废话, 我们直接修改xml中的查询条件:
<where>
<if test='parameters.types != null'>
<choose>
<when test="parameters.types instanceof String">
and t.type = #{parameters.types}
</when>
<otherwise>
and t.type in
<foreach collection="parameters.types" item="type" index="index" open="(" separator="," close=")">
#{type}
</foreach>
</otherwise>
</choose>
</if>
</where>
复制代码
Java中是Instanceof, Mybatis中是instanceof, i要小写
5.4 0和”
在Mybatis中, 如果出现如下情况, 会判断为true使条件失效
- parameters.status=0
<if test="parameters.status != null and parameters.status != ''">
原因就在源码中的这一句: return s.length == 0 ? 0.0D : Double.parseDouble(s)
;
我们的空字符串就这样变成了0.0D, 且和0相等
6 Maven打包
6.1 引入本地依赖
引入本地依赖有两种方式:
- 修改pom参数
- 导入本地仓库
修改pom参数
本地依赖目录结构如下:
project
|---src
|---lib
|---pig.jar
|---pom.xml
复制代码
增加dependency标签的scope和systemPath属性, pom.xml:
<dependency>
<groupId>cn.com.pig</groupId>
<artifactId>pig</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/pig.jar</systemPath>
</dependency>
复制代码
其中的groupId, version等内容, 可以在idea中打开jar包, 查看里面的MANIFEST.MF获取, 其实随便写也无妨
一定要注意打包的问题, pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--加入这条配置即可-->
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
<build>
复制代码
导入本地仓库
在cmd界面运行如下命令:
install:install-file -Dfile=<Jar包的地址>
-DgroupId=<Jar包的GroupId>
-DartifactId=<Jar包的引用名称>
-Dversion=<Jar包的版本>
-Dpackaging=<Jar的打包方式>
复制代码
用之前的例子:
install:install-file -Dfile=./lib/pig.jar
-DgroupId=cn.com.pig
-DartifactId=pig
-Dversion=1.0
-Dpackaging=jar
复制代码
6.2 私服
关于如何搭建Maven私服, 可以参考我的这篇文章Maven私服毕业攻略