前言(太长可以不看):
最近发现业务量上去很大而数据量上去不大.既每次编译很耗时但是直接上微服务又没有必要.毕竟我又不是拿着锤子看啥都是钉子.搞微服务是很傻的事情.
所以就开始考虑怎么拆项目.毕竟以后即使做微服务也一开始也是拆项目对不.而且项目之间管理不好其他一切也是扯淡.毕竟没有技术银蛋.这种东西不做好.到时候就是微服务的引用地狱
所以看了很多架构和方案.设计具体技术方案的没有特别深入.但是为了保证以后无缝升级.还是想了一些东西.这种技术实现最后再讲.当然对前面的分层没有太大想法直接可以看技术的小trick.毕竟这些trick还是挺有意思的
最后本人是kotlin习惯了.不太懂怎么写java.抱歉.不过相信大家都能看懂其中的逻辑
拆项目最关键的是项目之间的关系
项目之间最麻烦的就是依赖.依赖假如是乱的.基本完蛋.所以最关键的就是要把业务边界拆出来.做到高内聚低耦合.实际上业务这么乱.拆都很麻烦.
所以我定了几个规则
1.每一个模块分配Level.
2.只有Level高的(数字低的)可以调用Level低的(数值高的)
3.同级之间不允许互相掉
4.同级之间互相的信息传递走消息队列或者总线模式.
按这个思路我大概拆项目.发现了几个特点.
1. 服务拆分比较容易.但是数据结构的引用比较混乱.所以不得不逼迫我同一层要分配两个.一个是基础的数据项目.另一个才是接口调用项目
2. 同样假如拆分项目.数据结构部分很难通用.所以一些通用工具类方法无法直接用.所以不得不采用 HashMap/Json这种数据结构.同时重新构建通用的数据传递方法
我们业务怎么拆分的可以去看下附件.这里就不具体说了.针对这几个原则.我实现了一下我们的一些小的工具类和接口(gradle和spring).希望能帮到各位.
项目之间分层的小技巧
假如认同我说的这些规则.那么强迫开发者遵守这一套规则就很重要.否则基本上大家能偷懒就偷懒.
首先定义一个module.yml放到项目中去(不直接放到gradle是因为这个文件在Spring运行的时候也会用到).格式如下(具体你自己改).
地址我定义放在每一个项目的:src/main/resources/config/module.yml
module.yml:
module:
name: qiuqiu-content
level: 2
version: 1.0
subProjects:
- qiuqiu-content-base
gradle读取起配置并进行检查和把数据放到Project.ext里面
configure(subprojects){
println("submodule config file check:"+it.name)
if(!new File(it.getProject().getProjectDir().toString()).exists())
{
//创建yml文件...
return
}
//是否存在yml文件
def ymlFile=new File("${it.getProject().getProjectDir()}/src/main/resources/config/module.yml")
if(!ymlFile.exists())
{
throw new GradleException("请创建子项目的配置文件module.yml.格式请自己参考已有项目,当前错误项目名字:"+it.name)
}
//读取yml文件
def moduleYaml = new org.yaml.snakeyaml.Yaml().load(ymlFile.newInputStream())
//检查module.yml的名字和gradle的名字是否相同
if(moduleYaml.module.name!=it.name)
{
throw new GradleException("module名字跟yaml配置要相同.gradle项目名字:"+it.name)
}
//检查module.yml的版本和gradle的版本是否一致
if(it.getProject().version.toString()!=moduleYaml.module.version.toString())
{
throw new GradleException("项目版本号没有配置.项目是${it.name}," +
"gradleVersion:${it.getProject().version}," +
"moduleVersion:${moduleYaml.module.version}," +
"")
}
//Project.ext.moduleLevel把level层级是否相同
it.getProject().ext.moduleLevel=moduleYaml.module.level
//检查module.yml的子项目注释和真子项目是否相同
def gradleSubProjects=it.getProject().subprojects.collect{ sub->sub.name.trim() }
def ymlSubProjects=moduleYaml.module.subProjects.collect{ name->name.toString().trim() }
if(ymlSubProjects==null&&gradleSubProjects.size==0)
{
}
else {
def commons = ymlSubProjects.intersect(gradleSubProjects)
if(commons.size!=ymlSubProjects.size||commons.size!=gradleSubProjects.size)
{
def difference=gradleSubProjects.plus(ymlSubProjects)
difference.removeAll(commons)
throw new GradleException("子项目${it.name}依赖没有配置+冲突子项目配置为${difference},请求相应的config文件修改配置")
}
}
}
等configure跑完后.整体做依赖的检查.既确保高级不能调用低级.具体实现如下
task dependenciesCheck(){
//一定要加doLast.保证是最后执行
doLast {
otherSubjects.forEach {
if (!new File(it.getProject().getProjectDir().toString()).exists()) {
println("${it.getProject().name}项目还在创建中,过滤检查")
return;
}
//上面设置的level
def projectLevel = it.getProject().ext.moduleLevel;
//只有compile才能获得相应的依赖.
def projectDepends = it.getProject().getConfigurations().matching {
conf->conf.name.endsWith("compile")
}.collect {
conf ->
conf.getDependencies().matching { dep ->
dep.getGroup()!=null&&
dep.getName() != it.getProject().name
}
}.flatten()
//这里的实现逻辑有问题.下阶段会修改.就是重新通过特殊方法获得此项目的Project
def depProjects=projectDepends.collect {
dep->project(dep.group.replace("qiuqiuhuiben.",":").replace(".",":")+":"+dep.name)
}
//这里检查level是否是低级还是高级
depProjects.forEach{
depproject->
def depLevel=depproject.ext.moduleLevel?:0
if(depLevel==0||depLevel==null){}
else if(depLevel<=projectLevel)
{
throw new GradleException(
"项目依赖请检查.请Level层次低的项目不要同级和调用层级高的项目," +
"项目名字:${it.getProject().name},"+
"当前Level:${projectLevel}," +
"子项目为:${depproject.name},"+
"子项目成Level为:${depLevel}")
}
else{}
}
}
}
}
这样就可以确定项目不会出现重复依赖.
拆分模块之间的小技巧
假如想单独启动每一个gradle模块.而并不想每一个项目都配置一个启动类还有相应的配置文件.
1.首先在基础类中生成一个启动类(比如我的就是ModuleDevStarter)然后全体依赖
2. 在项目上层创建文件夹.丢进去配置文件.比如我创建了Project/子项目合集/config然后丢入了通用的config的配置文件
1.配置每一个文件生成jar的gradle脚本(实际少一个部分.打包的jar启动不起来.还在修改)
configure(subprojects)
{
apply plugin: 'org.springframework.boot'
apply plugin: 'kotlin'
apply plugin: 'application'
dependencies {
compile project(":qiuqiu-core-packages:qiuqiu-module-base")
//测试文件依赖
testCompile project(":qiuqiu-core-packages:qiuqiu-module-base").sourceSets.test.output
}
}
//放在Module里面
mainClassName = 'com.qiuqiu.huiben.module.ModuleStarter'
jar {
baseName = '${it.name}'
version = '${ext.version}'
//不想每次编译都打包.就改成false就行
enabled = true
manifest {
attributes("Implementation-Title": "Gradle")
attributes("Start-Class": "com.qiuqiu.huiben.module.ModuleStarter")
}
}
}
2. 配置启动项目
首先配置下config地址
--spring.config.location=/home/friddle/Project/qiuqiuhuiben/qiuqiu-core-packages/config/
然后选择相应的classPathOfModule就可以单独启动某个jar进行调试了
DevServlet
单独jar启动的时候其实大家都是为了调试.这个工具就提供了非常好用的调试接口工具
先贴代码:
open class RequestData {
//var serviceName:String?=""
//var methodName:String?=""
var arguments: HashMap<String,Any>? =null
}
open class ServiceDevServlet : HttpServlet() {
val gson = Gson()
//log4j
val logger= LoggerFactory.getLogger(ServiceDevServlet::class.java)
//很简单直接调用嘛
override fun doPost(req: HttpServletRequest?, resp: HttpServletResponse?) {
val urls = req!!.pathInfo.replaceFirst("/", "").split("/")
if (urls.size <= 1) {
throw IllegalArgumentException("接口调用方式不对")
}
var methodName = urls[1]
var serviceName = urls[0]
//调用逻辑后直接返回数据.多方便
try {
val service = SpringContextHolder.getBean<Any>(beanName = serviceName)!!
//多个方法不管
val method = service::class.java.methods.filter { it.name == methodName }.first()
val requestJsonBody = IOUtils.toString(req.inputStream, "UTF-8");
val requestJson = requestJsonBody.toGsonObject(RequestData::class.java)!!;
if(requestJson.arguments==null){requestJson.arguments= hashMapOf()};
val params = method.parameterTypes
if (requestJson.arguments!!.size != params.size) {
throw java.lang.IllegalArgumentException("参数数量不匹配")
}
JsonParser()
val arguments = params.mapIndexed { index, clazz ->
val jsonString = requestJson.arguments!!.values.toList().get(index).toGsonString()
//fastjson
JSON.parseObject(jsonString,clazz)
}.toTypedArray()
val methodRsp = method.invoke(service, *arguments)
val writer = resp!!.writer
resp.contentType = "application/json;charset=UTF-8"
resp.characterEncoding = "UTF-8"
writer.println(methodRsp.toGsonString())
return
} catch (e: Exception) {
val writer = resp!!.writer
logger.error("erropr",e);
writer.println(e.message)
}
}
}
inline fun <reified T> String.toGsonObject(t: Class<T>): T? {
try {
return Gson().fromJson(this, T::class.java)
}catch (e:Exception)
{
logger.error("gson parse error",e)
return null
}
}
注册
@Bean
fun serviceDev(@Autowired context: ApplicationContext): ServletRegistrationBean<*>? {
val registrationBean: ServletRegistrationBean<Servlet> = ServletRegistrationBean<Servlet>(
ServiceDevServlet()
)
registrationBean.addUrlMappings("/dev/*")
return registrationBean
}
再贴调用
简单说下就是:
Post http://localhost:8088/dev/bookServiceImpl/getAllBookByRanks
bookServiceImpl->对应的是beanName
getAllBookByRanks->对应的是方法名字(重名参数不同不管)
{
"arguments": {
"page":{"total":20}
}
}
是参数对象.其实"page"这个key无所谓.关键是顺序不能错.
这样就可以直接用http调试接口了.
Gradle之间的小技巧
gradle build是不是编译很慢很烦人.其实你可以这样:
在本地进行编译且不需要运行jar的时候.可以引入下面的脚本
build.gradle
def profile=System.getenv('profile')
if(profile=='local')
{
subprojects{
tasks.withType(Jar){
task->task.enabled=false
}
tasks.whenTaskAdded {
task->if(
task.name.contains("bootJar")
||task.name.contains("bootDistTar")
||task.name.contains("bootDistZip")
||task.name.contains("jar")
||task.name.contains("distZip")
||task.name.contains("distTar")
//这个是过滤grpcproto的任务的
||task.name.contains("extractTestProto")
||task.name.contains("extractIncludeTestProto")
//||task.name.contains("compileTestKotlin")
//||task.name.contains("compileTestJava")
){
task.enabled=false
}
}
}
}
这样就可以过滤掉吃cpu的bootJar/bootDistTar等操作了.本地本来就不需要打包Jar
同时还可以修改gradle.properties
org.gradle.caching=true
//开启缓存
项目分层后测试文件无法通过compile引入.实际上要引入必须通过sourceSets引入
testCompile project(":qiuqiu-core-packages:qiuqiu-module-base").sourceSets.test.output
全局配置就可以这样配置
configure(subprojects)
{
dependencies {
compile project(":qiuqiu-core-packages:qiuqiu-module-base")
testCompile project(":qiuqiu-core-packages:qiuqiu-module-base").sourceSets.test.output
}
}
附件
这是我项目拆分后的示意图(可供参考)
这是拆分的模块图