Maven+SpringBoot+Java 搭建restful 接口API框架到docker部署及遇到的问题记录

背景:

三年前用Spring MVC搭过api服务。最近组内的其它工程是用的SpringBoot,觉得比较好用,于是这次选用的是Maven+SpringBoot+Java,踩坑无数,努力改掉技术上不求甚解的毛病,整理如下。

maven理解:

maven就是一种强大的代码资源整合器,是jar包的管理者。主要包含三个部分自定义(groupId、artifactId、version、properties等)、依赖定义(dependencies)、编译插件(build)。

第一部分“自定义”决定了当前子工程(pom文件在其根目录下)打成一个什么样的jar包(包名、版本号、编码等);

第二部分“依赖定义”把子工程依赖的所有外部jar包都列好(groupId、artifactId、version,maven里边所有的jar包都是靠这些识别的),maven提前下载,代码里import就能直接用。

第三部分“编译插件”决定了用什么plugins把工程代码编译成一个可用的包。如常用的maven-compiler-plugin,springBoot工程还要引入spring-boot-maven-plugin插件,不然打的jar包就运行不了,提示 no main manifest attribute in *.jar ,这里埋了一个我docker部署的时候的一个大坑。

spring理解:

spring是强大的类对象管理器,是class的集合。其核心精神就是注解@,通过注解的配置告诉spring这个类或对象是要干嘛用的,spring就会做调度管理了,例如@controller 表示此类用来接收web请求的控制器等等、@service就是业务层的服务类、@Component是中立组件类等。

两大特色,一是Ioc控制反转,其实就是类管理,通过bean的概念把类变成了一个个虚拟容器,随时取用;二是Aop切面编程,这个我看过但没用过,大概意思就是在一个类的各个生命周期节点上,可以设定一些代码,完成一些日志、安全、缓存和事务管理等相关的处理,实现类解耦(不保证正确,请谨慎参考)。

工程设计:

工程结构上采用基础服务和前端web服务分离的方式。

common中主要处理后端数据交互及核心数据逻辑处理。此处的配置主要是DB的配置(Jdbc+application.xml)、spring的配置和maven的pom文件配置。

web中通过springBoot启动web服务,处理web请求。此处的配置处理spring和maven的配置外,核心的就是springBoot的配置。

代码结构示意如下:

-common

    -main

        -java

             -base\dao\bean\service\config\util

        -resource

             -application.yml

             -spring.xml

    -pom.xml

-web

    -main

        -java

            -controller

            -WebBootStrap.java

        -resource

             -application.yml

             -spring.xml

    -docker

        -build.sh/Dockerfile/restart.sh

    -pom.xml

-pom.xml

工程配置:

这里顺便强调一下intellij indea中的工程配置,每一个子工程,也就是modules(有自己独立的pom文件)都要在Project Structure里边配置好其Sources、Resources等,不然application等资源文件不会生效。Facets里则配置了spring需要依赖的文件,如xml配置和controller、bootStrap、或配置文件资源配置读取等,这些都是如果带了@controller、@SpringBootApplication、@Configuration等注解的ide都会自动捞出来,配置一下即可。

Maven+SpringBoot+Java 搭建restful 接口API框架到docker部署及遇到的问题记录_第1张图片

Maven+SpringBoot+Java 搭建restful 接口API框架到docker部署及遇到的问题记录_第2张图片

在下面的讲述过程中会随时插入一些问题及解决办法。

值得注意的是:1. 要学会看日志、debug运行分析 2. 不能不求甚解,对于直接借鉴过来的代码或工程、脚本等,整体运行失败后,要学会化整为零,知道每一步都在做什么,逐步分析。

问题1: 资源配置和DB连接问题

web中引用这个dao类会提示:required a bean of type 'dao.XXX' that could not be found.

web的pom文件中,common已经作为jar包引入了,按说不会找不到。

