写代码:记一次项目经历

99 阅读6分钟

项目结构

近期学习到一个项目结构,对比之前的有可取可学习之处。在这里记录一下。

my-java-project/
│
├── pom.xml (父模块)
│
├── submodule-api/
│   └── pom.xml
│
├── submodule-api-impl/
│    └── pom.xml
│
├── submodule-application/(干嘛的?)
│    └── config(mybatis、redis配置)
│    └── convert(一个实体类一个convert?)
│    └── dao
│    └── utils
│    └── pom.xml
│
├── submodule-infrastructure/
│    └── do (dataobjcet)
│    └── mapper
│    └── pom.xml
│
├── submodule-kafka/(一个比较独立的消费kafka的服务)
│    └── pom.xml
│
├── submodule-rpc/
│    └── Application.java(启动类。关键是启动了哪些地方?)
│    └── pom.xml
│
├── submodule-sdk/(给另一个项目引用的)
│    └── pom.xml
│
├── submodule-types/
│    └── dto (data transfer object)
│    └── entity(这里也放config啥的,和application里还不一样)
│    └── request
│    └── vo
│    └── pom.xml
│
├── submodule-web/(好像没啥用?)
     └── pom.xml

POM

在父module中执行clean或者deploy命令,所有子module也会依次执行clean或deploy命令。

