1、工程结构描述
源工程目录结构:
打包之后的标准部署工程结构:
bin: 包含start.sh、stop.sh脚本sbin:一些供用户打包的bat脚本和shell脚本config:包含工程配置文件xxx.properties(包括数据库连接信息等)、logback.xml等java-project.jar:工程打包之后的jar,也可以放在lib文件夹下,只要在start.sh中能将其拼接CLASS_PATH中即可lib:工程依赖的第三方jar,比如commons-lang3-3.2.1.jar、mysql-connector-java-5.1.29.jar等version:里面一本是一个txt文件,用于描述每个版本修改了什么webapp:web的HTML、css、js、web.xml等文件需要在JettyServiceStarter指定路径
小结:任何能够从main方法启动的工程,比如java、spring-java、springboot、spring-webmvc-jetty,都能够打包成这样的部署结构。因为是通过java --classpath $CLASS_PATH MAIN_CLASS 来启动的,只要在start.sh中实现CLASS_PATH拼接上lib文件夹下的jar即可解决jar依赖问题
2、如何打包成上面的结构?
如何将自己的工程打包成上面的结构?
使用如下三个插件将工程打包成上面的结构
maven-compiler-plugin ==> 指定jdk版本maven-jar-plugin ==> 自定义自己工程,剔除一些配置信息maven-assembly-plugin ==> 将自己工程的jar和其他依赖的jar组装起来 打包成bin、 config、 lib、 version的标准格式
-
1、pom.xml 的build部分
${project.artifactId}-${project.version} org.apache.maven.plugins maven-compiler-plugin 3.2 org.apache.maven.plugins maven-jar-plugin db/db.properties quartz/job.properties caiwutong.properties logback.xml notice.txt org.apache.maven.plugins maven-assembly-plugin 2.6 ${project.finalName} assembly/assembly.xml false package single -
2、assembly.xml
assembly zip true bin bin 0755 sbin sbin 0755 src/main/resources config db/*.xml db/*.sql mybatis/ quartz/*.xml spring/ notice.txt src/main/resources version notice.txt src/main/webapp webapp . :*${project.artifactId}*: lib :*${project.artifactId}*:
3、启动和停止脚本
-
1、启动脚本start.sh
主要是:找deploydir、jps判断程序是否已经启动、拼接lib下jar和config到CLASS_PATH
#!/bin/bash # 获取关键路径:BIN_DIR 和 DEPLOY_DIR cd `dirname $0` BIN_DIR=`pwd` cd .. DEPLOY_DIR=`pwd` # 创建logs文件夹 mkdir -p $DEPLOY_DIR/logs STDOUT_FILE=$DEPLOY_DIR/logs/stdout.log # main类,程序入口 并且 从conf.properties中获取程序名和端口 MAIN_CLASS=com.yuanmei.caiwutong.JettyServiceStarter APPLICATION_NAME="JettyServiceStarter" APPLICATION_PORT=`cat config/caiwutong.properties | grep 'application.port=' | cut -d '=' -f2- | tr -d '\r'` # 先判断此应用程序是否已经启动了以及端口是否被占用 PIDS=`jps | grep $APPLICATION_NAME | awk '{print $1}'` if [ -n "$PIDS" ]; then echo "ERROR: The $APPLICATION_NAME already started!" echo "PID: $PIDS" exit 1 fi if [ -n "$APPLICATION_PORT" ]; then SERVER_PORT_COUNT=`netstat -tln | grep $APPLICATION_PORT | wc -l` if [ $SERVER_PORT_COUNT -gt 0 ]; then echo "ERROR: The $APPLICATION_NAME port $APPLICATION_PORT already used!" exit 1 fi fi # 将config文件夹和lib下jar拼接到--classpath中。 # config 放在前面,是因为如下两种方式都是顺序查找--classpath,找到就返回 # classpath:/db/db.properties # xxx.class.getResourceAsStream("/db/db.properties") # 将config拼接到--classpath中,那么logger使用class.getResource("logback.xml")能够找到 # tr 表示将前面的那个值替换为后面的那个值 CLASS_PATH=$DEPLOY_DIR/config CLASS_PATH=$CLASS_PATH:`ls $DEPLOY_DIR/lib/*.jar | tr "\n" ":"` CLASS_PATH=$CLASS_PATH`ls $DEPLOY_DIR/*.jar` # jvm参数 JAVA_OPTS=" -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true " # 开启debug JAVA_DEBUG_OPTS="" if [ "$1" = "debug" ]; then JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=8000,server=y,suspend=n " fi # 开启远程jmx,也就是可以使用jconsole进行连接 JAVA_JMX_OPTS="" if [ "$1" = "jmx" ]; then JAVA_JMX_OPTS=" -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false " fi #内存以及gc的配置 JAVA_MEM_OPTS=" -server -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 " # 启动应用程序 echo -e "Starting the $APPLICATION_NAME ...\c" nohup java $JAVA_OPTS $JAVA_DEBUG_OPTS $JAVA_JMX_OPTS $JAVA_MEM_OPTS -classpath $CLASS_PATH $MAIN_CLASS $APPLICATION_PORT $1> $STDOUT_FILE 2>&1 & # 循环判断是否有对应的pid(也就是是否启动成功) while true; do sleep 1 echo -e ".\c" PIDS=`jps | grep $APPLICATION_NAME | awk '{print $1}'` if [ -n PIDS ]; then break fi done echo "OK!" echo "PID: $PIDS" echo "STDOUT: $STDOUT_FILE"
-
2、stop.sh脚本 ==> 和纯java工程一模一样,都是jps找出pid,然后kill -9
主要是找路径、jps判断程序是否已经启动
#!/bin/bash # 先获取关键位置BIN_DIR和DEPLOY_DIR cd `dirname $0` BIN_DIR=`pwd` cd .. DEPLOY_DIR=`pwd` # 先停止监控脚本 monitorPidArr=($(ps aux | grep $BIN_DIR/monitorRestart.sh | awk '{print $2}')) if [ ${#monitorPidArr[*]} -gt 1 ]; then ps aux | grep monitorRestart.sh | grep -v "S+" | awk '{print $2}' | xargs kill -9 fi # 给定应用名字(和start.sh配置一样) APPLICATION_NAME="JettyServiceStarter" # 通过应用名字来获取pid,最后kill(下面是通用的,不用改了,只要配置上面的APPLICATION_NAME即可) PIDS=`jps | grep $APPLICATION_NAME | awk '{print $1}'` if [ -z "$PIDS" ]; then echo "ERROR: The $APPLICATION_NAME does not started!" exit 1 fi echo -e "Stopping the $APPLICATION_NAME ...\c" while true; do echo -e ".\c" PIDSTEMPS=`jps | grep $APPLICATION_NAME | awk '{print $1}'` if [ -n "$PIDSTEMPS" ]; then for PID in $PIDSTEMPS ; do kill -9 $PID > /dev/null 2>&1 done else break; fi sleep 1 done echo "OK!" echo "PID: $PIDS"
4、如何找到配置文件?
-
4.1、使用xxx.class.getResource("/db/db.properties")
任何时候都可以在代码中使用这种方式来找到配置文件,并且获取流来使用。
它使用了java启动命令中--classpath参数的路径,逐个查找,找到就返回,所以如果想优先查找config文件夹,则需要将config拼接在--classpath最前面。
代码使用例子:
-
4.2、spring工程使用classpath:/db/db.properties
内部还是使用了上面的class.getResource()来加载配置文件。找到就返回,不会继续找,所以同样的如果想要优先使用config的配置文件,需要将config拼接在--classpath的最前面
内部实现源码:
-
4.3、使用user.dir系统参数来获取(不通用)
上面两种情况都是通过--classpath的路径来获取配置文件,所以必须将config文件夹放到--classpath中,但是user.dir不需要。
user.dir的路径指的是用户工作空间,也就是start.sh脚本在启动java命令之前,cd到哪里就是哪里,一般都会先cd到部署目录才启动java命令
cd到部署目录,这样就可以使用如下命令获取路径:
String dbPath = System.getProperty("user.dir") + "/config/db/db.properties";
可以发现,有一个缺陷,需要拼接config,如果不打包,直接在工程里面调试,那么还需要
1、设置user.dir System.setProperty("user.dir", "e:/javaproject/src/main/resources");
2、去除config这段,因为resources下面是没有config这一层的。
|||||||||||||||||||||||||||||||||||||| ||||||||||||||||||||||||||||||||||||||
5、代码编程方面的一些经验
-
1、@Scope("prototype") 实现多例
-
2、@authwrite注解的全局变量不能设置为static,否则注解不进来,为null
-
3、关于profile
spring.profiles.default production -
4、关于classpath: 和 classpath*:
classpath:notice*.txt 加载不到资源 classpath*:notice*.txt 加载到resource根目录下notice.txt classpath:META-INF/notice*.txt 加载到META-INF下的一个资源 (classpath是加载到匹配的第一个资源,就算删除classpath下的notice.txt,他仍然可以加载jar包中的notice.txt) classpath:META-*/notice*.txt 加载不到任何资源 classpath*:META-INF/notice*.txt 加载到classpath以及所有jar包中META-INF目录下以notice开头的txt文件 classpath*:META-*/notice*.txt 只能加载到classpath下 META-INF目录的notice.txt 经验配置:将config文件夹路径拼接到CLASS_PATH中,就可统一使用classpath:/db/db.properties (强烈推荐这样处理)
-
5、关于mybatis
-
5.1、Mapper.xml配置项设置,里面的一定是Mapper.xml这种mybatis认识的格式文件,比如如果配置为<property name="mapperLocations" value="classpath:/mybatis/"/>,刚好mybatis文件夹下有一个logback.xml的文件,它不符合mybatis认识的Mapper.xml格式,就会如下错误
Caused by: org.xml.sax.SAXParseException; lineNumber: 2; columnNumber: 16; 文档根元素 "configuration" 必须匹配 DOCTYPE 根 "null"
所以一定要加上*Mapper.xml确保全部都是mybatis需要的xml文件
-
5.2、entity的配置也需要注意,com.yuanmei.caiwutong.entity中必须包含所有Mapper.xml配置文件中需要用到的类,否则即使你的工程中有对应的java类,还是会报:classnotfound错误。比如某个Mapper.xml中需要一个com.yuanmei.caiwutong.response.UserOV这个类,由于不在entity包中,所以会报错,此时必须将response包放在entity包中
-
5.3、定义mybatis标签模板,方便后面的sql拼接,比如表的字段
account, `name`, passwd, gender, phone, `identity`, `job`, addr, interest, privilege, headPath, motto, createTime id, -
5.4、save和msave时,设置useGeneratedKeys="true" keyProperty="自增字段名"取回自增id设置给当前实例对象。msave需要mybatis3.3.1之后的版本才支持。
INSERT INTO user_info ( ) VALUES (#{account}, #{name}, #{passwd}, #{gender}, #{phone}, #{identity}, #{job}, #{addr}, #{interest}, #{privilege}, #{headPath}, #{motto}, now()) INSERT INTO user_info ( ) VALUES (#{item.account}, #{item.name}, #{item.passwd}, #{item.gender}, #{item.phone}, #{item.identity}, #{item.job},#{item.addr}, #{item.interest}, #{item.privilege}, #{item.headPath}, #{item.motto}, now()) -
5.5、Dao类中的接口(注意返回值类型一定是Long,而不是long,因为有可能没有找到任何的值,为null,无法转化为long)
-
5.6、MySQL date 类型只精确到秒,不精确到毫秒,所以在做时间等于查询的时候,不能够使用new Date()对象去比较
-
5.7、如果值传递一个参数,可以使用任何类型:int string long 等,而且不需要名字对应 List<TControlWarning> findValidConByType(String varl);
其他单参数也是这样
-
5.8、传递多个参数(也可以用map)List<TControlWarning> findBySameParam(@Param("name") String var1, @Param("age") int var2);
-
-
6、代码中读取配置文件properties中的参数
-
6.1、xml加载properties配置文件
-
6.2、代码中使用@Value来获取
@Value("${stat.amount.total.cron.one:0/3 * * * * ?}") private String cronExps;
-
-
7、quartz定时调度方法的写法
-
7.1、xml配置文件中扫描调度方法
-
7.2、代码调度
@Scheduled(cron = "${stat.amount.total.cron.one}") public void amountJobTest1() { LOG.info("第一个schedule,appName : {}, basePath : {}", appName, appPort); }
-
-
8、关于读写分离实现(读写分离中间件,比如“mysql-proxy和Amoeba for MySQL”或者自己在业务层写代码,如下)
-
8.1、首先数据库一定要先配置主从结构,具体的请自行百度
-
8.2、application-db.xml配置多个数据源、并且使用spring提供的AbstractRoutingDataSource来组织,这个类需要实现
1:)实现类DynamicDataSource.java,DataSourceAspect.getDataSource()是获取8.3中保存在ThreadLocal的值(master或者slave)
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceAspect.getDataSource(); } }
2:)application-db.xml(),使用上面的实现类DynamicDataSource来组织所有数据源
-
8.3、现在的问题就是如何才能够让DynamicDataSource知道到底用那个数据源:1、设置aop(内部原理是动态代理和反射)这样就能够知道当前执行的service的那个方法;2、需要在每一个service层方法的上面加上自定义的一个注解@DataSource(value = "master"),因为如果这个方法上面没有任何东西,即使动态代理得到这个方法信息,还是不知道这个方法到底是用master还是slave;3、最后定义一个类来处理动态代理得到的方法,获取它上面的DataSource注解,得到值,保存在ThreadLocal中给DynamicDataSource使用
1:)先定义一个注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value() default "master"; }
2:)在service层的方法上加上DataSource注解,一般save用master,find用slave
@DataSource(value = "master") public CommonResult save(UserInfo userInfo, UserInfo loginUser) { }
3:)设置aop,动态代理所有service层的方法,同样在application-db.xml配置文件中
3:)从上面的配置可以发现aop得到的代理方法信息会给DataSourceAspect处理,这里是关键。1、反射方法上的DataSource值,保存在ThreadLocal中供DynamicDataSource使用,具体如下代码:
package com.yuanmei.caiwutong.datasource; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.concurrent.atomic.AtomicInteger; /** * 面向切面的类 * 通过before方法,解析本次service方法上的DataSource注解,得出是master还是slave,同时保存到ThreadLocal中,供DynamicDataSource获取 * 通过ThreadLocal获取当前线程需要处理DataSource的类型(master或者是slave) * * @Author liufu * @CreateTime 2018/3/13 10:31 */ public class DataSourceAspect { private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceAspect.class); private AtomicInteger masterIndex = new AtomicInteger(0); private AtomicInteger slaveIndex = new AtomicInteger(0); private String[] masterArr; private String[] slaveArr; private int masterLength; private int slaveLength; private static final ThreadLocal
THREAD_LOCAL = new ThreadLocal (); public DataSourceAspect() { } public DataSourceAspect(String masterPool, String slavePool) { if (StringUtils.isNotBlank(slavePool)) { String splitStr = ","; if (slavePool.contains("|")) { splitStr = "\\|"; } else if (slavePool.contains(";")) { splitStr = ";"; } else if (slavePool.contains("&&")) { splitStr = "&&"; } masterArr = masterPool.split(splitStr); slaveArr = slavePool.split(splitStr); for (int i = 0; i < masterArr.length; i++) { masterArr[i] = masterArr[i].trim(); } for (int i = 0; i < slaveArr.length; i++) { slaveArr[i] = slaveArr[i].trim(); } masterLength = masterArr.length; slaveLength = slaveArr.length; } } /** * 绑定当前线程数据源 * * @param datasource */ public static void putDataSource(String datasource) { THREAD_LOCAL.set(datasource); } /** * 获取当前线程的数据源 * * @return */ public static String getDataSource() { return THREAD_LOCAL.get(); } /** * service方法在调用dao层方法前,解析方法上的DataSource注解得到master还是master * 分两种情况,service层有接口,和没有接口 */ public void methodBefore(JoinPoint point) { // 获取方法信息 MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); String methodName = method.getName(); Class [] parameterTypes = method.getParameterTypes(); Parameter[] parameters = method.getParameters(); // 获取方法上的注解信息 Annotation[] annotations = method.getAnnotations(); DataSource annotation = method.getAnnotation(DataSource.class); boolean flag = method.isAnnotationPresent(DataSource.class); //判断方法所在类到底是接口还是业务实现类 Class targetClass = point.getTarget().getClass(); if (targetClass.isInterface()) { //接口改怎么做 } else { //业务实现类该怎么做 } if (annotation != null) { String value = annotation.value(); String dataSource = null; if ("master".equalsIgnoreCase(value)) { int index = masterIndex.getAndIncrement(); if (index > 999) { index = 0; masterIndex.set(0); } dataSource = masterArr[index % masterLength]; } else { int index = slaveIndex.getAndIncrement(); if (index > 999) { index = 0; slaveIndex.set(0); } dataSource = slaveArr[index % slaveLength]; } LOGGER.info("用户选择数据库库类型:" + dataSource); DataSourceAspect.putDataSource(dataSource); // 数据源放到当前线程中 } } /** * 在方法调用完成后,执行此方法进行通知打印 */ public void methodAfter(JoinPoint point) { //获取方法,然后获取方法上的DataSource注解,得到master或者是slave Method method = ((MethodSignature) point.getSignature()).getMethod(); LOGGER.info("service 层方法:{},执行完毕, 调用了:{} 数据库", method.getName(), DataSourceAspect.getDataSource()); } }
-
-
9、swagger前后端联调利器
通过swagger可以在页面上直接浏览到后端的所有接口,能够直接在上面输入参数去测试后端的接口情况。
-
9.1、maven依赖
io.springfox springfox-swagger2 ${springfox.version} io.springfox springfox-swagger-ui ${springfox.version} -
9.2、配置swaggerConfig,来启动swagger扫描web包,从而生成json描述信息供swagger-ui来访问获取
package com.yuanmei.caiwutong.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.annotations.ApiIgnore; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * swagger扫描配置,只要自动扫描到就行 * * @Author liufu * @CreateTime 2018/1/29 21:04 */ @Configuration // 配置注解,自动在本类上下文加载一些环境变量信息 @EnableSwagger2 // 开启swagger2 public class SwaggerConfig { /** * 正是因为加了@Configuration,才能够拿到application.name这个配置值 */ @Value("${application.name}") private String applicationName; @Bean public Docket productApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.yuanmei.caiwutong.web")) .paths(PathSelectors.any()) .build() .ignoredParameterTypes(ApiIgnore.class) .enableUrlTemplating(false); } private ApiInfo apiInfo() { System.out.println(applicationName); return new ApiInfoBuilder() .title("源美财务通项目") .description("模块: spring-mvcweb-project, 平台页面 Restful 接口说明.") .version("4.0") .build(); } }
-
9.3、需要在application-webmvc.xml中配置信息扫描它,同时由于swagger-ui.html页面是springfox-swagger-ui这个jar提供的,所以还需要配置静态资源映射,否则访问不到
-
9.4、最后需要在web.xml中配置servlet拦截器拦截所有的路径/,尝试过拦截swagger-ui.html 和 /webjars不成功,最后设置了/才能成功,如果有解决此问题的兄弟麻烦下面留言,谢谢!
servelet /
-
-
10、原本的war工程基础上套jetty容器
-
10.1、引入jetty依赖
org.eclipse.jetty.aggregate jetty-all-server 8.2.0.v20160908 -
10.2、pom.xml的build部分使用上面的打包方式,将工程打成bin、sbin、config、lib、version、webapp的格式
-
10.3、JettyServiceStarter启动类配置webapp路径即可
package com.yuanmei.caiwutong; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; /** * 通过启动嵌入式的jetty,然后由jetty解析web.xml配置文件 * 由于web.xml配置了spring和springMVC,所以最终能够把整个springmvc工程启动起来 * * @Author liufu * @CreateTime 2018/1/25 11:32 */ public class JettyServiceStarter { public static void main(String[] args) throws Exception { //在idea启动测试时,设置user.dir。在Linux上就不需要了,因为start.sh脚本启动时user.dir会设置正确 // System.setProperty("user.dir", "E:\\workplace\\chinaOpenGit\\spring-mvcweb-jetty-separation\\src\\main"); int port = 8080; if (args.length > 0){ port = Integer.parseInt(args[0]); } // 创建服务器,并设置监听端口 Server server = new Server(port); // 关联一个已经存在的上下文 WebAppContext context = new WebAppContext(); // 设置上下文路径 context.setContextPath("/caiwutong"); // 设置Web内容上下文路径(所以assembly.xml需要将webapp文件夹打包出来) context.setResourceBase(System.getProperty("user.dir") + "/webapp"); /** * 设置描述符位置(通过web.xml,jetty就可以启动spring和springmvc) * * ===================注意:======================================= * 默认这个参数是可以不配置的,只要给定上面的webapp,那么他就知道webapp下面有一个WEB-INF/web.xml * 但是如果把web.xml 改为了如下的webtest.xml,程序虽然能启动,但是由于找不到web.xml,所以没法构建spring和springmvc * 这个时候就需要指定这个描述文件是在哪里了 */ // context.setDescriptor(System.getProperty("user.dir") + "/webapp/WEB-INF/webtest.xml"); context.setParentLoaderPriority(true); server.setHandler(context); try { server.start(); } catch (Exception e) { e.printStackTrace(); } System.out.println("server is start"); } }
-