SpringBoot项目开发全过程-基础篇

SpringBoot项目开发全过程-基础篇

1.关于SpringBoot

SpringBoot本质上是一个基于Maven的、以SpringBoot框架作为基础的进阶框架,很好的支持了主流的其他框架,并默认完成了许多的配置,其核心思想是"约定大于配置".

2.创建SpringBoot项目

使用IntelliJ IDEA,在创建向导中选择Spring Initializer即可开始创建Spring Boot项目,在创建向导的界面中,需要关注的部分有:

  • Group ID
  • Artifact ID
    以上两个值会共同构成一个Package name,如果Artifact ID的名词中有减号,在Package name中会去除,推荐手动添加小数点进行分隔.

由于Spring Boot官方更新版本的频率很高,在创建项目时,随便选取版本即可,当项目创建成功后,推荐打开pom.xml,将中的(即Spring Boot父项目的版本)改成你的JDK兼容的版本,例如:2.2.6.RELEASE(我的JDK版本是15)

在创建过程中,还可以在创建向导的界面中勾选所需的依赖项,如果创建时没有勾选,也可以在创建工程之后手动在pom.xml中添加.

3.Spring Boot项目的项目结构

由于Spring Boot项目本质上就是一个Maven项目,所以,目录结构基本没有区别.

与普通Maven项目最大的不同在于:Spring Boot项目在src\main\javasrc\test\java下默认已经存在Package,是创建项目时指定的Package,**需要注意:此Package已经被配置为Spring执行组件扫描的根包,所以,在编写代码时,所有的组件类都必须放在此包或其子孙包中!**通常,推荐将所有的类(及接口)都创建在此包及其子孙包下。

my-springboot-app/
|-- mvnw               # Maven包装器脚本,允许运行未安装Maven的构建
|-- mvnw.cmd           # Windows下的Maven包装器脚本
|-- pom.xml            # Maven项目对象模型文件
|-- src                # 源代码目录
|   |-- main           
|   |   |-- java
|   |   |   `-- com
|   |   |       `-- example
|   |   |           `-- myapp
|   |   |               |-- MySpringBootApplication.java  # Spring Boot主应用程序类
|   |   |               |-- controller                    # 控制器层代码
|   |   |               |-- service                       # 服务层代码
|   |   |               |-- repository                    # 数据访问层代码
|   |   |               |-- domain                        # 实体类
|   |   |               `-- model                         # 领域模型对象
|   |   |-- resources
|   |   |   |-- static            # 静态资源文件,如HTML、CSS、JavaScript
|   |   |   |-- templates         # 模板文件,如Thymeleaf模板
|   |   |   |-- application.properties (或 .yml) # 应用程序配置文件
|   |   |   `-- banner.txt        # 自定义Spring Boot启动banner
|   |   `-- webapp
|   |       `-- WEB-INF            # 可选,如果打包为WAR则可能包含此目录
|   `-- test
|       |-- java
|       |   `-- com
|       |       `-- example
|       |           `-- myapp
|       |               |-- MySpringBootApplicationTests.java  # 测试类
|       |               |-- controller                        # 控制器层测试
|       |               |-- service                           # 服务层测试
|       |               `-- repository                        # 数据访问层测试
|       `-- resources
|           |-- application-test.properties (或 .yml) # 测试环境配置
|-- .gitignore          # Git忽略文件配置
|-- README.md           # 项目README文件
`-- (其他项目相关的文件)

在这个结构中:

  • src/main/java:存放项目的 Java 源代码。

    • com/example/myapp:包路径,通常按照组织的域名反转来命名。
    • MySpringBootApplication.java:Spring Boot 应用的入口点,这个类通常会有 @SpringBootApplication 注解。
  • src/main/resources:存放所有的资源文件,包括配置文件、静态资源和模板。

    • static:存放静态资源,比如 HTML、CSS、JavaScript 等。
    • templates:存放模板文件,如使用 Thymeleaf 时的模板文件。
    • application.propertiesapplication.yml:Spring Boot 的应用配置文件。
    • banner.txt:自定义 Spring Boot 启动时的控制台 banner。
  • src/main/webapp:可选,如果将应用打包成 WAR 文件,则存放 JSP、HTML 文件等。

  • src/test/java:存放项目单元测试和集成测试的 Java 代码。

    • MySpringBootApplicationTests.java:主要的测试类,通常由 Spring Initializr 自动生成。
  • src/test/resources:存放测试时使用的资源文件。

  • pom.xml:Maven 项目配置文件,声明了项目的依赖、插件和其他构建配置。

  • mvnwmvnw.cmd:Maven Wrapper,允许即使没有安装 Maven 也能构建项目。

  • .gitignore:指定不需要添加到版本控制中的文件或目录。

  • README.md:项目的 README 文件,通常包含项目的说明、构建步骤等信息。

除了这些核心的目录和文件外,Spring Boot 项目还可能包含其他文件和目录,比如 Docker 配置文件、CI/CD 配置文件(如 Jenkinsfile)、项目文档等。

小结:

  • 创建项目后默认的Package不要修改,避免出错
  • 在编码过程中,自行创建的所有类、接口均放在默认的Package或其子孙包中
  • 在src\main\java下默认已存在XxxApplication是启动类,执行此类中的main()方法就会启动整个项目
  • 启动类本身也是配置类
  • 配置都应该编写到src\main\resources下的application.properties中,Spring Boot会自动读取
  • 测试类也必须放在src\test\java下的默认Package或其子孙包中
  • 在测试类上添加@SpringBootTest注解,则其中的测试方法执行之前会自动加载Spring环境及当前项目的配置,可以在测试类中使用自动装配