进一步debug,发现是common模块db连不上,dao类中引用的jdbc操作类报空指针错误。

@Autowired
private JdbcOperation jdbcOperation;

配置步骤:

1. resources里边的applicationd.yml文件代替了原有的properties文件,配置用到存储资源,可读性更强。

datasource:
  redis:
    ip: 
    port: 
    timeout: 
    password: 
  mysql:
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://***?characterEncoding=utf8&serverTimezone=GMT%2B8
    username: 
    password: 

2. 有一个CommonConfig类带有@Configuration注解,把资源yml文件读成value

@Value("${datasource.mysql.driverClassName}")
private String driverClassName;

@Value("${datasource.mysql.url}")
private String url;

@Value("${datasource.mysql.username}")
private String username;

@Value("${datasource.mysql.password}")
private String password;

@Bean("terminator-mysql")
public JdbcConfig getJdbcConfig() {
    JdbcConfig config = new JdbcConfig(driverClassName, url, username, password);
    log.warn("[CONFIG] terminator using jdbc config {}", config.toString());
    return config;
}

3. jdbc操作类中把配置读取进来

@Autowired
@Qualifier("terminator-mysql")
private JdbcConfig jdbcConfig;

这样完整的DB配置才完成,空指针的问题也就解决了。

SpringBoot的配置:

代码如下。这里边就要理解逐个注解的意思了。这个不在此详述,百度吧。
配置完成后可以通过 http://127.0.0.1:port/ 测试,返回hello spring boot!则表示配置成功。

@RestController
@SpringBootApplication
@EnableAutoConfiguration
@ImportResource({ "classpath:spring.xml" })
@RequestMapping("/")
public class PayRiskWebBootStrap {

    @Value("${http.port}")
    private Integer port;

    @Bean
    public EmbeddedServletContainerFactory servletContainerFactory() {
        TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
        tomcat.addAdditionalTomcatConnectors(createStandardConnector()); 
        return tomcat;
    }

    private Connector createStandardConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setPort(port);
        return connector;
    }

    public static void main(String[] args) {SpringApplication.run(PayRiskWebBootStrap.class);

    }

    @RequestMapping
    public String hello(){
        return "hello spring boot!";
    }
}

需要注意两个问题:

1. web服务请求的端口号port设置

代码中是通过value从yml配置文件中读取的端口号,所以如果resource里的yml配置文件没生效或者配置不对,springBoot都会启动失败。

server:
  port: 28481

http:
  port: 28480

logging:
  config: classpath:log4j2-dev.xml

2. 注意springBoot端口号与dockerfile中的Expose端口号一致

springBoot中用的是http.port,那么在后面的docker部署中启动的端口号就要与这个保持一致,也用这个http.port值,例如28480.

问题2: web模块的controller类(非springBoot类)url访问404,且类名或方法名IDE提示not used。

原因:

之前SpringBoot类里边还加了一个@ComponentScan的注解,这个注解的作用是告诉spring,要用到哪些jar包下面的类。

其实默认跟springBoot文件同级别及子目录的文件都会引入(我的controller类就属于这个情况),而这个地方配置不对,反倒帮了倒忙。

这个注解正确的用法我暂时没深入理解,请额外查询吧。

@ComponentScan(value = "XXX", excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = {
        "XXX.*", "XXX.*" }))

docker部署:

IDE里边运行成功了,那么就着手docker部署。

已经有打好的镜像,因此准备三个文件dockerfile、启动脚本restart.sh、编译脚本build.sh。

1. dockerfile

FROM 镜像文件获取地址
MAINTAINER 账号

RUN mkdir -p jar包docker中的工作目录
ADD XXX.jar jar包docker中的工作目录/XXX.jar
ADD restart.sh jar包docker中的工作目录/restart.sh

RUN chmod +x jar包docker中的工作目录/restart.sh

EXPOSE 28480  #此处要跟springboot的工作端口一致!
WORKDIR jar包docker中的工作目录

