# 零. 本文环境

  • IntelliJ IDEA 2018.2
  • java 1.8
  • mysql 8.0.12

版本不苛求一致,基于自身团队版本即可。建议选择最新的稳定版本,一般该版本兼容旧版本且出错也存在较好的纠错生态。

# 一. 项目初始化

  1. 新建项目(New Project),选择Spring Initializr,即Spring Boot,下一步全部默认值,点击Next。也可适当修改路径名,这里为 com.springboot.study

springboot

springboot

  1. 选择Web选项卡下的Web。这里注意springboot版本不同,名称或有不同,但区别不大。

springboot

  1. 此时目录结构如下:

springboot

目前新手学习,暂时只需知道三个地方:

  • src下的java下的com.springboot.study文件夹,即主要的代码编辑区。其中的Application.java 即是项目启动类。注意这里的小数点表示的是文件夹的层级递进关系,只是Intellij工具的配置显示问题。

  • src下的resources下的application.properties文件,即配置文件。

  • 根目录下的pom.xml文件,即依赖管理文件。

此时,可以看到pom.xml有这么一段:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

仔细观察名称,就会发现这其实就是刚刚第二步所引入的依赖。

# 二. controller层的建立

com.springboot.study 下,新建controller包(控制层),来个 HelloController.java

package com.springboot.study.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @RequestMapping("/hello")// 建立请求映射
    public String hello(){
        return "hello springboot";
    }
}

@RestController 注解声明这是controller层。

@RequestMapping 注解建立请求的映射路径,默认GET请求。

可以将鼠标光标移至相关代码下方,通过 Alt+Enter 键来快速导包。

直接运行。

springboot

访问 http://localhost:8080/hello,成功。

springboot

# 三. bean层的建立

  1. com.springboot.study 下,新建bean包(实体层),来个 ReturnInfo.java ,作为返回信息的封装。

    package com.springboot.study.bean;
    
    public class ReturnInfo {
        private Integer code;
        private Object data;
        private String message;
        
        public ReturnInfo(Integer code, Object data, String message) {
            this.code = code;
            this.data = data;
            this.message = message;
        }
        
        // Getter 和 Setter方法略,后同。
    }
    

    可以通过 Alt+Insert 键来快速生成Constructor,Getter和Setter方法,

    再在 HelloController.java

    @RequestMapping("/getReturnInfo")
    public ReturnInfo getHello(){
        return new ReturnInfo(0, new Date(), null);
    }
    

    运行,得到:

    springboot

    这里选择将返回信息封装为三个部分:code用于判断操作的成功与失败,data储存相应的返回信息,message处于两者的之间,既是code的补充,也是data的精简。

  2. (此步可略)值得一提的是,message作为null值也同样返回了,在某些时候,这样也许会有些难受,此时可以借用alibaba的fastjson包来进行“美化”。

    首先导包,在 pom.xml 文件里引入依赖:

    <!--fastjosn依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>
    

    其次,在 Application.java 文件里写入配置项:

    @SpringBootApplication
    public class StudyApplication extends WebMvcConfigurationSupport {
    
        @Override
        public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
            //1、定义一个convert转换消息的对象
            FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
            //2、添加fastjson的配置信息
            FastJsonConfig fastJsonConfig = new FastJsonConfig();
            fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
            //3、在convert中添加配置信息
            fastConverter.setFastJsonConfig(fastJsonConfig);
            //4、将convert添加到converters中
            converters.add(fastConverter);
            //5、追加默认转换器
            super.addDefaultHttpMessageConverters(converters);
        }
    
        public static void main(String[] args) {
            SpringApplication.run(StudyApplication.class, args);
        }
    
    }
    

    再次运行,即可得到:

    springboot

  3. 学习至此,实际上算是完成了服务端对客户端简单的服务提供,但很显然现在还缺少最重要的一环,就是真实的数据。所以,理所当然,下一步应当是建立起服务端与数据库的连接。

    这里选择使用mysql作为数据库,使用Java Persistence API(简称jpa)进行连接。

    首先导包,在 pom.xml 文件里引入依赖。注意mysql和mysql驱动的的版本对应。

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <mysql-connector.version>8.0.11</mysql-connector.version>
    </properties>
    
    <dependencies>
        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector.version}</version>
        </dependency>
        <!--jpa依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    
