Maven 模块化开发

按功能模块? 如果按功能分模块的话,比较复杂,难免会有代码循环依赖的问题。 按分层模块? 这个相对比较简单点。

然后还要区分不同的环境下的包(web与非web环境,为以后的task等非web代码部署时,不需要再依赖tomcat等容器)

日志的统一

slf4j,不过不能配置将标准输出,重定向到Tomcat的catalina.out里。 即不能这样子配置:

<!-- 控制台输出 -->
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>%C{50}.%M %L [%date{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] : %m%ex%n</Pattern>
		</layout>
	</appender>

而应该将应用的所有输出,重定向到一个文件(然后根据该文件增长的快慢,来设置按天,小时,或分或切割文件),例如按天:

<appender name="STDOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
		<file>${LOG_HOME}/console/console.log</file>
		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
			<!-- daily rollover. Make sure the path matches the one in the file element or else
             the rollover logs are placed in the working directory. -->
			<fileNamePattern>${LOG_HOME}/console/console_%d{yyyy-MM-dd}.%i.log</fileNamePattern>

			<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
				<maxFileSize>100MB</maxFileSize>
			</timeBasedFileNamingAndTriggeringPolicy>
		</rollingPolicy>
		<layout class="ch.qos.logback.classic.PatternLayout">
			<Pattern>%-10(%C{40}.%M %L) [%date{yyyy-MM-dd HH:mm:ss}] [%level]: %m%ex%n</Pattern>
		</layout>
	</appender>

然后每种业务,添加一个统一的logger,再分别存在到不同的日志文件里,方便排查问题.

框架的选择

SpringBoot + Redis + RabbitMQ + MySQL

然后部署时,配置多种profile,并且要简化xml冗余的问题。(通过import来避免重复的配置文件问题)

注意事项:

redis:尽可能用pipeline,以及选择最优的数据类型来存取数据.不能使用keys命令,可参考Github rabbitmq:添加用户和vhost来隔离(利用好交换机的类型,处理相应的业务,比如扇形交换机,可以模似做发布-订阅功能等,listener要不要ACK以提升性能等,Executor的配置计算好) MySQL:使用最新稳定版,做好备份和复制。(not null default, 每张表都要有create_time, update_time这些与业务无关的,但又很重要的信息, id auto_increment非业务主键) nginx:负载均衡(其他的负载均衡有 haproxy)

代码注意

  1. 将一些模块,写成服务。比如发邮件,写个统一的Service,由其他系统来调用(强调的是自治,以及自运行,这时可以有一个独立的redis+mq来为它服务)。

  2. 不同系统间的调用(比如请求多个HTTP,善于利用异步Future的方式来调用)

  3. 同一个方法里,避免多次查询DB,HTPP,Redis等调用(利用本地缓存来暂存)

  4. 千万不写如下类似代码!!!(即删除时,尽可能使用 Iterator来做)

    List xxxx
    
    for(int i=0 ;i<10; i++){
    xxxx
    list.remove(i);
    }
    
  5. HTTP请求时的超时设置(一般的类库,默认是无限)

  6. 因为spring里的对象,几乎都是单例的。所以,在操作时要特别注意,不要随便set这些单例的状态(如rabbitTemplate.setxxx,或者类似这种代码!!!!!)

  7. 统一 checkstyle: 这里以Google的 Java Style. Github

关于自动化部署

脚本rsync + nginx做负载均衡(部署时自动切换下)

开发流程

git flow

Java编码规范

关于代码质量监控

Sonar + Findbugs 结合,每周一次自我检测代码质量.不断完善.

关于Tomcat

如果要进行远程调试,完成后,千万不要关闭远程调试再重启Tomcat!! 不然,JVM极有可能会因为这个原因,而莫名停掉!

Tomcat启动前添加参数

tomcat目录下添加bin/setenv.sh,内容如下:

#!/bin/sh

JAVA_HOME=/home/you/jdk/jdk1.7.0_79
JRE_HOME=/home/you/jdk/jdk1.7.0_79/jre

#CATALINA_OPTS="-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -XX:+PrintGCDetails -Xloggc:weibosdk-gc.log -XX:+DisableExplicitGC -Xdebug -Xrunjdwp:transport=dt_socket,address=9999,server=y,suspend=n"
NOW="$(date +'%Y_%m_%d_%H_%M_%S')"

JVM_TYPE=" -server"

JVM_GC=" -XX:+PrintGCDateStamps -verbose:gc -XX:+PrintGCDetails -Xloggc:weibosdk-gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=10M"
JVM_DUMP=" -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/you/tomcat/product_wbsdk_jvm-dump.hprof"

#JVM_GC_TYPE=" -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=90  -XX:+ScavengeBeforeFullGC -XX:+CMSScavengeBeforeRemark"

JVM_GC_TYPE=" "

# 单位:秒
JVM_DNS_CACHE=" -Dsun.net.inetaddr.ttl=60"

# hostname如果为远程,则是外网的IP
JVM_RMI=" "

# survivorratio = eden / from 的比值

#JVM_MEM=" -Xms2048m -Xmx2048m  -XX:PermSize=512m -XX:MaxPermSize=512m -XX:SurvivorRatio=8"
JVM_MEM=" "


#JVM_DEBUG=" -Xdebug -Xrunjdwp:transport=dt_socket,address=9999,server=y,suspend=n"

JVM_DEBUG=" "

JMX="-XX:+UnlockCommercialFeatures -XX:+FlightRecorder -Dcom.sun.management.jmxremote.port=7002 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=IP"

CATALINA_OPTS=" -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager ${JVM_TYPE} ${JVM_GC_TYPE} ${JVM_GC} ${JVM_DUMP} ${JVM_DNS_CACHE} ${JVM_RMI} ${JVM_MEM} ${JVM_DEBUG} ${JMX}"

echo "java opts = "  ${CATALINA_OPTS}

ulimit -c unlimited
tomcat_dir/bin/startup.sh

Tomcat控制脚本

#!/bin/bash

###############################################################
#
#author: Zhiyong Yang
#date: 2015-12-4
#
###############################################################

#set -x 

#作用 启动,停止,重启某tomcat,注意 ,tomcat使用绝对路径,并以/结束.
#./tomcat_ct.sh /path/to/tomcat/ start|stop|restart

TOMCAT_HOME_DIR=""

if [ ! -z $1 ]; then
	TOMCAT_HOME_DIR=$1
fi

if [ -z ${TOMCAT_HOME_DIR} ]; then
	echo "no tomcat special..., so exit" && exit 1
fi

TOMCAT_HOME_DIR_GREP="${TOMCAT_HOME_DIR/apache/[a]pache}"

echo "-->$TOMCAT_HOME_DIR_GREP"

function stop(){
	if checkIsExist ; then
		"${TOMCAT_HOME_DIR}"bin/shutdown.sh
		sleep 1
		while checkIsExist ; do
			sleep 2
			kill $(getPID)
		done
	fi
}

function getPID(){
	# replace the grep content with your tomcat path, start with []
	#$PID=`ps aux | grep ${TOMCAT_HOME_DIR_GREP}`
	local PID=`ps aux | grep ${TOMCAT_HOME_DIR_GREP} | grep "[o]rg.apache.catalina.startup.Bootstrap" | awk '{print $2}'`
	echo $PID
}

function start(){
	if ! checkIsExist ; then
		"${TOMCAT_HOME_DIR}"bin/startup.sh
		tail -f -n 100 "${TOMCAT_HOME_DIR}"logs/catalina.out
	fi
}


function checkIsExist(){
	local P_ID=$(getPID)
	if [ -z ${P_ID} ]; then
		echo "not found running tomcat : ${TOMCAT_HOME_DIR}"
		return 1
	else
		echo "found running tomcat pid = ${P_ID}"
		return 0
	fi
}

function usage(){
	echo "Usage:"
	echo "$0 $1 start : for start tomcat, no repeat"
	echo "$0 $1 stop : for stop tomcat"
	echo "$0 $1 restart : for restart"
}

if [ -z $2 ];then
	usage $1
	exit 0
fi

if [ $2 = "stop" ]; then
	stop
elif [ $2 = "start" ]; then
	start
elif [ $2 = "restart" ]; then
	stop
	start
else
	usage $1
fi

错误时发送警报(这些方面要尽可能完善, 及时发现问题, 然后修复问题.)

在所有出现异常的地方, 都添加上通知 RabbitMQ + Email 的方式来发送警报.

SpringMVC加上如下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class ControllerExceptionAdvice {

    @Autowired
    private RabbitMQService rabbitMQService;

    @ExceptionHandler(value = {Exception.class, RuntimeException.class})
    @ResponseBody
    public Map<String, Object> defaultErrorHandler(HttpServletRequest request, Exception e) {

        String requestUrl = request.getRequestURI();
        Map<String, String[]> params = request.getParameterMap();
        String stackMsg = getStack(e);

        rabbitMQService.sendEmail("yourmail", "subject", getFormatText(requestUrl, params, stackMsg));

        Map<String, Object> json = new HashMap<>();
        json.put(Constants.ERROR_CODE, 500);
        json.put(Constants.ERROR_MSG, "server interval error.");
        return json;
    }

    private String getFormatText(String requestUrl, Map<String, String[]> params, String stackMsg) {
        return String.format("请求的URL为 %s\n请求的参数为%s\n异常信息为:%s\n", requestUrl, params, stackMsg);
    }

    private String getStack(Exception e) {
        StackTraceElement[] stackTraceElements = e.getStackTrace();
        StringBuilder sb = new StringBuilder(e.getMessage());
        sb.append("\n");
        for (StackTraceElement stackTraceElement : stackTraceElements) {
            String className = stackTraceElement.getClassName();
            String methodName = stackTraceElement.getMethodName();
            int lineNum = stackTraceElement.getLineNumber();
            String fileName = stackTraceElement.getFileName();
            sb.append(className).append(".").append(methodName).append(" --> ").append(fileName).append(":").append(lineNum);
            sb.append("\n");
        }
        return sb.toString();
    }
}