4.在Spring Boot工程中使用Mybatis

什么是MyBatis?
MyBatis 是一个Java持久层框架,它提供了简便易用的方法来操作数据库。MyBatis 通过使用简单的XML或注解来配置和映射原生信息,将接口和Java的POJOs(Plain Old Java Objects,普通的Java对象)映射到数据库中的记录。

在Spring Boot中集成MyBatis通常涉及以下几个步骤:

1.添加依赖

<!-- mybatis启动器 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<!-- mysql连接器 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.13</version>
    <scope>runtime</scope>
</dependency>

<!-- mybatis生成器  -->
<dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <version>1.3.2</version>
</dependency>

2.配置数据源

application.propertiesapplication.yml文件中配置数据源,包括数据库驱动、URL、用户名和密码。
例如,使用application.properties配置MySQL数据源可能如下所示:

# 数据库配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456

创建实体类(也称为POJOs或JavaBeans)是推荐的做法,但并不是强制性的。实体类在MyBatis中代表数据库表中的行,通常包含与表列相对应的属性以及相应的getter和setter方法。使用实体类可以提高开发效率,因为它们提供了一种类型安全的方式来处理数据库操作的结果。

3.启用MyBatis映射器扫描

确保SpringBoot能够扫描到MyBatis的Mapper接口。可以用@MapperScan注解指定接口所在的包。

在我的主应用类或配置类上添加@MapperScan:

@SpringBootApplication
@MapperScan("com.example.project.mapper")//指向我的mapper接口所在的包
public class YourApplication{
    public static void main(String[] args){
        SpringApplication.run(YourApplication.class,args);
    }
}

4.定义Mapper接口和数据库操作

MyBatis 允许你通过两种方式来定义数据库的操作:使用 XML 映射文件或者使用注解。这两种方式可以根据项目的需要和开发者的偏好来选择。

使用 XML 映射文件
  1. 创建 Mapper 接口

    首先,你需要创建一个接口,该接口定义了将要执行的数据库操作。例如:

    package com.example.mapper;
    
    public interface UserMapper {
        User selectUser(int id);
    }
    
  2. 创建 XML 映射文件

    然后,你需要为这个接口创建一个 XML 映射文件。文件名通常是接口的名称加上 .xml 后缀,并放在与接口相同的包路径下。这个文件定义了具体的 SQL 语句和它们如何映射到接口方法。例如:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.example.mapper.UserMapper">
        <select id="selectUser" parameterType="int" resultType="com.example.domain.User">
            SELECT * FROM users WHERE id = #{id}
        </select>
    </mapper>
    

在这个例子中,selectUser 方法会执行一个 SQL 查询来返回一个 User 对象。

使用注解

如果你倾向于使用注解,那么可以省略 XML 映射文件,而是直接在 Mapper 接口的方法上使用 MyBatis 的注解来指定 SQL 语句。例如:

package com.example.mapper;

import org.apache.ibatis.annotations.Select;

public interface UserMapper {

    @Select("SELECT * FROM users WHERE id = #{id}")
    User selectUser(int id);
}

在这种情况下,@Select 注解告诉 MyBatis 执行的 SQL 语句是什么,而参数 #{id} 是一个占位符,它将被方法参数 id 的值所替代。

如果你使用的是XML方式定义MyBatis的SQL映射,则需要告诉MyBatis这些XML文件的位置。在application.propertiesapplication.yml中设置mybatis.mapper-locations属性。