~~~

其次,在 application.properties 文件里写入配置。注意数据库账号和密码的对应。

#数据库连接信息
spring.datasource.url = jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=false
spring.datasource.username = root
spring.datasource.password = 123456
#驱动名
spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver
#运行时的端口号
server.port = 8080
#SrpingBoot 2.0 版本中,Hibernate 创建数据表的时候,默认的数据库存储引擎选择的是 MyISAM (之前好像是 InnoDB,这点比较诡异)。这个参数是在建表的时候,将默认的存储引擎切换为 InnoDB 用的。
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#ddl-auto:update----每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新
spring.jpa.hibernate.ddl-auto=update
#是否在控制台展示sql
spring.jpa.show-sql=true
spring.jpa.database=mysql
#初始化数据库字段的时候采用遇大写字母加下划线的命名规则,如userId映射为user_id; 另一种命名规则PhysicalNamingStrategyStandardImpl则为保持原始的名字
spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

接着,新建一个bean类,UserInfo.java

package com.springboot.study.bean;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "user_info")
public class UserInfo {
    @Id
    private String userId;
    private String userName;
    private String password;
    private String phoneNumber;
    private String headPic;
 
    // Getter 和 Setter方法略
}

@Entity 注解声明这是实体类,默认实体类名对应数据库的表名。

@Table 注解用于说明指定不同表名。name指明表名。

@Id 注解声明主键。

再次运行,可以发现数据库里已经自动建表:

springboot

另外,与 @Id 经常搭配使用的还有两个注解:@GeneratedValue@GenericGenerator

@GeneratorValue注解----JPA通用策略生成器

@GenericGenerator注解----自定义主键生成策略

@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
private String userId;

这里使用uuid来作为主键。uuid是由计算机随即生成的一串字符串,重复的概率极低。实际上uuid已经不适合作为主键,因为它的性能并不好,只是uuid是自带的Id生成策略,正好用于初学。

