背景:
三年前用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都会自动捞出来,配置一下即可。
在下面的讲述过程中会随时插入一些问题及解决办法。
值得注意的是: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部署的全体验了。不是一步步的详细教程,但希望其中踩过的坑及排雷办法能帮到大家。