April 21, 2018

快速搭建生产级别应用

How to build a sample spring boot project.

1. 什么是spring boot?

随着使用 Spring 进行开发的个人和企业越来越多,Spring 也慢慢从一个单一简洁的小框架变成一个大而全的开源软件,Spring 的边界不断的进行扩充,到了后来 Spring 几乎可以做任何事情了,市面上主流的开源软件、中间件都有 Spring 对应组件支持,人们在享用 Spring 的这种便利之后,也遇到了一些问题。 Spring 每集成一个开源软件,就需要增加一些基础配置,慢慢的随着人们开发的项目越来越庞大,往往需要集成很多开源软件,因此后期使用 Spirng 开发大型项目需要引入很多配置文件,太多的配置非常难以理解,并容易配置出错,到了后来人们甚至称 Spring 为配置地狱。 Spring 似乎也意识到了这些问题,急需有这么一套软件可以解决这些问题,这个时候微服务的概念也慢慢兴起,快速开发微小独立的应用变得更为急迫,Spring 刚好处在这么一个交叉点上,于 2013 年初开始的 Spring Boot 项目的研发,2014年4月,Spring Boot 1.0.0 发布。 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是: 用来简化Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式。

2. 怎样1分钟构建一个spring boot应用?

(1)intel idea创建new project

(2)选择Spring Initializr

(3)填写项目信息

(4)选择项目默认依赖web

(5)项目创建完成,其中Chapter1Application是spring boot默认启动类,application.properties是默认配置文件,module1包是我新建的示例业务模块

(6)项目创建完成后的pom文件如下。

(7)查看spring-boot-starter-parent的pom文件,发现项目结构是chapter1---->spring-boot-starter-parent---->spring-boot-dependencies,我们在spring-boot-dependencies的pom文件中看到了dependencyManagement标签,dependencyManagement标签一般声明在父POM中,用来声明依赖项的默认版本号,保证所有的子项目都是一个版本号。该依赖不会引入,因此子项目需要显示声明所需要引入的依赖。

(8)我们不直接依赖spring-boot-starter-parent父项目,而是maven import项目spring-boot-dependencies。

(9)我们删掉application.properties文件,新建application.yml文件,使用新式的语法来写配置项。

(10)然后在pom文件中,显式的声明必要的依赖项。

                  
<!-- dependencies标签:声明依赖的jar包 -->
<dependencies>

    <!-- DAO相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>com.zaxxer</groupId>
        <artifactId>HikariCP</artifactId>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
            <exclusion>
                <artifactId>spring-boot-starter-jdbc</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
            <exclusion>
                <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
            <exclusion>
                <artifactId>mybatis-spring</artifactId>
                <groupId>mybatis-spring</groupId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>5.1.4</version>
    </dependency>

    <!-- spring mvc相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- spring test相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- json相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-json</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>

    <!-- spring aop相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- redis java api -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <!-- 日志相关 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
    </dependency>

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>

    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>

    <!-- 一个很好用的通过注解提供getter setter方法的插件 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <!-- end -->
</dependencies>
                  
                

spring boot提供了一系列类似spring-boot-starter-*的模块化依赖,方便你构建你自己的项目,例如spring-boot-starter-web模块就声明了以下依赖。

(11)项目启动。

(12)接下来我们测试下DAO,这里使用mysql的示例库sakila,它的模型如下所示。

在spring boot2.0中,默认连接池是HikariDataSource,因为在代码实现上很高效,所以速度很优秀。我们先配置下mybatis的mapper路径和分页插件

使用pagehelper进行自动分页。

直接使用spring boot的测试类Chapter1ApplicationTests。

打印信息如下。

(13)接下来我们配置下MVC,我们都知道HttpServletRequest类提供的获取请求参数方法getParameter()返回类型为java.lang.String,对于restful接口的断言不够方便,我们在这里提取HttpServletRequest的参数,封装成JSON形式的参数类,以提供获取参数的快捷接口。 首先添加fastjson的依赖

                  
<!-- json相关 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.47</version>
</dependency>
                  
                

定义参数类

                  
/**
 * 描述: 入口参数类
 *
 * @author linfengda
 * @create 2018-08-19 22:46
 */
public class RequestParam extends JSONObject {