# 四. mapper层的建立

  1. com.springboot.study 下,新建mapper包(持久层),来个 UserMapper.java

    package com.springboot.study.mapper;
    
    import com.springboot.study.bean.UserInfo;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public interface UserMapper extends JpaRepository<UserInfo, String> {
    }
    

    @Repository 注解声明这是dao层。即这里的mapper层。

    继承的JpaRepository是简单查询的接口,可以减少SQL语句的书写。与其对应的还有JpaSpecificationExecutor复杂查询。

  2. 新建 UserController.java 来测试一下。

    package com.springboot.study.controller;
    
    import com.springboot.study.bean.ReturnInfo;
    import com.springboot.study.bean.UserInfo;
    import com.springboot.study.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
    //    自动装配,变量形式注入
        @Autowired
        private UserMapper userMapper;
    
        // 新建用户
        @PostMapping()
        public ReturnInfo createUser(@RequestBody UserInfo userInfo) {
            userMapper.saveAndFlush(userInfo);
            return new ReturnInfo(0, null, "success");
        }
    }
    

    @Autowired 注解可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。

    @PostMapping 注解是 @RequestMapping(value = "/", method = RequestMethod.POST) 的简写。

    运行,使用postman测试。postman是一款接口测试软件,安装部分不多作介绍。

    springboot

    提交。

    springboot

    可以看到数据库也已经新增了这一条记录。

    springboot

    此时,我们其实已经完成了简单的用户注册功能,算是成功入门。不妨总结一下。

    目录结构如下:

    springboot

    清晰明了,bean层负责项目的实体类,并对应数据表的连接;controller层负责业务逻辑的控制,以交互客户端;mapper层负责数据库操作。当然,后续还有其他层面的划分,暂不细说。

    不过,细心的话,会发现每次成功的返回都要 new ReturnInfo(0, null, "success"),这显然不友好,所以,通常也会再将其封装一次,作为返回信息工具类。

    # 五. utils包的建立

    1. com.springboot.study 下,新建utils包(工具类集合)。因为 ReturnInfo类 也不算真正意义上的bean类,所以这里选择将 ReturnInfo.java 改写进 com.springboot.study.utils 中 ,并封装一下操作成功时的信息返回。

      当然,也可以选择将 ReturnInfo.java 在bean包中,在utils包里另建文件封装 ReturnInfo.java ,二者做法各有优劣。本文选择前者,这里注意要删除之前的 ReturnInfo.java 文件,并且修改之前的导包路径,避免名称重复的导包问题。

      package com.springboot.study.utils;
      
      public class ReturnInfo {
          private Integer code;
          private Object data;
          private String message;
      
          private ReturnInfo(Integer code, Object data, String message) {
              this.code = code;
              this.data = data;
              this.message = message;
          }
      
          public static ReturnInfo success() {
              return new ReturnInfo(0, null, "success");
          }
      
          public static ReturnInfo success(Object data) {
              return new ReturnInfo(0, data, "success");
          }
      }
      
    2. 接着,操作失败的信息返回也封装一下,这里增加为一个枚举类。在utils里新建一个 Error.java

      package com.springboot.study.utils;
      
      public enum Error {
          FORMAT_ERROR(4000, "数据格式错误");
      
          // 成员变量
          private Integer code;
          private String message;
      
          Error(Integer code, String message) {
              this.code = code;
              this.message = message;
          }
          
          // getter 和 setter 
      }
      

      然后,同样,再次封装进 ReturnInfo.java

      public static ReturnInfo error(Error error){
          return new ReturnInfo(error.getCode(), null, error.getMessage());
      }
      
      public static ReturnInfo error(Error error, Object data){
          return new ReturnInfo(error.getCode(), data, error.getMessage());
      }
      
    3. 测试一下,同时将用户新建的接口完善一下。

      import com.springboot.study.utils.Error;
      
      /**
       * 新建用户
       * @param userInfo 用户实体
       * @return ReturnInfo
       * 注:这是一种注释形式,也算是一种代码规范,IntelliJ工具可以通过输入"/**" + Enter键自动生成
       */
      @PostMapping()
      public ReturnInfo createUser(@RequestBody UserInfo userInfo) {
          Map<String, String> map = isFormat(userInfo);
          if(!map.isEmpty()) {
              // 注意Error的导入
              return ReturnInfo.error(Error.FORMAT_ERROR, map);
          }
          userMapper.saveAndFlush(userInfo);
          return ReturnInfo.success();
      }
      
      // 表单的验证
      private Map<String, String> isFormat(UserInfo userInfo) {
          Map<String, String> map = new HashMap<>();
          // 判断用户名是否为空和是否格式正确
          if (StringUtils.isEmpty(userInfo.getUserName()) || !checkRealName(userInfo.getUserName())) {
              map.put("userName", "wrong");
          }
          // 判断用户名是否存在,以保证后续登录的唯一性
          if(userMapper.countByUserName(userInfo.getUserName()) > 0){
                  map.put("userName", "exist");
              }
          if (StringUtils.isEmpty(userInfo.getPassword())) {
              map.put("password", "wrong");
          }
          if (StringUtils.isEmpty(userInfo.getPhoneNumber()) || !checkPhoneNumber(userInfo.getPhoneNumber())) {
              map.put("phoneNumber", "wrong");
          }
          return map;
      }
      
      // 检查手机号码合法性
      private Boolean checkPhoneNumber(String phoneNumber) {
          return phoneNumber.matches("^1[3456789]\\d{9}$");
      }
      
      

    // 检查姓名合法性 private Boolean checkRealName(String realName) { return realName.matches("^[\u4E00-\u9FA5A-Za-z]+$"); }

    
    ~~~java
    package com.springboot.study.mapper;
    
    @Repository
    public interface UserMapper extends JpaRepository<UserInfo, String> {
        // 根据用户名查询用户数量,以判断用户是否存在
     int countByUserName(String userName);
    }
    

    运行测试:

    springboot

    springboot

    成功。

    至此,springboot学习(一)结束。

    # 六. (选学)snowflake 生成Id策略