之前有子module的pom中使用revision变量,但是build时却报错显示无法识别revision的问题。好像在加入flatten之后,就没有了这个问题,父pom中的properties能够进入到子pom中。

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.9</version>
    </parent>

    <groupId>com.xxx</groupId>
    <artifactId>xxx</artifactId>
    <version>${revision}</version>
    <packaging>pom</packaging>
    <modules>
        <module>xxx-types</module>
        <module>xxx-api</module>
        <module>xxx-infrastructure</module>
        <module>xxx-application</module>
        <module>xxx-api-impl</module>
        <module>xxx-kafka</module>
        <module>xxx-rpc</module>
        <module>xxx-sdk</module>
        <module>xxx-web</module>
    </modules>

    <properties>
        <!-- 当前工程版本号 -->
        <revision>1.0.1-SNAPSHOT</revision>
        <java.version>1.8</java.version>
        <start-class>com.xxx.Application</start-class>
        <dubbo.version>xxx</dubbo.version>
    </properties>

    <!-- 定义所有子模块的依赖版本管理。 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-spring-boot-starter</artifactId>
                <version>${dubbo.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 仓库管理 -->
    <repositories>
        <repository>
            <id>nexus</id>
            <url>http://xxx/groups/public/</url>
        </repository>
    </repositories>
    
    <!-- 分发管理 -->
    <distributionManagement>
        <repository>
            <id>nexus-release</id>
            <name>releases</name>
            <url>http://xxx/repositories/releases</url>
            <uniqueVersion>true</uniqueVersion>
        </repository>
        <snapshotRepository>
            <id>nexus-snapshots</id>
            <name>snapshots</name>
            <url>http://xxx/repositories/snapshots</url>
        </snapshotRepository>
    </distributionManagement>
    
    <!-- 构建配置 -->
    <build>
        <plugins>
            <!-- 插件 maven-compiler-plugin 配置了 Java 编译器的版本,设置了源代码和目标代码版本为指定的 Java 版本,并设置编码。-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                    <compilerArgument>-parameters</compilerArgument>
                </configuration>
            </plugin>
            
            <!-- 在很多情况下,flatten-maven-plugin 会解析父 POM 中的所有属性,并将它们写入被创建的扁平化 POM 文件中。这意味着 ${revision} 和其他属性会被实际的值替换,然后这些值会被写入每个子模块的 POM 文件中。 -->
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>flatten-maven-plugin</artifactId>
                <version>1.2.5</version>
                <configuration>
                    <!-- 避免IDE将 .flattened-pom.xml自动识别为功能模块 -->
                    <flattenedPomFilename>pom-xml-flattened</flattenedPomFilename>
                    <updatePomFile>true</updatePomFile>
                    <flattenMode>resolveCiFriendliesOnly</flattenMode>
                </configuration>
                <executions>
                    <execution>
                        <id>flatten</id>
                        <phase>process-resources</phase>
                        <goals>
                            <goal>flatten</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>flatten-clean</id>
                        <phase>clean</phase>
                        <goals>
                            <goal>clean</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

部署

选择pom:父module的pom,因为父module的pom里面才有所有的依赖包。项目运行需要这些依赖包。

选择target:rpc module的target,不因为别的,就因为这个module的target包里面,才有启动脚本里设置的Main函数啊!

重点其实要看main函数

因为上面的父module的pom中显示,它引用了各个子module,所以这各个子module下面,com.xxx这个路径下的类都可以扫描到。

如果api-module下面有对外的service接口,也会被注册提供;如果kafka-module下面有InitializingBean的初始操作,也会执行;等等。

package com.xxx;

import xxx;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@xxx
@SpringBootApplication(scanBasePackages = {"com.xxx"})
@Slf4j
@EnableDubbo
public class Application {
    public static void main(String[] args) {
        try{
            SpringApplication.run(Application.class, args);
            log.info("start end");
        } catch (Throwable th) {
            th.printStackTrace();
        }

    }
}

代码

构造函数

选择使用构造函数注入而不是字段注入是一个更好的选择,主要因为它增强了代码的可读性、可测试性和安全性:

@Service
@DubboService
public class zzzServiceImpl implements zzzService {

    private final xxxDao xxxDao;
    private final yyyDao yyyDao;

    @Autowired
    public AppServiceImpl(xxxDao xxxDao, yyyDao yyyDao) {
        this.xxxDao = xxxDao;
        this.yyyDao = yyyDao;
    }
    
    // 字段注入
    // @Autowired
    // private xxxDao xxxDao;
}

这一做法有很多的理论依据,比如Spring官方文档、《Effective Java》和《Spring in Action》等书,这么写有以下好处

1、使得这个类的依赖关系非常清晰。

2、以在测试中直接提供实现依赖的假对象或模拟对象,而无须使用反射或其他方式设置类的状态。

xxxDao mockXxxDao = Mockito.mock(xxxDao.class);
yyyDao mockYyyDao = Mockito.mock(yyyDao.class);
zzzServiceImpl service = new zzzServiceImpl(mockXxxDao, mockYyyDao);

3、防止依赖注入为空(Null): 构造函数确保在创建对象时必须提供所需的所有依赖。

4、保持字段不可变: 使用构造函数注入时,通常将依赖声明为 final,这确保了在对象实例化后,依赖项不会被重新分配给其他对象,增加类的不可变性,降低潜在的错误。

5、符合依赖反转原则: 构造函数注入通常被认为是更符合依赖反转原则和单一责任原则,尤其是在需要多个依赖的情况下。

流式编程

现在写java代码不写流式感觉好像很low,可怎么样写流式算得上优雅呢?在处理前端发送的对象、处理查询数据库获得的list时,用流式编程真的非常合适。

convert

一个典型的场景,从数据库里查出数据来了,要把do转一下,转成dto,或者直接转成vo。

public void convert() {
    IPage<xxxDo> dataIPage = xxxDao.pageList(iPage, params);
    LIst<xxxDto> dtoList = dataIPage.getRecords().stream()
                                             .map(xxxConverter::convertDoToDto)
                                             .collect(Collectors.toList());
}

public static xxxDto convertDoToDto(xxxDo source) {
    xxxDto xxx = new xxxDto();
    xxxDto.set(xxxDo.get());
    return xxxDto;
}

构造函数

这里就可以将上面提到的构造函数和流式编程合起来使用。除了上面的convert之外,构造函数也可以用于转换。

public void convert(xxxRequest request) {
    LIst<xxxDto> dtoList = request.getxxxConfigList().stream()
                                             .map(xxxDto::enw)
                                             .collect(Collectors.toList());
}

public class xxxDto() {
    private p1;
    private p2;
    
    public xxxDto(xxxConfig) {
        this.p1 = xxxConfig.p1;
        this.p2 = xxxConfig.p2;
    }
}

dubbo

dubbo.yml

dubbo:
  # 应用程序配置
  application:
    name: xxx
    parameters:
      telnet: invoke,status,clear,log,trace,count,warm,connect
    logger: log4j2
  # 注册中心配置
  registry:
    protocol: zookeeper
    address: xxx:xxx,xxx:xxx,xxx:xxx
    client: curator
    check: true
    timeout: 20000
    parameters:
      file: dubboFile/dubbo-registry.properties # 这个好像没啥用,没啥影响
  # 协议配置
  protocol:
    name: dubbo
    transporter: netty
    port: 9999
    accesslog: false
    threads: 10
  # 服务提供相关配置
  provider:
    timeout: 10000
    filter: -exception
    parameters:
      throw:
        exception: false
      provider:
        category: ${xxx:}
  # 扫描配置,关系到 Dubbo 服务的自动发现和注册
  scan:
    base-packages: com.xxx

@DubboService

@DubboService与上述yaml文件的关系:

1、服务注册:在 zzzServiceImpl 类上使用 @DubboService 注解时,这会使该类被标记为 Dubbo 服务提供者,自动注册到配置的注册中心(如 Zookeeper)。这意味着 Dubbo 会使用 yml 中定义的注册中心配置将服务注册,服务名通常和 application.name 相关。

2、协议和超时:@DubboService 注解通常还可以配置一些参数(例如超时、负载均衡策略等),这些会与 yml 中的 provider 设置相结合。

3、组件扫描: 因为 yml 中定义了扫描包的基础路径,Dubbo 将自动检测并注册使用 @DubboService 注解的类。

注解来源于dubbo-spring-boot-starter依赖,是一个方便的集成工具,使得 Apache Dubbo 和 Spring Boot 的集成变得简单而有效。

mybatis

transactional

事务是一系列的数据库操作,这一系列操作要么都成功,要么都失败。使用@Transactional注解,Spring可以轻易的将一个类、方法作为事务的边界。

下面代码中,如果在第四步报错了,那么前三步是不会插入数据的。而且如果debug的话,可以看到,如果第四步没有执行成功,整个过程中都不会有数据写入任何一张表。

@Transactional(rollback = Exception.class)
public void addData(Request request) {
    // 第一个插入操作
    dao1.saveBatch(request.getData1());
    
    // 第二个插入操作
    dao2.saveBatch(request.getData2());
    
    // 第三个插入操作
    dao3.saveBatch(request.getData3());
    
    // 第四个插入操作,报错了
    dao4.saveBatch(request.getData4());
}

先查询再写入

也许在往数据库里写入数据之前,都应该想一想是不是要先查询在不在库里。

并不是因为有幂等性、有mysql唯一索引校验报错,就可以放任数据写入数据库中。先查后写会有些好处,尤其是在面向C端,请求量大延时要低的情况下。

比如说回头上线了这个表数据已经好几百万上千万加索引什么需要很久的时间,像一些ToC的业务场景都是需要先check数据存不存在。

@Transactional(rollback = Exception.class)
public boolean save(List<Dto> dtoList) {
    List<Do> list = dtoList.stream().map(Convert::convertToDo).collect(Collectors.toList());
    List<Do> saveList = lists.newArrayList();
    for (Do do1 : list) {
        if (!dao.checkExists(do1) {
            saveList.add(do1);
        }
    }
    dao.saveBatch(saveList);
}

更新:先删除再写入

具体的业务背景是,修改的参数和新建的参数是同一套,按照一般的想法,就是各种比对数据库现有的参数,哪些是新增的,哪些是修改的,这里面判断起来非常复杂。

那有什么简单又能满足逻辑的方法呢:deleteAndBindConfiguration();

public boolean deleteAndBindConfiguration(String updater, List<xxx> xxxConfiguration) {
    addModHistory(xxxConfiguration);
    xxxDao.deleteBy(xxxConfiguration);
    return xxxDao.saveBatch(xxxConfiguration);
}
记录更新历史

抽象出来一个专门的interface接口ModHistoryService,里面定义方法记录操作的记录,操作前后的数据

public interface ModHistoryService {
    
}

@Data
@TableName(value = "mod_history")
public class ModHistoryDo {
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    private Integer batchId;
    private Integer dataType;
    private Long    dataId;
    private Integer modType;
    private String  modDesc;
    private String  modBefore;
    private String  modAfter;
    private String  modUser;
    private String  modUserName;
    private Date    modTime;
}

config

之前的项目中用了很多xml文件,包括datasource.xml,显得很老旧,如果多用一些@注解,配以少量的yml文件,就会使得代码看起来更高级,显得更会一些。

之前的代码中datasource.xml长篇大论:

<beans>
    <!-- 其实就是用xml生成bean对象,这些bean做一些mybatis的初始化 连接 事务等等操作 -->
    <bean id="dataSourceTransactionManager" calss="xxx"/>
    
    <bean id="datasource" class="xxx"/>
    <bean id="sqlSessionFactoryBean" class="xxx"/>
    <bean id="mapperScannerConfigurer" class="xxx"/>
    
    <bean id="anotherDatasource" class="xxx"/>
</beans>

没想到这些xml配置有一个算一个都可以用注解替代,压根不用xml文件,看着这么low:

@Configuration
@MapperScan(basePackages="xxx")
@EnableTransactionManagement
public class DatasourceConfig {
    @Autowired
    Datasource datsource;
    
    @Bean(name = "datasource")
    @ConfigurationProperties(prefix = "spring.datasource")
    @Primary // 当有多个同类型的bean时,优先选择这个bean
    public Datasource initDatasource() { // 这个方法竟然是被上面的Autowired的datasource调用的,神奇
        return DataSourceBuilder.create().build();
    }
    
    @Bean(name = "sqlSessionFactory")
    @Primary
    public SqlSessionFactory sqlSessionFactory(
        @Quafier("datasource")) { // 用于指定要注入的具体的bean名称
        // xxx
    }
    
    @Bean(name = "sqlSessionTemplate")
    @Primary
    public SqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
    
    @Bean(name = "transactionManager")
    @Primary
    public TransactionManager transactionManager(@Qulifier("datasource") Datasource datasource) {
        return new DataSourceTransactionManager(datasource);
    }
}

public class xxxDatasourceConfig {
    // 和上面一样
}

定时任务

之前的定时任务都是咋写的,搞个InitializingBean和ScheduledThreadPoolExecutor开机就开始跑定时任务了。

@Override
public void afterPropertiesSet() throws Exception {
    scheduledThreadPoolExecutor.scheduleAtFixedRate(this::func(), 60, 10, TimeUnit.SECONDS);
}

连个Spring的定时任务都不写,没有专门的schedule包。

@Component
public calss xxxSchedule() {
    @Scheduled(fixedRate = 600000)
    public void func() {
        // 这里面的逻辑都被注释掉了,就是定时任务这么写也不行
    }
}

为什么上面的不行呢:没有这种网关控制,上面那样启定时不好;每个实例都有定时,不是唯一的;尤其有写的操作,全靠redis的分布式事务锁;如果没有对应的框架,只能自己部署一个单例的。

那怎么样才能行呢:接人家的平台应该有告警什么的,也能提供负载均衡这些。

原来的代码写的不好,需要重构,重构是有价值的:提升后续开发效率、后续逻辑也清晰、出问题排查起来也快。

等等

@Slf4j

@Slf4j是lombok库提供的一个注解,这个库的目的在于提供注解减少样板代码的编写。

@Slf4j注解可以自动生产一个Logger对象,不用每个类第一行都写个Logger对象出来了。

@Slf4j
public class Example {
    public void func() {
        // 这个log是Lombok在编译时生成的一个Logger对象
        // 相当于 private static final Logger log = LoggerFactory.getLogger(Example.class);
        log.info("xxx"); 
        log.error("xxx");
    }
}

枚举

前端直接传入String,但后端对应的字段是一个枚举类型:

{
    "id": 123,
    "type": "OFFLINE"
}

public class Entity implements Serializable {
    private Long id;
    private TypeEnum type;
}

这样也是可以接收的,可以先看下枚举类是怎么写的:

public Enum TypeEnum {
    // 枚举类上来都是枚举对象都列出来
    OFFLINE(1, "info"),
    REALTIME(2, "info");
    
    // 每个枚举对象都有的属性
    private int code;
    private String name;
    
    TypeEnum(int code, String name) {
        this.code = code;
        this.name = name;
    }
}

可以看出传入的值,与枚举对象的名称是一样的。但这好像并不是影响因素,就是可以这样接收。

equals

int a = 10;
long b = 10L;
a == b; // true
a.equals(b); // 报错,基本类型调用不了equals方法

Integer aObj = 10;
Long bObj = 10L;
aObj.equals(bObj);

aObj.equals(b); // false,没报错是因为b被封装为了Long对象
bObj.equals(a); // false,同上

bObj.equals(aL);