# MyBatis 配置
mybatis.mapper-locations=classpath:mapper/*.xml

选择使用哪种方式
使用 XML 还是注解取决于你的个人偏好和项目需求。XML 映射文件方式提供了一种更为分离的方式来管理 SQL 语句,这在 SQL 语句非常复杂或者需要经常更改的时候是有利的。另一方面,注解方式能让你的代码看起来更为简洁,所有东西都在一个地方定义,这在 SQL 相对简单的时候是一个优点。有些团队甚至会混合使用这两种方式,以适应不同的情况。

6.开始使用

创建完成后,你可以在服务层注入Mapper接口,并开始使用它进行数据库操作。

@Service
public class UserService{
    private final UserMapper userMapper;

    @AutoWired
    public UserService(UserMapper userMapper){
        this.userMapper = userMapper;
    }

    public User getUserById(Integer id){
        return userMapper.getUserById(id);
    }
}

5.关于Profile配置

在Spring Boot中,对Profile配置有很好的支持,开发者可以在src\main\resources下创建更多的配置文件,这些配置文件的命名规则是application-xxx.properties(其中xxx是自定义的名称)

例如:

  • 仅在开发环境中使用的配置值可以写在application-dev.properties中
  • 仅在测试环境中使用的配置值可以写在application-test.properties中
  • 仅在生产环境(项目上线的环境)中使用的配置值可以写在application-prod.properties中

当把配置写在以上这类文件后,Spring Boot默认并不会应用以上这些文件中的配置,当需要应用某个配置时,需要在application.properties中激活某个Profile配置,例如:

# 激活Profile配置
spring.profiles.active=dev

提示:以上配置值中的dev是需要激活的配置文件的文件名后缀,当配置为dev时,就会激活application-dev.properties,同理,如果以上配置值为test,就会激活application-test.properties

6.关于YAML配置

Spring Boot也支持使用YAML配置,在开发实践中,YAML的配置也使用得比较多。

YAML配置就是把原有的.properties配置的扩展改为yml。

YAML配置原本并不是Spring系列框架内置的配置语法,如果在项目中需要使用这种语法进行配置,解析这类文件需要添加相关依赖,在Spring Boot中默认已添加此依赖。

在YAML配置中,原本在.properties的配置表现为使用多个小数点分隔的配置将改为换行并使用2个空格缩进的语法,换行前的部分使用冒号表示结束,最后的属性名与值之间使用冒号和1个空格进行分隔,如果有多条属性在.properties文件中属性名有重复的前缀,在yml中不必也不能重复写。

例如,原本在.properties中配置为:

server.port=8080

则在yml文件中配置为:

server:
  port: 8080

提示:在IntelliJ IDEA中编写yml时,当需要缩进2个空格时,仍可以使用键盘上的TAB键进行缩进,IntelliJ IDEA会自动将其转换为2个空格。

无论是.properties还是yml,只是配置文件的扩展名和文件内部的配置语法有区别,对于Spring Boot最终的执行其实没有任何表现上的不同。

7.使用Druid数据库连接池

Druid数据库连接是阿里巴巴团队研发的,在Spring Boot项目中,如果需要显式的指定使用此连接池,首先,需要在项目中添加依赖:

<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.19</version>
</dependency>

当添加了此依赖,在项目中需要应用时,需要在配置文件中指定spring.datasource.type属性,取值为以上依赖项的jar包中的DruidDataSource类型的全限定名。

例如,在yml中配置为:

# Spring系列框架的配置
spring:
    # 连接数据库的相关配置
    datasource:
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/yourdatabasename?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 使用的数据库连接池类型,选择使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource

8.编写数据访问层(持久层)代码

数据持久化:在开发领域中,讨论数据时,通常指定是正在执行或处理的数据,这些数据都是在内存中的,而内存(RAM)的特征包含"断电丢失",为了让数据永久保存,通常会将数据存储到能够永久存储数据的介质中,通常是计算机的硬盘,硬盘上的数据都是以文件的形式存在的,所以,当需要永久保存数据时,可以将数据存储到文本文件中,或存储到XML文件中,或存储到数据库中,这些数据保存的做法就是数据持久化,而文本文件、XML文件都不利于实现CRUD中的所有数据访问操作,而数据库是实现CRUD这四种操作都比较便利的持久化方式,所以,一般在讨论数据持久化实,默认指的都是使用数据库存储数据从而实现数据的持久化。

在项目中,会将代码划分一些层次,各层用于解决不同的问题,其中,持久层就是用于解决数据持久化问题的,甚至,简单来说,持久层对应的就是数据库编程的相关文件或代码。

目前,使用Mybatis技术实现持久层编码,需要:

  • 编写一次性的基础配置
    • 使用@MapperScan指定Mapper接口所在的Base Package
    • 指定配置SQL语句的XML文件的位置(如果采取XML映射文件的方式)
  • 编写每个数据访问功能的代码
    • 在接口中添加必须的抽象方法
      • 可能需要创建相关的POJO类
    • 在XML文件中配置抽象方法映射的SQL语句

关于一次性的配置,@MapperScan注解需要添加在配置类上,有2种做法:

  • 直接将此注解添加在启动类上,因为启动类本身也是配置类
  • 自行创建配置类,在此配置类上添加@MapperScan

如果采用以上的第2种做法,则应该在src\main\java的根包下,创建config.MybatisConfig类,并在此类使用@MapperScan注解:

package cn.henu.kyle.demo.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("cn.henu.kyle.demo.mapper")
public class MybatisConfig {
}

另外,关于指定配置SQL语句的XML文件的位置,需要在application.yml(或application.properties)中配置mybatis.mapper-locations属性,例如:

# Mybatis相关配置
mybatis:
  # 用于配置SQL语句的XML文件的位置
  mapper-locations: classpath:mapper/*.xml

基于以上的配置值,还应该在src/main/resources下自行创建名为mapper的文件夹。

至此,关于使用Mybatis实现数据库编程的一次性配置结束!

接下来,可以使用任何你已知的Mybatis使用方式实现所需的数据访问。

动手编写数据访问层代码

目前,设定目标为:最终实现”添加管理员账号“的业务功能。则在数据访问层需要做到:

  • 插入管理员数据

    • 创建cn.henu.kyle.demo.entity.Admin
    • cn.henu.kyle.demo.mapper包(不存在,则创建)下创建AdminMapper接口,并在接口中声明int insert(Admin admin);方法
    • src/main/resources/mapper文件夹下通过粘贴得到AdminMapper.xml文件,在此文件中配置与以上抽象方法映射的SQL语句
    • 编写完成后,应该及时测试,测试时,推荐在src/test/java的根包下创建mapper.AdminMapperTests测试类,并在此类中编写测试方法
  • 根据用户名查询管理员数据

    • 后续,在每次插入数据之前,会调用此功能进行查询,以此保证”重复的用户名不会被添加到数据库中“
      • 即便在数据表中用户名已经添加了unique,但是,不应该让程序执行到此处
    • AdminMapper接口中添加Admin getByUsername(String username);方法
    • AdminMapper.xml文件中添加与以上抽象方法映射的SQL语句
    • 编写完成后,应该及时测试
  • 其它问题暂不考虑,例如在ams_admin中,其实phoneemail也是设置了unique的,如果完整的实现,则还需要添加根据phone查询管理员的功能,和根据email查询管理员的功能,在不实现这两个功能的情况下,后续进行测试和使用时,应该不使用重复的phoneemail值来测试或执行

  • 以上只是一个编写代码前应该做的思路梳理(很必要,花费再多的时间都要做,脑子清晰了,手敲的就快了),实际代码还是找项目做一做

9.编写业务逻辑层(Service层)

业务逻辑层是被Controller直接调用的层(Controller不允许直接调用持久层),通常,在业务逻辑层中编写的代码是为了保证数据的完整性和安全性,使得数据是随着我们设定的规则而产生或发生变化.

通常,在业务逻辑层的代码会由接口和实现类组成,其中,**接口(Service层的关键)**被视为是必须的

  • 推荐使用基于接口的编程方式
  • 部分框架在处理某些功能时,会使用基于接口的代理模式,例如Spring JDBC框架子啊处理事务时

在接口中,声明抽象方法时,仅以操作成功为前提来设计返回值类型(不考虑失败),如果业务在执行过程中可能出现某些失败(不符合所设定的规则),可以通过抛出异常来表示!

关于抛出的异常,通常是自定义的异常,并且,自定义异常通常是RuntimeException的子类,主要原因:

  • 不必显示的抛出或捕获,因为业务逻辑层的异常用于是抛出的,而控制器层会调用业务逻辑层,在控制器层的Controller中其实也是永远抛出异常的,这些异常会通过Spring MVC同意处理异常的机制进行处理,关于异常的整个过程都是固定流程,所以,没有必要显示抛出或捕获
  • 部分框架在处理某些事情时,默认支队RuntimeException的子孙进行识别并处理,例如Spring JDBC框架在处理事务时

所以,在实际编写业务逻辑层之前,应该先规划一场,例如先创建ServiceException类:

package cn.henu.kyle.demo.ex;

public class ServiceException extends RuntimeException {

}

另外,在插入数据时,如果返回的受影响行数不是1时.必然是某种错误,则创建对应的插入数据异常:

package cn.henu.kyle.demo.ex;

public class InsertException extends ServiceException {

}

关于抽象方法的参数,应该设计为客户端提交的数据类型或对应的封装类型,不可以时对数表对应的实体类型!如果使用封装的类型,这种类型在类名上应该添加某种后缀,例如DTO或其他后缀,例如:

package cn.henu.kyle.demo.pojo.dto;

public class AdminAddNewDTO implements Serializable {
    private String username;
    private String password;
    private String nickname;
    private String avatar;
    private String phone;
    private String email;
    private String description;
    // Setters & Getters
    // hashCode(), equals()
    // toString()
}

然后,在cn.henu.kyle.demo.service包下声明接口及抽象方法:

package cn.henu.kyle.demo.service;

public interface IAdminService {
    void addNew(AdminAddNewDTO adminAddNewDTO);
} 

并在以上service包下创建impl子包,再创建AdminServiceTmpl类:

package cn.henu.kyle.demo.service.impl;

import cn.henu.kyle.demo.entity.Admin;
import cn.henu.kyle.demo.exception.InsertException;
import cn.henu.kyle.demo.exception.UsernameDuplicateException;
import cn.henu.kyle.demo.mapper.AdminMapper;
import cn.henu.kyle.demo.service.dto.AdminAddNewDTO;
import cn.henu.kyle.demo.service.IAdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Service
public class AdminServiceImpl implements IAdminService {

    @Autowired
    private AdminMapper adminMapper;

    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {
        // 通过参数获取用户名
        String username = adminAddNewDTO.getUsername();
        // 调用adminMapper的Admin getByUsername(String username)方法执行查询
        Admin result = adminMapper.getByUsername(username);
        // 判断查询结果是否不为null
        if (result != null) {
            // 是:表示用户名已经被占用,则抛出UsernameDuplicateException
            throw new UsernameDuplicateException("用户名已被占用");
        }

        // 通过参数获取原密码
        String rawPassword = adminAddNewDTO.getPassword();
        // TODO: 通过加密方式,得到加密后的密码encodedPassword
        String encodedPassword = rawPassword; // 暂时不加密

        // 创建当前时间对象now
        LocalDateTime now = LocalDateTime.now();

        // 创建Admin对象
        Admin admin = new Admin();
        // 补全Admin对象的属性值
        admin.setUsername(username);
        admin.setNickname(adminAddNewDTO.getNickname());
        admin.setAvatar(adminAddNewDTO.getAvatar());
        admin.setPhone(adminAddNewDTO.getPhone());
        admin.setEmail(adminAddNewDTO.getEmail());
        admin.setDescription(adminAddNewDTO.getDescription());
        admin.setPassword(encodedPassword); // 密码应加密处理
        admin.setIsEnabled(1); // 假设1表示启用
        admin.setLastLoginIp(null);
        admin.setLoginCount(0);
        admin.setGmtLastLogin(null);
        admin.setGmtCreate(now);
        admin.setGmtModified(now);

        // 调用adminMapper的insert(Admin admin)方法插入管理员数据,获取返回值
        int rows = adminMapper.insert(admin);

        // 判断以上返回的结果是否不为1
        if (rows != 1) {
            // 抛出InsertException异常
            throw new InsertException("插入管理员数据时出现未知错误,请联系系统管理员");
        }
    }
}

以上代码未实现对密码的加密处理!关于密码加密,相关的代码应该定义在别的某个类中,不应该直接将加密过程编写在以上代码中,因为加密的代码需要在多处应用(添加用户、用户登录、修改密码等),并且,从分工的角度上来看,也不应该是业务逻辑层的任务!所以,在cn.henu.kyle.demo.util(包不存在,则创建)下创建PasswordEncoder类,用于处理密码加密:

package cn.henu.kyle.demo.util;

@Component
public class PasswordEncoder {
    
    public String encode(String rawPassword) {
        return "aaa" + rawPassword + "aaa";
    }
    
}

完成后,需要在AdminServiceImpl中自动装配以上PasswordEncoder,并在需要加密时调用PasswordEncoder对象的encode()方法。

// ... 省略其他代码 ...
import cn.henu.kyle.demo.util.PasswordEncoder;

@Service
public class AdminServiceImpl implements IAdminService {

    @Autowired
    private AdminMapper adminMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void addNew(AdminAddNewDTO adminAddNewDTO) {
        // ... 省略其他代码 ...

        // 通过参数获取原密码
        String rawPassword = adminAddNewDTO.getPassword();
        // 通过加密方式,得到加密后的密码encodedPassword
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // ... 省略其他代码 ...

        // 创建Admin对象并使用encodedPassword
        Admin admin = new Admin();
        // ... 省略其他属性设置 ...
        admin.setPassword(encodedPassword); // 使用加密后的密码

        // ... 省略其他代码 ...
    }
}

10.使用Lombok框架

在编写POJO类型(包括实体类、VO、DTO等)时,都有统一的编码规范,例如:

  • 属性都是私有的
  • 所有属性都有对应的Setter和Getter方法
  • 应该重写equals()hashCode()方法,以保证:如果两个对象的字面值完全相同,则equals()对比结果为true,且hashCode()返回值相同,如果两个对象的字面值不相同,则equals()对比结果为false,且hashCode()返回值不同
  • 实现Serializable接口

另外,为了便于观察对象的个属性值,通常还会重写toString()方法.

由于以上操作方式非常固定,且涉及的代码量虽然不然,但是篇幅较长,并且,当类中的属性需要修改时(包括修改原有属性、或增加新属性、删除原有属性),对应的其他方法都需要修改(或重新生成),管理起来比较麻烦.

使用Lombok框架可以极大的简化这些操作,此框架可以通过注解的方式,在编译器来生成Setter和Getter、equals()、hashCode()、toString(),甚至生成构造方法等,所以,一旦使用此框架,开发人员就只需要在类中声明各属性、实现Serializable、添加Lombok指定的注解即可.

在Spring Boot中,添加Lombox依赖,可以在创建项目时勾选,也可以后期自行添加,依赖项的代码如下:

<!-- lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.6</version>
    <scope>provided</scope>
</dependency>

完成后,在各POJO类型中,将不再需要在源代码中添加Setter、Getter、equals()、hashCode()、toString()这些方法,只需要在POJO类上添加@Data注解即可!

为了避免IntelliJ IDEA判断失误而提示了警告和错误,推荐安装Lombok插件(不安装不影响代码编译和运行),可参考:

11.Slf4j日志框架

在开发实践中,不允许使用System.out.println()或类似的输出语句来输出显示关键数据(核心数据、敏感数据等),因为,如果是这样使用,无论是在开发环境中,还是测试环境,还是生产环境中,这些输出语句都将输出相关信息,而删除或添加这些输出语句的操作成本比较高,操作可行性低.

推荐的做法是使用日志框架来输出相关信息!

当添加了Lombox依赖后,可以在需要使用日志的类上添加@Slf4j注解,然后,在类的任意位置,均可以使用名为log的变量,且调用其方法来输出日志(名为log的变量也是Lombox框架在编译期自动补充的声明并创建对象)!

在Slf4j日志框架中,将日志的显示级别根据其重要程度(严重程度)由低到高分为:

  • trace:跟踪信息
  • debug:调试信息
  • info:一般信息,通常不涉及关键流程和敏感数据
  • warm:警告信息,通常代码可以运行,但不够完美,或不规范
  • error:错误信息

在配置文件中,可以通过logging.level.包名.类名来设置当前类的日志级别,例如:

logging.level.cn.henu.kyle.demo.service.impl.AdminServiceImpl: Info

当设置了显示的日志级别后,仅显示设置级别和更重要的级别的日志,例如,设置为info时,只显示infowarnerror,不会显示debugtrace级别的日志!

当输出日志时,通过log变量调用trace()方法输出的日志就是trace级别的,调用debug()方法输出的日志就是debug级别的,以此类推,可调用的方法还有info()warn()error().

在开发实践中,关键数据和敏感数据都应该通过trace()dubug()进行输出,在开发环境中,可以将日志的显示级别设置为trace级别,则会显示所有日志,当需要交付到生产环境中时,只需将日志的显示级别调整为info即可.

默认情况下,日志的显示级别时info,所以,即使没有在配置文件中进行正确的配置,所有info,warn,error级别的日志都会输出显示.

在配置时,属性名称中logging.level部分时必须的,在其后,必须写至少一级包名,例如:

logging.level.cn: trace

以上配置表示cn包及其子孙包下的所有类中的日志都按照trace级别进行显示!

在开发实践中,属性名称通常配置为logging.level.项目根包,例如:

logging.level.cn.henu.kyle.demo: trace

在使用Slf4j时,通过log调用的每种级别的方法都被重载了多次(各级别对应除了方法名称不同,重载的次数和参数列表均相同),推荐使用的方法是参数列表为(String format,Object... arguments)的,例如:

public void trace(String format, Object... arguments);
public void debug(String format, Object... arguments);
public void info(String format, Object... arguments);
public void warn(String format, Object... arguments);
public void error(String format, Object... arguments);

以上方法中,第1个参数是将要输出的字符串的模式(模版),在此字符串中,如果需要包含某个变量值,则使用{}表示,如果有多个变量值,均是如此,然后,再通过第2个参数(是可变参数)依次表示各{}对应的值,例如:

log.debug("加密前的密码:{},加密后的密码:{}", password, encodedPassword);

使用这种做法,可以避免多变量时频繁的拼接字符串,另外,日志框架会将第1个参数进行缓存,以此提高后续每一次的执行效率。

在开发实践中,应该对程序执行关键位置添加日志的输出,通常包括:

  • 每个方法的第1行有效语句,表示代码已经执行到此方法内,或此方法已经被成功调用
    • 如果方法是有参数的,还应该输出参数的值
  • 关键数据或核心数据在改变之前和之后
    • 例如对密码加密时,应该通过日志输出加密前和加密后的密码
  • 重要的操作执行之前
    • 例如尝试插入数据之前、修改数据之前,应该通过日志输出相关值
  • 程序走到某些重要的分支时
    • 例如经过判断,走向抛出异常之前

其实,Slf4j日志框架只是日志的一种标准,并不是具体的实现(感觉上与Java中的接口有点相似),常见有具体实现了日志功能的框架有log4j、logback等,为了统一标准,所以才出现了Slf4j,同时,由于log4j、logback等框架实现功能并不统一,所以,Slf4j提供了对主流日志框架的兼容,在Spring Boot工程中,pring-boot-starter就已经依赖了spring-boot-starter-logging,而在此依赖下,通常包括Slf4j、具体的日志框架、Slf4j对具体日志框架的兼容。

12.密码加密

【这并不是Spring Boot框架的知识点,但是十分有趣,推荐学习】
对密码进行加密,可以有效地保障密码安全,即使出现数据库泄密,密码安全也不会受到影响!为了实现此目标,需要在对密码进行加密时,使用不可逆的算法进行处理!

通常,不可以使用加密算法对密码进行密码处理,从严格意义上看,所有的加密算法都是可以逆向运算的,即同时存在加密和解密两种操作,加密算法只能用于保证传输过程的安全,并不应该用于保存需要存储下来的密码的安全!

哈希算法都是不可逆的,通常,用于处理密码加密的算法中,典型的是一些消息摘要算法,例如MD5、SHA256或以上位数的算法.

消息摘要算法的主要特征有:

  • 消息相同时,摘要一定相同
  • 某种算法,无论消息长度多少,摘要的长度是固定的
  • 消息不同时,摘要几乎不会相同

在消息摘要算法中,以MD5为例,其运算结果是一个128位长度的二进制数,通常会转换成十六进制数显示,所以是32位长度的十六进制数,MD5也被称之为128位算法。理论上,会存在2的128次方种类的摘要结果,且对应2的128次方种不同的消息,如果在未超过2的128次方种消息中,存在2个或多个不同的消息对应了相同的摘要,则称之为:发生了碰撞。一个消息摘要算法是否安全,取决其实际的碰撞概率,关于消息摘要算法的破解,也是研究其碰撞概率。

存在穷举消息和摘要的对应关系,并利用摘要在此对应关系进行查询,从而得知消息的做法,但是,由于MD5是128位算法,全部穷举是不可能实现的,所以,只要原始密码(消息)足够复杂,就不会被收录到所记录的对应关系中去!

为了进一步提高密码的安全性,在使用消息摘要算法进行处理时,通常还会加盐!盐值可以是任意的字符串,用于与密码一起作为被消息摘要算法运算的数据即可,例如:

@Test
public void md5Test() {
    String rawPassword = "123456";
    String salt = "kjfcsddkjfdsajfdiusf8743urf";
    String encodedPassword = DigestUtils.md5DigestAsHex(
            (salt + salt + rawPassword + salt + salt).getBytes());
    System.out.println("原密码:" + rawPassword);
    System.out.println("加密后的密码:" + encodedPassword);
}

加盐的目的是使得被运算数据变得更加复杂,盐值本身和用法并没有明确要求!

甚至,在某些用法或算法中,还会使用随机的盐值,则可以使用完全相同的原消息对应的摘要却不同!

推荐了解:预计算的哈希链、彩虹表、雪花算法。

为了进一步保证密码安全,还可以使用多重加密,即反复调用消息摘要算法.

除此以外,还可以使用安全系数更高的算法,例如SHA-256是256位算法,SHA-384是384位算法,SHA-512是512位算法。

一般的应用方式可以是:

public class PasswordEncoder {

    public String encode(String rawPassword) {
        // 加密过程
        // 1. 使用MD5算法
        // 2. 使用随机的盐值
        // 3. 循环5次
        // 4. 盐的处理方式为:盐 + 原密码 + 盐 + 原密码 + 盐
        // 注意:因为使用了随机盐,盐值必须被记录下来,本次的返回结果使用$分隔盐与密文
        String salt = UUID.randomUUID().toString().replace("-", "");
        String encodedPassword = rawPassword;
        for (int i = 0; i < 5; i++) {
            encodedPassword = DigestUtils.md5DigestAsHex(
                    (salt + encodedPassword + salt + encodedPassword + salt).getBytes());
        }
        return salt + encodedPassword;
    }

    public boolean matches(String rawPassword, String encodedPassword) {
        String salt = encodedPassword.substring(0, 32);
        String newPassword = rawPassword;
            for (int i = 0; i < 5; i++) {
                newPassword = DigestUtils.md5DigestAsHex(
                        (salt + newPassword + salt + newPassword + salt).getBytes());
        }
        newPassword = salt + newPassword;
        return newPassword.equals(encodedPassword);
    }

}

13.编写控制器层

Spring MVC是用于处理控制器层开发的,在使用Spring Boot时,在pom.xml中添加spring-boot-starter-web即可整合Spring MVC框架及相关的常用依赖项(包含jackson-databind),可以将已存在的spring-boot-starter直接改为spring-boot-starter-web,因为在spring-boot-starter-web中已经包含了spring-boot-starter

先在项目的根包下创建controller子包,并在此子包下创建AdminController,此类应该添加@RestController@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")注解,例如:

@RestController
@RequestMapping(values = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
    
}

由于已经决定了服务器端响应时,将响应JSON格式的字符串,为保证能够响应JSON格式的结果,处理请求的方法返回值应该是自定义的数据类型,即JsonResult类及相关类型,如下:。

package cn.henu.kyle.demo.util;

public class JsonResult<T> {
    private Integer state;
    private String message;
    private T data;

    // 无参构造器
    public JsonResult() {}

    // 响应状态和消息的构造器
    public JsonResult(Integer state, String message) {
        this.state = state;
        this.message = message;
    }

    // 响应所有数据的构造器
    public JsonResult(Integer state, String message, T data) {
        this.state = state;
        this.message = message;
        this.data = data;
    }

    // Getter和Setter方法
    // ...省略...

    // 成功的快捷方法
    public static <E> JsonResult<E> ok(E data) {
        return new JsonResult<E>(200, "success", data);
    }
    
    // 错误的快捷方法
    public static <E> JsonResult<E> fail(Integer state, String message) {
        return new JsonResult<E>(state, message);
    }
}

接下来,即可在AdminController中添加处理“增加管理员”的请求:

@RestController
@RequestMapping(values = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
    @Autowired
    private IAdminService adminService;

    // 注意:暂时使用@RequestMapping,不要使用@PostMapping,以便于直接在浏览器中测试
    // http://localhost:8080/admins/add-new?username=root&password=1234
    @RequestMapping("/add-new") 
    public JsonResult<Void> addNew(AdminAddNewDTO adminAddNewDTO) {
        // 调用服务层的方法添加管理员
        adminService.addNew(adminAddNewDTO);
        // 响应成功结果
        return JsonResult.ok();
    }
}

完成后,运行启动类,即可启动整个项目,在spring-boot-starter-web中,包含了Tomcat的依赖项,在启动时,会自动将当前项目打包并部署到此Tomcat上,所以,执行启动类时,会执行此tomcat,同时,因为是内置的Tomcat,只为当前项目服务,所以,在将项目部署到Tomcat时,默认已经将Context Path(例如spring_mvn_war_exploded)配置为空字符串,所以,在启动项目后,访问的URL中并没有此前遇到的Context Path值.

当项目启动成功后,即可在浏览器的地址栏中输入网址进行测试访问!

注意:如果是未添加的管理员账号,可以成功执行结束,如果管理员账号已经存在,由于尚未处理异常,会提示500错误。

关于处理异常,应该先在State(中央地方)中确保有每种异常对应的枚举值,例如本次需要补充InsertException对应的枚举值:

public enum State {

    OK(200),
    ERR_USERNAME(201),
    ERR_PASSWORD(202),
    ERR_INSERT(500); // 新增的枚举值

    // 原有其它代码

}

然后,在cn.henu.kyle.demo.controller下创建handler.GlobalExceptionHandler类,用于统一处理异常,例如:

package cn.henu.kyle.demo.controller.handler;

import cn.henu.kyle.demo.ex.ServiceException;
import cn.henu.kyle.demo.ex.UsernameDuplicateException;
import cn.henu.kyle.demo.web.JsonResult;
import cn.henu.kyle.demo.web.State;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(InsertException.class)
    public JsonResult<Void> handleInsertException(InsertException e) {
        if (e instanceof UsernameDuplicateException) {
            return JsonResult.fail(State.ERR_USERNAME, "用户名错误!");
        } else {
            return JsonResult.fail(State.ERR_INSERT, "插入数据失败!");
        }
    }

}

InsertException被抛出时,GlobalExceptionHandler里面的handleInsertException方法将会被调用,并且返回一个包含错误代码和消息的JSON响应。这种方式使得错误响应的创建变得简洁,并且易于维护。JsonResult类是一个假定存在的用于封装JSON响应的类,InsertException是一个假定的自定义异常类,你应该根据实际需要来定义它们。

完成后,重新启动项目,当添加管理员时的用户名没有被占用时,将正常添加,当用户名已经被占用时,会根据处理异常的结果进行响应!

由于在统一处理异常的机制下,同一种异常,无论是在哪种业务中出现,处理异常时的描述信息都是完全相同的,也无法精准的表达错误信息,这是不合适的!另外,基于面向对象的“分工”思想,关于错误信息(异常对应的描述信息),应该是由Service来描述,即“谁抛出谁描述”,因为抛出异常的代码片段是最了解、最明确出现异常的原因的!

为了更好的描述异常的原因,应该在自定义的InsertException和其子孙类异常中添加基于父类的全部构造方法(5个),然后,在AdminServiceImpl中,当抛出异常时,可以在异常的构造方法中添加String类型的参数,对异常发生的原因进行描述,例如:

@Override
public void addNew(AdminAddNewDTO adminAddNewDTO) {
    // ===== 原有其它代码 =====
    
    // 判断查询结果是否不为null
    if (queryResult != null) {
        // 是:表示用户名已经被占用,则抛出UsernameDuplicateException
        log.error("此账号已经被占用,将抛出异常");
        throw new UsernameDuplicateException("添加管理员失败,用户名(" + username + ")已经被占用!");
    }

    // ===== 原有其它代码 =====

    // 判断以上返回的结果是否不为1,抛出InsertException异常
    if (rows != 1) {
        throw new InsertException("添加管理员失败,服务器忙,请稍后再次尝试!");
    }
}

最后,在处理异常时,可以调用异常对象的getMessage()方法获取抛出时封装的描述信息,例如:

@ExceptionHandler(ServiceException.class)
public JsonResult<Void> handleServiceException(ServiceException e) {
    if (e instanceof UsernameDuplicateException) {
        return JsonResult.fail(State.ERR_USERNAME, e.getMessage());
    } else {
        return JsonResult.fail(State.ERR_INSERT, e.getMessage());
    }
}

完成后,再次重启项目,当用户名已经存在时,可以显示在Service中描述的错误信息!

最后,当添加成功时,响应的JSON数据例如:

{
    "state":200,
    "message":null,
    "data":null
}

当用户名冲突,添加失败时,响应的JSON数据例如:

{
    "state":201,
    "message":"添加管理员失败,用户名(liuguobin)已经被占用!",
    "data":null
}

可以看到,无论是成功还是失败,响应的JSON中都包含了不必要的数据(为null的数据),这些数据属性是没有必要响应到客户端的,如果需要去除这些不必要的值,可以在对应的属性上使用注解进行配置,例如:

@Data
public class JsonResult<T> implements Serializable {

    // 状态码,例如:200
    private Integer state;
    // 消息,例如:"登录失败,用户名不存在"
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String message;
    // 数据
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;
    
    // ===== 原有其它代码 =====
    
}

则响应的JSON中只会包含不为null的部分。

此注解还可以添加在类上,则作用于当前类中所有的属性,例如:

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonResult<T> implements Serializable {

    // ===== 原有其它代码 =====
    
}

即使添加在类上,也只对当前类的3个属性有效,后续,当响应某些数据时,data属性可能是用户、商品、订单等类型,这些类型的数据中为null的部分依然会被响应到客户端去,所以,还需要对这些类型也添加相同的注解配置!

以上做法相对比较繁琐,可以在application.properties / application.yml中添加全局配置,则作用于当前项目中所有响应时涉及的类,例如在properties中配置为:

spring.jackson.default-property-inclusion=non_null

yml中配置为:

spring:
  jackson:
    default-property-inclusion: non_null

注意:当你需要在yml中添加以上配置时,前缀属性名可能已经存在,则不允许出现重复的前缀属性名,例如以下配置就是错误的:

spring:
  profiles:
    active: dev
spring: # 此处就出现了相同的前缀属性名,是错误的
  jackson:
    default-property-inclusion: non_null

正确的配置例如:

spring:
  profiles:
    active: dev
  jackson:
    default-property-inclusion: non_null

最后,以上配置只是“默认”配置,如果在某些类型中还有不同的配置需求,仍可以在类或属性上通过@JsonInclude进行配置。

  • 52
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
微擎是一款基于PHP的开源微信公众号管理系统,可以帮助开发者快速搭建微信公众号应用。本将介绍微擎开发基础知识,包括环境搭建、目录结构、配置文件、路由规则、模块开发等内容。 ### 环境搭建 微擎的开发需要先搭建好开发环境,包括PHP、MySQL、Apache或Nginx等。具体步骤可以参考微擎的官方文档,建议使用PHP7及以上版本。 ### 目录结构 微擎的目录结构如下: ``` addons/ // 存放插件 api/ // 存放接口文件 app/ // 存放前台模块 attachment/ // 存放上传的文件 framework/ // 存放框架核心文件 payment/ // 存放支付相关文件 static/ // 存放静态资源文件 template/ // 存放前台模板文件 upgrade/ // 存放升级相关文件 wap/ // 存放手机端模块 we7/ // 微擎的核心目录 favicon.ico // 网站图标 index.php // 入口文件 ``` ### 配置文件 微擎的配置文件主要有两个:config.php和database.php,前者用于配置微擎的基础信息,后者用于配置数据库连接信息。 config.php的配置项包括: - `setting`:微擎基础设置,如网站名称、网站域名、微信公众号AppID等; - `site`:网站设置,如首页模板、错误页面模板、静态资源路径等; - `wxapp`:小程序设置,如小程序名称、小程序AppID、小程序Secret等; - `oauth`:公众号设置,如公众号AppID、公众号Secret等; - `remote`:远程附件设置,如附件存储方式、FTP信息等; - `app`:应用设置,如开启模块、插件管理等; - `global`:全局设置,如错误处理方式、调试模式等。 database.php的配置项包括: - `master`:主数据库连接信息,如数据库类型、主机地址、数据库用户名、密码等; - `slave`:从数据库连接信息,可选; - `tablepre`:数据表前缀。 ### 路由规则 微擎的路由规则遵循MVC模式,每个请求都会依据路由规则被分配到相应的控制器和方法中。微擎的路由规则主要有两种: - URL规则:URL规则是通过URL中的参数来确定控制器和方法的,如`index.php?c=site&a=entry&m=my_module&do=my_page`; - Rewrite规则:Rewrite规则通过修改服务器配置文件,将URL中的参数转换为美观的URL,如`/my_module/my_page`。 ### 模块开发 微擎的模块开发主要包括前台模块和后台模块两种。前台模块用于展示内容,后台模块用于管理内容。 模块的目录结构如下: ``` my_module/ ├── controller/ │ ├── index.php │ └── my_page.php ├── model/ │ └── my_page.php ├── template/ │ ├── index.html │ └── my_page.html ├── module.php └── version.php ``` 其中,controller目录存放控制器文件,model目录存放模型文件,template目录存放模板文件,module.php是模块的基本信息,version.php是模块的版本信息。 控制器文件中定义了控制器和方法,模型文件中定义了数据操作方法,模板文件中定义了页面布局和样式。模块的基本信息包括模块名称、模块描述、模块图标等,版本信息包括版本号、升级文件等。 ### 小结 本介绍了微擎开发基础知识,包括环境搭建、目录结构、配置文件、路由规则、模块开发等内容。掌握这些知识是进行微擎开发基础,希望可以帮助到初学者。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值