snowflake 算法是 twitter 开源的分布式 Id 生成算法,一个 64 位的 long 型的 Id,1 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 Id,12 bit 作为序列号。

  1. 在utils里,新建 SnowflakeId.java

    package com.springboot.study.utils;
    
    import org.hibernate.HibernateException;
    import org.hibernate.engine.spi.SharedSessionContractImplementor;
    import org.hibernate.id.IdentityGenerator;
    
    import java.io.Serializable;
    
    // Long型继承IdentityGenerator,String型继承UUIDGenerator
    public class SnowflakeId extends IdentityGenerator {
        @Override
        public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
            return getId();
        }
    
        // ==============================Fields===========================================
        /**
         * 开始时间截 (2015-01-01)
         */
        private static final long TWEPOCH = 1420041600000L;
    
        /**
         * 机器id所占的位数
         */
        private static final long WORKER_ID_BITS = 5L;
    
        /**
         * 数据标识id所占的位数
         */
        private static final long DATA_CENTER_ID_BITS = 5L;
    
        /**
         * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
         */
        private static final long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS);
    
        /**
         * 支持的最大数据标识id,结果是31
         */
        private static final long MAX_DATA_CENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS);
    
        /**
         * 序列在id中占的位数
         */
        private static final long SEQUENCE_BITS = 12L;
    
        /**
         * 机器ID向左移12位
         */
        private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
    
        /**
         * 数据标识id向左移17位(12+5)
         */
        private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    
        /**
         * 时间截向左移22位(5+5+12)
         */
        private static final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
    
        /**
         * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
         */
        private static final long SEQUENCE_MASK = -1L ^ (-1L << SEQUENCE_BITS);
    
        /**
         * 工作机器ID(0~31)
         */
        private static long workerId;
    
        /**
         * 数据中心ID(0~31)
         */
        private static long datacenterId;
    
        /**
         * 毫秒内序列(0~4095)
         */
        private static long sequence = 0L;
    
        /**
         * 上次生成ID的时间截
         */
        private static long lastTimestamp = -1L;
    
        //==============================Constructors=====================================
    
        public SnowflakeId() {
        }
    
        private static SnowflakeId snowflakeId=null;
    
        public static synchronized SnowflakeId getInstance(){
            if(snowflakeId==null){
                snowflakeId=new SnowflakeId();
            }
            return snowflakeId;
        }
    
    
        /**
         * 构造函数
         *
         * @param workerId     工作ID (0~31)
         * @param datacenterId 数据中心ID (0~31)
         */
        private SnowflakeId(long workerId, long datacenterId) {
            if (workerId > MAX_WORKER_ID || workerId < 0) {
                throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID));
            }
            if (datacenterId > MAX_DATA_CENTER_ID || datacenterId < 0) {
                throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
            }
            SnowflakeId.workerId = workerId;
            SnowflakeId.datacenterId = datacenterId;
        }
    
        // ==============================Methods==========================================
    
        /**
         * 获得下一个ID (该方法是线程安全的)
         *
         * @return SnowflakeId
         */
        public static synchronized long getId() {
            long timestamp = timeGen();
    
            //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (timestamp < lastTimestamp) {
                throw new RuntimeException(
                        String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
    
            //如果是同一时间生成的,则进行毫秒内序列
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & SEQUENCE_MASK;
                //毫秒内序列溢出
                if (sequence == 0) {
                    //阻塞到下一个毫秒,获得新的时间戳
                    timestamp = tilNextMillis(lastTimestamp);
                }
            }
            //时间戳改变,毫秒内序列重置
            else {
                sequence = 0L;
            }
    
            //上次生成ID的时间截
            lastTimestamp = timestamp;
    
            //移位并通过或运算拼到一起组成64位的ID
            return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
                    | (datacenterId << DATA_CENTER_ID_SHIFT)
                    | (workerId << WORKER_ID_SHIFT)
                    | sequence;
        }
    
        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         *
         * @param lastTimestamp 上次生成ID的时间截
         * @return 当前时间戳
         */
        protected static long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        /**
         * 返回以毫秒为单位的当前时间
         * @return 当前时间(毫秒)
         */
        protected static long timeGen() {
            return System.currentTimeMillis();
        }
    }
    
    
  2. 添加注解。

    @Id
    @GeneratedValue(generator = "snowflakeId")
    @GenericGenerator(name = "snowflakeId", strategy = "com.springboot.study.utils.SnowflakeId")
    private Long userId;
    

    注意Long型的数据类型,strategy 需要写全包名

  3. 运行测试。

    springboot

    可以看到Id已经变成了snowflakeId,成功。因为此时的数据库的字段设置类型是varchar,所以兼容uuid,可以选择删表重建,或修改相应字段的数据类型。

    本文结束。


# 德狗子镇楼

springboot