    public List<Long> getArrayLong(String key) {
        JSONArray jsonArray = getJSONArray(key);
        if (jsonArray==null){
            return null;
        }
        List<Long> ls = new ArrayList<>(jsonArray.size());
        for (Object ob:jsonArray) {
            if(ob instanceof String){
                String v = (String)ob;
                if (StringUtils.isNotEmpty(v)){
                    ls.add(Long.valueOf(v));
                }
                continue;
            } else if (ob instanceof Long){
                ls.add((Long)ob);
                continue;
            } else if (ob instanceof Integer){
                Integer ov = (Integer)ob;
                ls.add(Long.valueOf(ov.toString()));
                continue;
            }else{
                throw new ParamParesException("参数类型不对");
            }
        }

        return  ls;
    }
......
                  
                

使用ThreadLocal维持线程封闭

                  
/**
 * 描述: 请求参数上下文
 *
 * @author linfengda
 * @create 2018-08-19 22:46
 */
public class RequestParamContext {
    private final static ThreadLocal<RequestParam> REQUEST_PARAM_THREAD_LOCAL = new ThreadLocal();

    public static void setParam(RequestParam requestParam){
        REQUEST_PARAM_THREAD_LOCAL.set(requestParam);
    }

    public static RequestParam getParams(){
        return REQUEST_PARAM_THREAD_LOCAL.get();
    }

    public static void remove(){
        REQUEST_PARAM_THREAD_LOCAL.remove();
    }
}
                  
                

使用aop拦截请求,拦截请求之后可以做很多事情,这里我们仅仅封装下参数。

                  
package com.linfengda.sb.support.api.aop;

import com.linfengda.sb.support.api.context.RequestParamContext;
import com.linfengda.sb.support.api.entity.RequestParam;
import com.linfengda.sb.support.api.util.HttpServletUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.stereotype.Component;

/**
 * 描述: HTTP请求AOP
 *
 * @author linfengda
 * @create 2018-08-19 23:10
 */
@Aspect
@Component
@Slf4j
public class ApiAspect extends ApplicationObjectSupport {

    @Pointcut("execution(public * com.linfengda.sb..api..*(..))")
    public void authPoint() {

    }

    @Around("authPoint()")
    public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        RequestParam requestParam = null;
        Object result = null;
        try {
            requestParam = HttpServletUtil.getParams();
            RequestParamContext.setParam(requestParam);

            Object[] args = proceedingJoinPoint.getArgs();
            result = proceedingJoinPoint.proceed(args);
            return result;
        } finally {
            releaseSource();
        }
    }

    /**
     * 释放资源
     */
    private void releaseSource() {
        RequestParamContext.remove();
    }

}
                  
                

统一controller层参数处理

                  
/**
 * 描述: 基本control控制类
 *
 * @author linfengda
 * @create 2018-08-19 22:51
 */
public class BaseController {

    /** 默认成功的result **/
    public static final Result SUCCESS_RESULT = new Result();

    /**
     * 获取传入参数
     * @return
     * @throws Exception
     */
    protected RequestParam getParams(){
        return RequestParamContext.getParams();
    }

}
                  
                

添加restful api,然后直接使用postman测试请求。

                  
/**
 * 描述: 测试MVC
 *
 * @author linfengda
 * @create 2018-08-16 10:29
 */
@RestController
public class FilmBizController extends BaseController {

    @Resource
    private FilmBizService filmBizService;

    @PostMapping("/film/queryFilmList")
    public Result queryFilmList() throws Exception {

        // 从context中获取封装的请求参数
        RequestParam params = getParams();
        // 使用spring提供的断言
        Assert.notNull(params.getInteger("pageNo"), "分页页码必须为数字且不能为空");
        Assert.notNull(params.getInteger("pageSize"), "分页大小必须为数字且不能为空");

        Page<FilmPlacardInfo> filmPlacardInfoPage = filmBizService.queryFilmPlacardInfo(params);
        return new Result(filmPlacardInfoPage);
    }
}
                  
                

返回信息如下。

(14)如果你没有声明HttpMessageConverter,spring boot2.0默认的json格式解析器为MappingJackson2HttpMessageConverter,我们这里不搞骚操作,因为Jackson目前是最快的json处理方式了。

(15)到这里一个准生产spring boot框架已经搭建起来了,快去试试给它添加缓存(redis)或者集群通信机制(zookeeper)吧。