通过Tomcat部署SpringBoot程序 作者: nbboy 时间: 2020-03-17 分类: Java 评论 # 概述 因为以前没有通过Tomcat方式部署过程序,所以才有了这次踩坑之旅。这里记录下,权当备忘吧。 # POM文件改动 先把打包方式改为打War包 ``` war ``` 增加Tomcat的依赖,需要说明的是SpringBoot本来就自带一个Tomcat,但是在生产环境中不推荐用这个Tomcat,就是打包的时候不要把他打包进去。 ``` org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat ``` 然后再自己增加一个Tomcat依赖,但是把作用域设置为provided,意思是说在本地开发的时候还是要这个Tomcat包的。 ``` org.springframework.boot spring-boot-starter-tomcat provided ``` 另外再修改servlet-api也根据环境打包,因为生产环境Tomcat有自己的servlet-api包。 ``` javax.servlet javax.servlet-api provided ``` # 安装Tomcat 这个在网上教程比较多,也没有什么坑,我参考的是这篇:[Ubuntu安装Tomcat](https://www.howtoing.com/how-to-install-apache-tomcat-8-on-ubuntu-16-04 "Ubuntu安装Tomcat") # 继承SpringBootServletInitializer 在启动类上继承这个SpringBootServletInitializer,然后重载configure方法,代码如下: ```java @SpringBootApplication @EnableSwagger2Doc @MapperScan(basePackages = "org.chenxf.studystore.dao") public class StudyStoreApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(StudyStoreApplication.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(StudyStoreApplication.class); } } ``` # 项目打包 最后一步,执行mvn clean install -Dmaven.test.skip=true进行打包,打包完成后再target目录下生成的*.war包拷贝到Tomcat的webapps目录下即可。 # 总结 在实际操作中,我没有执行最后一步的打包命令,而是执行了SpringBoot配置的Plugins里的war命令去打包的,结果总是404错误,后来发现打的war包其实并没有把classes下的编译文件打进去。 另外也参考了这两篇文章提供的环境配置思路和tomcat的war包部署方法。基础还是很薄弱呀,需要继续加油,和读者共勉!!! 参考资料: [springboot+tomcat不同环境采用不同配置文件](https://www.cnblogs.com/GreenMountain/p/9720070.html "springboot+tomcat不同环境采用不同配置文件") [tomcat部署war包的三种方式](https://blog.csdn.net/sinat_36710456/article/details/83308236 "tomcat部署war包的三种方式")
Vue项目部署总结 作者: nbboy 时间: 2020-03-17 分类: Vue 评论 这篇算是备忘吧,主要现在博客稳定后,记录在博客里,以后就可以翻阅。平时做前端时间也不多,所以很大概率会把这些配置忘记。这次记录的是VUE项目的打包分发问题,之前采用VUE项目结构后,开发完成先对项目进行打包,输入yarn build 如果输出以下信息代表打包完成 ```shell File Size Gzipped dist/js/chunk-vendors.ba781709.js 198.06 KiB 65.07 KiB dist/js/chunk-44211732.e3e35f12.js 17.01 KiB 6.70 KiB dist/js/chunk-0b2124a4.8bffe25a.js 9.16 KiB 3.61 KiB dist/js/chunk-38e8d3ac.1a464434.js 8.65 KiB 3.08 KiB dist/js/chunk-6ff45dd8.2ae717de.js 8.00 KiB 2.88 KiB dist/js/app.0b3e8e26.js 5.94 KiB 2.49 KiB dist/js/chunk-7c5ee8e8.c8b49195.js 1.26 KiB 0.61 KiB dist/js/chunk-5c9b991c.61a5548b.js 0.78 KiB 0.67 KiB dist/css/chunk-vendors.6bf76ec9.css 71.25 KiB 11.83 KiB dist/css/app.6ff5d64d.css 32.11 KiB 7.65 KiB dist/css/chunk-0b2124a4.7de6da76.css 2.59 KiB 0.74 KiB dist/css/chunk-44211732.9de0d084.css 2.28 KiB 0.54 KiB dist/css/chunk-6ff45dd8.29306b4a.css 1.27 KiB 0.48 KiB dist/css/chunk-38e8d3ac.dd984e30.css 1.27 KiB 0.48 KiB dist/css/chunk-7c5ee8e8.8eab5faf.css 0.81 KiB 0.34 KiB dist/css/chunk-5c9b991c.127e583c.css 0.08 KiB 0.09 KiB Images and other types of assets omitted. DONE Build complete. The dist directory is ready to be deployed. INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html ✨ Done in 11.34s. ``` 可以在项目目录下,看到一个dist文件。主要是html,css,js文件,我们直接把整个目录分发就可以了。 分发到nginx里进行托管,这里给出一个基础的nignx配置: ```shell server { listen 80; server_name you.domain.com; root /var/www/html/projectPath; location / { index index.html; try_files $uri $uri/ /index.html; add_header Cache-Control 'private, no-store, max-age=0'; } } ``` 最后让nginx再重新加载下就可以了~ ```shell /etc/init.d/nginx reload ``` # 总结 nginx分发VUE项目就是分发静态文件,如果需要更进一步,看下VUE文档里的history模式和hash模式。
房源推荐项目总结 作者: nbboy 时间: 2020-03-16 分类: Python 评论 # 接口的定义 先定义一个采样类Sample,主要负责收集采样的信息。 ```python class Sample(metaclass=abc.ABCMeta): """ 采样类 """ # 样本名称 name = '' # 样本类型 type = 0 # 决策者 manager = None def __init__(self, userId): self._userId = userId self._lazy_data = [] @abc.abstractmethod def load(self): """ :parameter userId 加载数据 :return: """ pass @property def items(self): if not self._lazy_data: self._lazy_data = self.load() return self._lazy_data @property def length(self): """ 数据长度 :return: """ return len(self.items) @property def weight(self): """ 数据权重 :return: """ pass @property def workQuota(self): """ 工作配额 :return: """ return int(self.manager.getWorkQuota(self)) ``` 具体的样本种类根据业务的数据来了,比如我的业务涉及到用户推荐房源有关的就用户点单的数据,用户收藏的数据,用户足迹的数据,每个类都可以实例化一个类。 另外一个关键类是SamplePolicyManager,主要负责采样信息的管理和计算采样结果的配额,它的定义如下: ```python class SamplePolicyManager(collections.UserList): """ 采样管理 """ def append(self, item): item.manager = self return super(SamplePolicyManager, self).append(item) def insert(self, i, item): item.manager = self super(SamplePolicyManager, self).insert(i, item) def getWorkQuota(self, sample): """ 根据权重和总数量计算配额 :param sample: :return: """ return sample.weight * TOTAL_RECOMMEND_NUM ``` 因为后面算法需要用到分类,所有我们把这部分概念抽象出来,方便后面灵活添加,创建一个分类器的接口IClassifierInterface。 ```python class IClassifierInterface(metaclass=abc.ABCMeta): """ 特征分类器 """ # 分类数量 CLASSIFY_NUM = 0 # 特征的权重,权重值参与最后的相似度计算,权重约高,影响越大 WEIGHT = 0 NAME = '' @abc.abstractmethod def classify(self, attrs, data): """ 计算分类数据 :parameter attrs 需要分类的数据列 :parameter data 分类好的数据 :return:返回data """ pass @abc.abstractmethod def isDirty(self, attrs): """ 是否脏数据 :param attrs: :return: """ pass def getVectors(self, infos): """ 根据用户的喜好计算出偏好向量 :param infos: :return: """ count = [0] * self.classifyNum vectors = [] for item in infos: count = self.classify(item, count) lenInfo = float(len(infos)) for item in count: vectors.append(item / lenInfo) return vectors @property def classifyNum(self): return self.CLASSIFY_NUM ``` 其实我实际的项目中,分类器有很多,比如价格分类器,面积分类器,户型分类器等等,根据自己的项目需求灵活增加即可。 比如价格分类器,代码如下: ```python class PriceClassifier(IClassifierInterface): """ 价格分类器 """ CLASSIFY_NUM = 13 WEIGHT = 0.2 NAME = 'price' TEN_THOUSAND_RMB = 10000 def isDirty(self, attrs): """ 非法价格,减少曝光 :param attrs: :return: """ return attrs.price is None or attrs.price == 0 def classify(self, attr, data): if attr.price <= 50 * self.TEN_THOUSAND_RMB: data[0] = data[0] + 1 elif 50 * self.TEN_THOUSAND_RMB < attr.price <= 100 * self.TEN_THOUSAND_RMB: data[1] = data[1] + 1 elif 100 * self.TEN_THOUSAND_RMB < attr.price <= 150 * self.TEN_THOUSAND_RMB: data[2] = data[2] + 1 elif 150 * self.TEN_THOUSAND_RMB < attr.price <= 200 * self.TEN_THOUSAND_RMB: data[3] = data[3] + 1 elif 200 * self.TEN_THOUSAND_RMB < attr.price <= 250 * self.TEN_THOUSAND_RMB: data[4] = data[4] + 1 elif 250 * self.TEN_THOUSAND_RMB < attr.price <= 300 * self.TEN_THOUSAND_RMB: data[5] = data[5] + 1 elif 300 * self.TEN_THOUSAND_RMB < attr.price <= 350 * self.TEN_THOUSAND_RMB: data[6] = data[6] + 1 elif 350 * self.TEN_THOUSAND_RMB < attr.price <= 400 * self.TEN_THOUSAND_RMB: data[7] = data[7] + 1 elif 400 * self.TEN_THOUSAND_RMB < attr.price <= 450 * self.TEN_THOUSAND_RMB: data[8] = data[8] + 1 elif 450 * self.TEN_THOUSAND_RMB < attr.price <= 500 * self.TEN_THOUSAND_RMB: data[9] = data[9] + 1 elif 500 * self.TEN_THOUSAND_RMB < attr.price <= 550 * self.TEN_THOUSAND_RMB: data[10] = data[10] + 1 elif 550 * self.TEN_THOUSAND_RMB < attr.price <= 600 * self.TEN_THOUSAND_RMB: data[11] = data[11] + 1 else: data[12] = data[12] + 1 return data ``` 咣咣,请我们的猪脚登场,看具体算法前,我们先看下推荐类的接口定义: ```python class IRecommendInterface(metaclass=abc.ABCMeta): """ 推荐接口 """ houses = None def addClassifier(self, classifier): """ :param classifier: :return: """ self._classifiers.append(classifier) return self def removeClassifier(self, classifier): """ :param classifier: :return: """ self._classifiers.remove(classifier) def __init__(self, houses, aggregation=None): super().__init__() self.houses = houses self._aggregation = aggregation # 分类器集合,分类器需要实现IClassifierInterface self._classifiers = [] self._samples = [] def recommend(self): """ 推荐计算 :param userInfo: :return: """ if self._aggregation is None: raise AssertionError() for sample, loadArgs in self._samples: if sample.length > 0: similarWeight = self._recommend(sample, sample.items) similarWeight = sorted(similarWeight, key=lambda item: item['score'], reverse=True)[sample.workQuota:] self._aggregation.aggregate(sample, similarWeight) return self._aggregation @abc.abstractmethod def _recommend(self, sample, infos): pass def addSample(self, sample, loadArgs=None): loadArgs = loadArgs or [] self._samples.append((sample, loadArgs)) return self @property def aggregation(self): return self._aggregation @aggregation.setter def aggregation(self, value): self._aggregation = value ``` 可以看到后面有一个聚合的方法,为什么需要这个聚合类?原因很简单,因为需要对推荐的数据做再一次的处理。看下聚合接口: ```python class IAggregationRoot(metaclass=abc.ABCMeta): """ 结果聚合处理 """ @abc.abstractmethod def aggregate(self, sample, vectors): pass def __init__(self): self._value = [] def _afterProcess(self, value): return value @property def value(self): return self._afterProcess(self._value) ``` 非常简单,只是做结果的收集,在看推荐过程前,我们先写下测试用例看看,有了上面的类定义,我们用例代码这样写: ```python # 初始化采样决策 policyManager = SamplePolicyManager() userOrderSample = UserOrderSample(userId) # 添加用户点单采样 policyManager.append(userOrderSample) userCollectSample = UserCollectSample(userId) # 添加用户收藏采样 policyManager.append(userCollectSample) userFootSample = UserFootSample(userId) # 添加用户足迹采样 policyManager.append(userFootSample) if not userOrderSample.items and not userCollectSample.items and not userFootSample.items: return # 初始化推荐引擎 cbRecommend = CBRecommend(houses) cbRecommend.aggregation = LimitScoreOrderAggregation(5) cbRecommend \ .addClassifier(PriceClassifier()) \ .addClassifier(SpaceClassifier()) \ .addClassifier(FloorClassifier()) \ .addClassifier(LayoutClassifier()) \ .addClassifier(CommunityClassifier(cityId)) # 获取到推荐相似度向量 similarWeight = cbRecommend \ .addSample(userOrderSample) \ .addSample(userCollectSample) \ .addSample(userFootSample) \ .recommend() print(similarWeight) return similarWeight ``` # 关键类实现 是不是很清晰了?最后只剩下实现推荐接口的CBRecommend实现类没讲解,这个类其实就是正正工作的地方,也就是推荐算法的逻辑,当然我们只要实现接口,完全可以把它换掉。他是这样的: ```python class CBRecommend(IRecommendInterface): """ 基于内容推荐引擎 """ def _cosineSimilarity(self, vectorA, vectorB, lenVectorA): ''' 函数功能:计算两向量的余弦相似度 参数:向量vector_A,vector_B 返回值:两向量的余弦相似度 ''' for i in range(len(vectorB)): if vectorB[i] == 1: index = i break vectorInner = vectorA[index] vectorCos = vectorInner / (lenVectorA) return vectorCos def _recommend(self, sample, infos): similarWeight = [] cachedUserInfos = {} for item in self.houses: weightCos = 0 classifierInfos = [] for classifier in self._classifiers: # 是不是脏数据? if not classifier.isDirty(item): # 这里只是做一个缓存,避免多次计算 if classifier.NAME not in cachedUserInfos: cachedUserInfos[classifier.NAME] = classifier.getVectors(infos) vectors = cachedUserInfos[classifier.NAME] # 分配初始化空军 count = [0] * classifier.classifyNum # 得到分类矩阵 count = classifier.classify(item, count) vectorA = np.sqrt(np.inner(vectors, vectors)) # 获得余弦相似度 sim = self._cosineSimilarity(vectors, count, vectorA) else: sim = -9999 classifierInfos.append({'classifier': classifier, 'score': round(sim * classifier.WEIGHT, 8)}) # 计算总得分 weightCos += sim * classifier.WEIGHT similarWeight.append( {'itemId': item.id, 'item': item, 'score': round(weightCos, 8), 'sample': sample, 'classifiers': classifierInfos}) return similarWeight def __init__(self, houses, aggregation=None): super().__init__(houses, aggregation) ``` 注释我都加上了,每个用户对每套房源都有一个得分,得分最高的推荐给用户就可以了。因为涉及到公司代码,业务相关的代码,我就不展示了,思路就是这个思路,用的推荐算法是基于内容的推荐算法。 # 总结 这个推荐算法当然是一个非常简单的推荐算法,但是效果也还可以,因为是基于用户内容的,所以基本上看过去的数据都是用户想要的。后面,我还会对推荐算法进行讲解,当然不是很专业,也不是我的本职工作,只是业余研究研究。
Laravel程序 最佳实践1 作者: nbboy 时间: 2020-03-14 分类: PHP 评论 # 层的定义 Laravel程序在初始化后,其实相比PHP的其他框架,分层还算比较明确的,正如它的作者所说,是为艺术家写的框架。刚开始的时候,会把所有的代码写在Model里,当代码比较多的时候,往往一个Model代码文件就会膨胀开来。不但Model文件代码非常多,Controller文件代码也非常多,这又会导致复用率非常低。 所以后来在思考一种更好的分层,相信很多从静态语言走过来的读者,比如Java或者C#在开发WEB程序的时候,也是会经典的三层架构MVC。他们更习惯把逻辑代码写在Service层,这样既解决了复用的问题,也解决了把业务逻辑写在Controller层的尴尬。 这时很多人感觉有点疑问,为什么把代码写在Controller层不好?因为根据标准的MVC模型的定义,C这层存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。也就是说,如果把大段的业务逻辑,放在了控制层,不但影响到了复用度,而且导致各层的职责混乱! 说道MVC,简单复习下MVC的一些概念。其实MVC最早不是产生于WEB程序,而开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。也就是按照标准的定义,我们很多人用的都不是MVC,而是变态版本的MVSC,中间加入一个Service层。这里也顺便说下,其实按照标准,M和V的数据变动是双向的,但是目前我们的WEB做不到这点。 再说回Laravel的情况,Laravel的模型层真的是非常舒服,借助于Eloquent ORM,写起查询来真的是一种享受。借用Laravel官方文档说:"Eloquent ORM 提供了一个漂亮、简洁的 ActiveRecord 实现来和数据库交互"。写过的肯定对他的链式写法映象深刻,这种做法是他的优点之一。有了一个足够强大的模型层,事情真的解决了一大半。**按照DDD的想法,模型应该反映领域驱动的模型,引入Repository层来解决模型的加载和存储问题,引入Service层来解决跨模型的事物处理问题。**对于一般的中小型项目,这种分层已经够用了。分层其实本身很简单,难得是大家都要去遵守这种约定和随着项目不停得去对代码和模块层级,类层级进行审视和重构。 # 框架的破坏 这个是一种糟糕的写法,其实第一次我看到这样写的时候,内心真的10万头草泥马在奔腾。在团队里的很多小伙伴,在Controller层写类似这种代码: AuthController.php中 ```php $token=User::createToken($oldUser); ``` User.php中 ```php protected function createToken($user){ ... } ``` Model类中的保护成员竟然在Controller层进行直接调用,说好的保护呢???不过回过头一想,业务程序员是缺乏基础的认识,但是框架怎么能放行这种愚蠢的写法呢!!! 看看框架做了什么?  哇,原来是这么一回事,果然是框架在作怪。不过仔细想想,作者的想法是不是就是这样?我猜作者的想法只是为了给使用者方便,不用再去"NEW"一个对象,但是的确在这里给使用者放行了这种行为。我想作者可能已经想到了这个问题,只是让使用者自己去做规范吧。 # 总结 其实这篇文字真的没写多少,一方面博客刚恢复访问,总得写点什么。另外一方面后期有很多文章正在构思,小弟文笔不怎么样,只求在这里做一个备忘吧,如果能帮到读者,那我很开心了。其实,Laravel总体来说真的是PHP中的好框架了,但是使用中有很多细节需要考虑和决策,甚至当项目大了之后,也有必要做一定的改造。