工作中的Java|Java 开发实战

这是我参与更文挑战的第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.AutoCloseablejava.io.Closeable就行

3.2 JDK8中接口的新特性

Java8中的接口新加入了defaultstatic关键字:

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];
  
}
复制代码

建议使用Hutool的SpringUtil

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私服毕业攻略

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享