#ENTRYPOINT jar包docker中的工作目录/restart.sh
CMD  ["jar包docker中的工作目录/restart.sh"]

2. build.sh

#!/usr/bin/env bash
function copy_jar() {
    cp ../target/XXX-*.jar XXX.jar
} 
function build_image() {
    image_name="$1"
    docker build -t $image_name .
    docker push $image_name
    docker rmi $image_name
    echo "build image $image_name done."
}
function main() {
    copy_jar 
    build_image "$1"
    echo 'build process done.'
}

main "$@"

3. restart.sh

#!/bin/bash
JAR=XXX.jar
JVM='-Xms2g -Xmx4g -Djava.net.preferIPv4Stack=true  -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gclog -Duser.timezone=GMT+08'
PID=`ps aux | grep ${JAR} | grep -v grep | awk '{print $2}'`
echo $PID
if [[ -n $PID ]] ;then
  kill -9 $PID && java -jar ${JVM} ${JAR} > runtime.log 2>&1
else 
   java -jar ${JVM} ${JAR} > runtime.log 2>&1
fi 

文件准备好之后就可以按照一下的步骤部署了:

cd 代码目录
git pull  #更新代码
mvn clean package #打好jar包
cd dockerfile等三个文件所在的目录
sh build.sh   #执行完后检查下目录下是否多了一个打好的jar包
docker build -t XXX #其中XXX是jar包名字 .
docker run -d -p 28480:28480 --net=host -v /etc/localtime:/etc/localtime -v /data1/logs/XXX:/data/weibo/service/logs  -e "LANG=en_US.utf8" XXX  # 这一步就是要注意端口号跟springboot用的,dockerfile里边expose的要一致!
docker ps  -a //查看镜像名字

提示:以上这些步骤都可以脚本化。

问题3: docker容器生成后秒退出,启动后查看状态就已经是XX second Exit(1)了

排查这个问题的时候我一直在跟部署脚本较劲,docker logs 容器id  也没有任何日志信息,无从下手的感觉。殊不知是jar包本身的问题。

当时搜索到一个外网论坛,其中一个回答就是Exit(1)就表示docker里边的主进程异常退出。因此我就单独执行我打出来的jar包。

怎么执行?此处就是化整为零了,要知道部署脚本每一步都是干嘛的就不会有这个疑问了。

restart.sh脚本的作用就是启动docker容器中的java进程,所以直接用里边的java -jar ${JVM} ${JAR} > runtime.log 2>&1命令就好了(jvm和jar变量值上面脚本里有,自己替换),然后还有日志输出到runtime.log。

日志中写着:no main manifest attribute in *.jar

原因就是上文提到的pom文件中build没有配置springboot啦。

排查这个问题的过程中还走了不少弯路,在这里排两个雷:

1. “docker run命令加上 -idt 就好了”

这是百度到的最多的结果。

docker容器当没有任何进程在执行的时候,就会默认退出;例如你的docker里边执行的是一个脚本,这个脚本没有放到后台执行,调一次之后就退出了,那么容器就会默认退出。而docker run加上 -idt 的作用就是放到后台运行,还能支持控制台交互啥的,详细请参考:https://www.cnblogs.com/yfalcon/p/9044246.html

但是我的问题恰好不属于这一种。

2. “docker run 命令最后加上 /bin/bash”

容器果真是起来了,没有exit,但是只是掩耳盗铃。问题并没有根本性解决,如果主进程jar包有问题,那服务该不对还是不对。

这个的思路其实跟1是一样的,都是为了避免容器退出,而启动一个bash在那顶着,给docker一种有进程在工作的假象。

 

以上就是我这次Maven+SpringBoot+Java的Restful服务到docker部署的全体验了。不是一步步的详细教程,但希望其中踩过的坑及排雷办法能帮到大家。

 

 

 

 

 


 

你可能感兴趣的:(java,docker,springBoot,Maven)