大数据技术之_24_电影推荐系统项目_08_项目总结及补充

一 数据加载服务1、目标2、步骤二 离线推荐服务2.1 基于统计性算法1、目标2、步骤2.2 基于隐语义模型(LFM)的协同过滤推荐算法(ALS)1、目标2、步骤2.3 基于 ElasticSearch 的内容推荐算法1、目标2、步骤2.4 基于内容的推荐服务--电影标签三 实时推荐服务3.1 推荐算法解析3.2 实时推荐算法的实现过程3.3 日志的预处理四 综合业务服务4.1 后台架构4.2 Spring 框架搭建4.3 API 接口规划五 用户可视化服务5.1 前端框架搭建5.2 创建与运行项目5.2.1 创建项目骨架5.2.2 添加项目依赖5.2.3 创建模块、组件与服务5.2.4 调试项目5.2.5 发布项目六 项目重构6.1 核心模型提取6.2 通过配置的方式来获取硬编码的值6.3 项目打包6.3.1 AngularJS 前端文件打包6.3.2 businessServer 下的 java web 项目的打包方式6.3.3 核心模型 项目的打包方式6.3.4 recommender 下的后端文件打包方式6.4 系统部署


一 数据加载服务

1、目标

【MongoDB】
  1)需要将 Movie【电影数据集】数据集加载到 MongoDB 数据库中的 Movie 表中。
  2)需要将 Rating【用户对电影的评分数据集】数据集加载到 MongoDB 数据库中的 Rating 表中。
  3)需要将 Tag【用户对电影的标签数据集】数据集加载到 MongoDB 数据库中的 Tag 表中。

【ElasticSearch】
  1)需要将 Movie【电影数据集】加载到 ElasticSearch 名叫 Movie 的 Index 中。
  2)需要将 Tag 数据和 Movie 数据做左外连接。

2、步骤

1)先新建一个 Maven 项目,将依赖添加好。
2)分析数据集 Movie、Rating、Tag。
3)新建 case class Movie、Rating、Tag、MongoConfig、ESConfig。
4)加载数据集。
5)将 RDD 数据集装换成 DataFrame。
6)将 DF 加载到 MongoDB 中:
  1. 将原来的 Collection 全部删除
  2. 通过 DF 的 write 方法将数据写入
  3. 创建数据库索引
  4. 关闭 MongoDB 连接
7)将 DF 加载到 ElasticSearch 中:
  1. 将存在的 Index 删除掉,然后创建新的 Index
  2. 通过 DF 的 write 方法将数据写入
8)关闭 Spark 集群

二 离线推荐服务

2.1 基于统计性算法

1、目标

1、优质电影
  1)获取所有历史数据中评分次数最多的电影的集合,统计每个电影的评分数 --> RateMoreMovies

2、热门电影
  1)按照月来统计,这个月中评分次数量最多的电影我们认为是热门电影,统计每个月中的每个电影的评分数量 --> RateMoreRecentlyMovies

3、统计电影的平均评分
  1)将Rating数据集中所有的用户评分数据进行平均,计算每一个电影的平均评分 --> AverageMoviesScore

4、统计每种类别电影的 TOP10 电影
  1)将每种类别的电影中评分最高的 10 个电影分别计算出来 --> GenresTopMovies

2、步骤

1、新建一个项目 StaticticsRecommender,配置好你的依赖
2、统计所有历史数据中电影的评分个数
  1)通过 Rating 数据集,用 mid 进行 group by 操作,count 计算总数
3、统计以月为单位的电影的评分个数
  1)需要注册一个 UDF 函数,用于将 Timestamp 这种格式的数据转换成 yyyyMM 这个格式 --> SimpleDateFormat
  2)需要将 RatingDF 转换成新的 RatingOfMouthDF【只有日期数据发生了转换】
  3)通过 group by yearmonth, mid 来完成统计
4、统计每个电影评分得分
  1)通过 Rating 数据集,用户 mid 进行 group by 操作,avg 计算评分
5、统计每种类别中评分最高的 10 个电影
  1)需要通过 JOIN 操作将电影的平均评分数据和 Movie 数据集进行合并,产生 MovieWithScore 数据集
  2)需要将电影的类别数据转换成 RDD --> GenresRDD
  3)将 GenresRDD 和 MovieWithScore 数据集进行笛卡尔积,产生一个 N * M 行的数据集
  4)通过过滤操作,过滤掉电影的真实类别和 GenresRDD 中的类别不匹配的电影
  5)通过 Genres 作为 Key,进行 groupByKey 操作,将相同电影类别的电影进行聚集
  6)通过排序和提取,获取评分最高的 10 个电影 --> genresTopMoviesDF
  7)将结果输出到 MongoDB 中

统计每种类别中评分最高的 10 个电影图解:

2.2 基于隐语义模型(LFM)的协同过滤推荐算法(ALS)

1、目标

1、训练 ALS 推荐模型(ALS:交替最小二乘法)
2、计算用户电影推荐矩阵
3、计算电影相似度矩阵

2、步骤

1、训练 ALS 推荐模型
  1)需要构建 RDD[Rating] 类型的训练集数据
  2)直接通过 ALS.train 方法来进行模型训练

2、计算用户电影推荐矩阵
  1)生成 userMovies --> RDD[(Int,Int)]
  2)通过 ALS 模型的 predict 方法来预测评分
  3)将数据通过 groupByKey 处理后排序,取前 N 个作为推荐结果

3、计算电影相似度矩阵
  1)获取电影的特征矩阵,转换成 DoubleMatrix
  2)电影的特征矩阵之间做笛卡尔积,通过余弦相似度计算两个电影的相似度
  3)将数据通过 GroupBy 处理后,输出

4、ALS 模型的参数选择
  1)通过计算 ALS 的均方根误差来判断参数的优劣程度

2.3 基于 ElasticSearch 的内容推荐算法

1、目标

  基于内容的推荐通常是给定一篇文档信息,然后给用户推荐与该文档相识的文档。Lucene 的 api 中有实现查询文章相似度的接口,叫 MoreLikeThis。Elasticsearch 封装了该接口,通过 Elasticsearch 的 More like this 查询接口,我们可以非常方便的实现基于内容的推荐。
  在本项目中 ElasticSearch 除了提供基础的模糊检索功能外,主要提供了电影之间基 于More like this 查询相似度之间的功能,使电影依据演员、导演、名称、描述、标签等进行相似度计算,返回查询电影的相似电影集合。
  由于该功能已有 ES 进行实现,故该功能不用提前计算或者实时计算,只是需要在业务服务器查询推荐集合的时候,将结果集按照业务规则进行合并即可。

2、步骤

核心算法如下:

// 基于内容的推荐算法
private List<Recommendation> findContentBasedMoreLikeThisRecommendations(int mid, int maxItems) {
    MoreLikeThisQueryBuilder query = QueryBuilders.moreLikeThisQuery(
            new String[]{"id"},
            new String[]{"name""descri""genres""actors""directors""tags"},
            new MoreLikeThisQueryBuilder.Item[]{new MoreLikeThisQueryBuilder.Item(Constant.ES_INDEX, Constant.ES_MOVIE_TYPE, String.valueOf(mid))});

    return parseESResponse(esClient.prepareSearch().setQuery(query).setSize(maxItems).execute().actionGet());
}

private List<Recommendation> parseRecs(Document document, int maxItems) {
    List<Recommendation> recommendations = new ArrayList<>();
    if (null == document || document.isEmpty())
        return recommendations;
    ArrayList<Document> recs = document.get("recs", ArrayList.class);
    for (Document recDoc : recs) {
        recommendations.add(new Recommendation(recDoc.getInteger("rid"), recDoc.getDouble("r")));
    }
    Collections.sort(recommendations, new Comparator<Recommendation>() {
        @Override
        public int compare(Recommendation o1, Recommendation o2) {
            return o1.getScore() > o2.getScore() ? -1 : 1;
        }
    });
    return recommendations.subList(0, maxItems > recommendations.size() ? recommendations.size() : maxItems);
}

2.4 基于内容的推荐服务--电影标签

  原始数据中的 tag 文件,是用户给电影打上的标签,这部分内容想要直接转成评分并不容易,不过我们可以将标签内容进行提取,得到电影的内容特征向量,进而可以通过求取相似度矩阵。这部分可以与实时推荐系统直接对接,计算出与用户当前评分电影的相似电影,实现基于内容的实时推荐。为了避免热门标签对特征提取的影响,我们还可以通过 TF-IDF 算法(词频-逆文档频率)对标签的权重进行调整,从而尽可能地接近用户偏好。

  核心算法如下:

package com.atguigu.content

import org.apache.spark.SparkConf
import org.apache.spark.ml.feature.{HashingTF, IDF, Tokenizer}
import org.apache.spark.ml.linalg.SparseVector
import org.apache.spark.sql.SparkSession
import org.jblas.DoubleMatrix

// 需要的数据源是电影内容信息
case class Movie(mid: Int, name: String, descri: String, timelong: String, issue: String,
                 shoot: String, language: String, genres: String, actors: String, directors: String)


case class MongoConfig(uri: String, db: String)

// 定义一个基准推荐对象
case class Recommendation(mid: Int, score: Double)

// 定义电影内容信息提取出的特征向量的电影相似度列表
case class MovieRecs(mid: Int, recs: Seq[Recommendation])

object ContentRecommender 
{

  // 定义表名和常量
  val MONGODB_MOVIE_COLLECTION = "Movie"
  val CONTENT_MOVIE_RECS = "ContentMovieRecs"

  def main(args: Array[String]): Unit = {
    val config = Map(
      "spark.cores" -> "local[*]",
      "mongo.uri" -> "mongodb://hadoop102:27017/recommender",
      "mongo.db" -> "recommender"
    )

    // 创建一个 SparkConf 对象
    val sparkConf = new SparkConf().setMaster(config("spark.cores")).setAppName("ContentRecommender")

    // 创建一个 SparkSession 对象
    val spark = SparkSession.builder().config(sparkConf).getOrCreate()

    // 声明一个隐式的配置对象
    implicit val mongoConfig = MongoConfig(config("mongo.uri"), config("mongo.db"))

    // 在对 DataFrame 和 Dataset 进行许多操作都需要这个包进行支持
    import spark.implicits._

    // 加载数据,并作预处理
    val movieTagsDF = spark.read
      .option("uri", mongoConfig.uri)
      .option("collection", MONGODB_MOVIE_COLLECTION)
      .format("com.mongodb.spark.sql")
      .load()
      .as[Movie]
      .map { // 提取 mid,name,genres 三项作为原始的内容特征,分词器默认分隔符是空格
        x => (x.mid, x.name, x.genres.map(c => if (c == '|'' ' else c))
      }
      .toDF("mid""name""genres")
      .cache()

    // TODO:从内容信息中提取电影特征的特征向量
    // 创建一个分词器,默认按照空格分词
    val tokenizer = new Tokenizer().setInputCol("genres").setOutputCol("words")
    // 用分词器对原始数据进行转换,生成新的一列words
    val wordsData = tokenizer.transform(movieTagsDF)

    // 引入 HashingTF 工具,该工具可以将词语序列转换成对应的词频
    val hashingTF = new HashingTF().setInputCol("words").setOutputCol("rawFeatures").setNumFeatures(50)
    val featurizeData = hashingTF.transform(wordsData)

    // 测试
    // wordsData.show()
    // featurizeData.show()
    // featurizeData.show(truncate = false) // 不压缩显示

    // 引入 IDF 工具,该工具可以得到 IDF 模型
    val idf = new IDF().setInputCol("rawFeatures").setOutputCol("features")
    // 训练 IDF 模型,得到每个词的逆文档频率
    val idfModel = idf.fit(featurizeData)

    // 用 IDF 模型对原数据进行处理,得到文档中每个词的 TF-IDF,作为新的特征向量
    val rescaleData = idfModel.transform(featurizeData)

    // 测试
    // rescaleData.show(truncate = false) // 不压缩显示

    val movieFeatures = rescaleData.map(
      row => (row.getAs[Int]("mid"), row.getAs[SparseVector]("features").toArray)
    ).rdd.map(
      x => (x._1, new DoubleMatrix(x._2))
    )

    // 测试
    // movieFeatures.collect().foreach(println)

    // 对所有电影两两计算它们的相似度,先做笛卡尔积
    val movieRecs = movieFeatures.cartesian(movieFeatures)
      .filter {
        // 把自己跟自己的配对过滤掉
        case (a, b) => a._1 != b._1
      }
      .map {
        case (a, b) => {
          val simScore = this.consinSim(a._2, b._2)
          (a._1, (b._1, simScore))
        }
      }
      .filter(_._2._2 > 0.6// 过滤出相似度大于 0.6 的
      .groupByKey()
      .map {
        case (mid, recs) => MovieRecs(mid, recs.toList.sortWith(_._2 > _._2).map(x => Recommendation(x._1, x._2)))
      }
      .toDF()

    // 把结果写入对应的 MongoDB 表中
    movieRecs.write
      .option("uri", mongoConfig.uri)
      .option("collection", CONTENT_MOVIE_RECS)
      .mode("overwrite")
      .format("com.mongodb.spark.sql")
      .save()

    spark.stop()
  }

  // 求两个向量的余弦相似度
  def consinSim(movie1: DoubleMatrix, movie2: DoubleMatrix): Double = {
    movie1.dot(movie2) / (movie1.norm2() * movie2.norm2()) // l1范数:向量元素绝对值之和;l2范数:即向量的模长(向量的长度),向量元素的平方和再开方
  }
}

三 实时推荐服务

3.1 推荐算法解析

3.2 实时推荐算法的实现过程

实时推荐算法的前提:
  1.在 Redis 集群中存储了每一个用户最近对电影的 K 次评分。实时算法可以快速获取。
  2.离线推荐算法已经将电影相似度矩阵提前计算到了 MongoDB 中。
  3.Kafka 已经获取到了用户实时的评分数据。
算法过程如下:
  实时推荐算法输入为一个评分<userId, mid, rate, timestamp>,而执行的核心内容包括:获取 uid 最近 K 次评分、获取 mid 最相似 K 个电影、计算候选电影的推荐优先级、更新对 uid 的实时推荐结果。

3.3 日志的预处理

1、目标
  1)根据埋点数据信息,格式化日志
2、步骤
  1)构建 LogProcessor 实现 Processor 接口,实现对于数据的处理
  2)构建 StreamsConfig 配置数据
  3)构建 TopologyBuilder 来设置数据处理拓扑关系
  4)构建 KafkaStreams 来启动整个处理

四 综合业务服务

4.1 后台架构

  


  后台服务通过 Spring 框架进行创建,主要负责后台数据和前端业务的交互。项目主要分为 REST 接口服务层、业务服务层、业务模型以及工具组件层等组成。
  REST 接口服务层:主要通过 Spring MVC 为 UI 提供了通讯接口,主要包括用户接口、推荐接口、评分接口、查询接口、标签接口以及统计接口。
  业务服务层:主要实现了整体系统的业务逻辑,提供了包含电影相对应操作的服务、评分层面的服务、推荐层面的服务、标签层面的服务以及用户层面的服务。
  业务模型方面:将推荐、业务请求以及具体业务数据进行模型创建。
  工具组件层面:提供了对 Redis、ES、MongoDB 的客户端以及项目常量定义。

 

4.2 Spring 框架搭建

  1、添加相对应对的依赖包。
  2、创建 application.xml 配置文件,配置 application context。
  3、创建 application-servlet.xml 配置文件,用于配置 web application context。
  4、配置 web.xml 将 application context 和 web application context 整合。

  在 MovieRecommendSystem 项目下新建一个 Module 项目 businessServer,然后在 MovieRecommendSystem\businessServer\src\main 目录下新建 webapp 目录,此时可能会出现一个问题:新建的 webapp 文件夹不能被识别(没有小蓝点),解决问题链接:https://www.cnblogs.com/chenmingjun/p/10920548.html

  注意:如果导入他人已经写好的项目时,发现导入的项目与自己的整个项“格格不入”时,这时可以删除整个项目在 IDEA 中的配置数据,其文件夹是 .idea,然后删除缓存索引数据并重启 IEDA(File -> Invalidate Caches / Restart…),再重新加载该项目,重新生成新的 .idea 文件 和 缓存索引数据。即可解决问题!

依赖 pom.xml 文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>MovieRecommendSystem</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>businessServer</artifactId>
    <packaging>war</packaging><!-- 说明是一个 web 项目,打的包不是 jar,而是 war 包 -->

    <properties>
        <log4j2.version>2.9.1</log4j2.version>
        <spring.version>4.3.6.RELEASE</spring.version>
        <spring.data.jpa.version>1.11.0.RELEASE</spring.data.jpa.version>
        <jackson.version>2.8.6</jackson.version>
        <servlet.version>3.0.1</servlet.version>
        <es.version>5.6.2</es.version>
        <mongo.version>3.5.0</mongo.version>
        <jedis.version>2.9.0</jedis.version>
    </properties>

    <dependencies>
        <dependency>
            <!-- log4j2 -->
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j2.version}</version>
        </dependency>

        <dependency>
            <!-- 用于 Servlet 的开发 -->
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${servlet.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>transport</artifactId>
            <version>${es.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mongodb</groupId>
            <artifactId>mongodb-driver</artifactId>
            <version>${mongo.version}</version>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Spring End -->

        <!-- fasterxml 用于 JSON 和对象之间的转换 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <!-- fasterxml end -->
    </dependencies>

    <build>
        <finalName>BusinessServer</finalName>
        <plugins>
            <plugin>
                <!-- 该插件用于在 Maven 中提供 Tomcat 运行环境,你还可以使用 Jetty 插件 -->
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <!-- 指定运行后可以访问的端口 -->
                    <port>8888</port>
                    <!-- 指定了运行时的根目录 -->
                    <path>/</path>
                    <!-- 当你更改了代码后,tomcat 自动重新加载 -->
                    <contextReloadable>true</contextReloadable>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

整个目录结构如下图所示:

配置文件内容如下:(注意:本博主配置文件的内容是整个项目的)
MovieRecommendSystem\businessServer\src\main\resources\application.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd"
>


    <!-- 用于让 Spring 去扫描整个 com.atguigu.business 目录下的代码 -->
    <context:component-scan base-package="com.atguigu.business"/>

    <context:property-placeholder location="classpath:recommend.properties" ignore-unresolvable="true"/>

    <!-- 用于 JSON 和对象之间的转换,这里是父容器 -->
    <bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
        <property name="featuresToEnable">
            <array>
                <util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.CLOSE_CLOSEABLE"/>
            </array>
        </property>
        <property name="featuresToDisable">
            <array>
                <util:constant static-field="com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS"/>
            </array>
        </property>
    </bean>
</beans>

MovieRecommendSystem\businessServer\src\main\resources\log4j.properties

log4j.rootLogger=INFO, file, stdout

# write to stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  %5p --- [%50t]  %-80c(line:%5L)  :  %m%n

# write to file
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=D:\\learn\\JetBrains\\workspace_idea\\MovieRecommendSystem\\businessServer\\src\\main\\log\\agent.log
log4j.appender.file.MaxFileSize=1024KB
log4j.appender.file.MaxBackupIndex=1
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  %5p --- [%50t]  %-80c(line:%6L)  :  %m%n

#log4j.appender.syslog=org.apache.log4j.net.SyslogAppender
#log4j.appender.syslog=com.c4c.dcos.commons.logger.appender.SyslogAppenderExt
#log4j.appender.syslog.SyslogHost= 192.168.25.102
#log4j.appender.syslog.Threshold=INFO
#log4j.appender.syslog.layout=org.apache.log4j.PatternLayout
#log4j.appender.syslog.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  %5p --- [%20t]  %-130c:(line:%4L)  :   %m%n
#demo|FATAL|2014-Jul-03 14:34:34,194|main|com.c4c.logdemo.App:(line:15)|send a log

MovieRecommendSystem\businessServer\src\main\resources\log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Root level="error">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

businessServer\src\main\resources\recommend.properties

#elasticSearch
es.host=hadoop102
es.port=9300
es.cluster.name=my-application

#Mongodb
mongo.host=hadoop102
mongo.port=27017

#Redis
redis.host=hadoop102

MovieRecommendSystem\businessServer\src\main\webapp\WEB-INF\application-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
                           http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd"
>


    <!-- 用于让 Spring 去扫描整个 com.atguigu.business.rest 目录下的代码 -->
    <context:component-scan base-package="com.atguigu.business.rest"/>

    <!-- 能够让 web 应用启用 web 方面的注解 -->
    <mvc:annotation-driven>
        <!-- 给 MVC 框架注册消息装换器 -->
        <mvc:message-converters register-defaults="true">
            <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
            <!-- 用于 JSON 的转换 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="supportedMediaTypes">
                    <list>
                        <value>text/html;charset=UTF-8</value>
                        <value>application/json;charset=UTF-8</value>
                    </list>
                </property>
                <!-- 子容器引用父容器中的 bean 即可,不用子容器自己创建了,父容器 application.xml -->
                <property name="objectMapper" ref="objectMapper" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 内容协商解析器 -->
    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="contentNegotiationManager" ref="contentNegotiationManager"/>
        <property name="viewResolvers">
            <list>
                <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                    <property name="prefix" value="/"/>
                    <property name="suffix" value=".jsp"/>
                </bean>
            </list>
        </property>
        <property name="defaultViews">
            <list>
                <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
            </list>
        </property>
        <property name="useNotAcceptableStatusCode" value="true"/>
    </bean>
    <bean id="contentNegotiationManager"
          class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">

        <property name="defaultContentType" value="application/json"/>
        <property name="mediaTypes">
            <value>
                json=application/json
                xml=text/xml
                html=text/html
            </value>
        </property>
    </bean>

    <!-- 用于处理静态文件用的 -->
    <mvc:default-servlet-handler/>

    <!-- 跨域访问的支持 -->
    <mvc:cors>
        <mvc:mapping path="/**" allowed-origins="*" allow-credentials="true" max-age="1800" allowed-methods="GET,POST,OPTIONS"/>
    </mvc:cors>
</beans>

MovieRecommendSystem\businessServer\src\main\webapp\WEB-INF\web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.0"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
>


    <display-name>recommender</display-name>

    <!-- 用于指定 Spring Application Context 的配置文件路径 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:application.xml</param-value>
    </context-param>
    <listener>
        <!-- Spring 用于监听 Serlvet 容器的启动,进而启动 Spring Application Context -->
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- 配置你的 Servlet -->
    <servlet>
        <servlet-name>recommend</servlet-name>
        <!-- spring 提供的 Servlet 的实现 -->
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- 指定了 Web Application Context 的配置文件 -->
            <param-value>/WEB-INF/application-servlet.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>recommend</servlet-name>
        <!-- 要让 recommend 这个 Servlet 处理所有的 URL -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <welcome-file-list>
        <!-- 定义你的欢迎文件 -->
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
</web-app>

MovieRecommendSystem\businessServer\src\main\webapp\index.html

<!--这是别人访问你的网站是看到的主页面的HTML文件。大多数情况下你都不用编辑它。
在构建应用时,CLI 会自动把所有 js 和 css 文件添加进去,所以你不必在这里手动添加任何 <script> 或 <link> 标签。-->

<!DOCTYPE html>
<html>
<head>
    <base href="/">
    <title>MovieLens海外电影推荐系统</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">

</head>

<body>
<movie-app>加载中...</movie-app>
    <script type="text/javascript" src="inline.bundle.js"></script>
    <script type="text/javascript" src="polyfills.bundle.js"></script>
    <script type="text/javascript" src="scripts.bundle.js"></script>
    <script type="text/javascript" src="styles.bundle.js"></script>
    <script type="text/javascript" src="vendor.bundle.js"></script>
    <script type="text/javascript" src="main.bundle.js"></script>
</body>

<script type="application/javascript">
    document.oncontextmenu = function () {
        return false;
    }
    document.onkeydown = function (event{
        var e = event || window.event || arguments.callee.caller.arguments[0];
        if (e && e.keyCode == 116) {
            return false;
        }
    }
</script>
</html>

4.3 API 接口规划

五 用户可视化服务

5.1 前端框架搭建

AngularJS 框架:

电影推荐系统前端框架:

5.2 创建与运行项目

详细文档参考:https://angular.cn/guide/quickstart

5.2.1 创建项目骨架

在 CMD 中相对应的目录中执行:ng new my-app,my-app 为项目的名称,可以任意起名称。

【Src主文件夹】
你的应用代码位于 src 文件夹中。所有的 Angular 组件、模板、样式、图片以及你的应用所需的任何东西都在那里。
这个文件夹之外的文件都是为构建应用提供支持用的。

【根目录文件夹】
src/ 文件夹是项目的根文件夹之一。 其它文件是用来帮助你构建、测试、维护、文档化和发布应用的。它们放在根目录下,和 src/ 平级。

5.2.2 添加项目依赖

在 CMD 中项目目录中执行:npm install bootstrap --save,添加 bootstrap 依赖。
在 CMD 中项目目录中执行:npm install jquery --save,添加bootstrap 依赖。
在 CMD 中项目目录中执行:npm install systemjs --save,添加 bootstrap 依赖。

5.2.3 创建模块、组件与服务

在 CMD 中项目目录中执行:ng g module AppRouting,来创建新模块。
在 CMD 中项目目录中执行:ng g component home,来创建新组件。
在 CMD 中项目目录中执行:ng g service service/login,来创建新服务组件。

5.2.4 调试项目

在 CMD 中项目目录中执行:ng serve –p 3000,启动整个应用程序。
访问:http://localhost:4200
当你修改了后台代码的时候,浏览器自动 Reload。

5.2.5 发布项目

在 CMD 中项目目录中执行:ng build,来打包发布整个应用程序。
会在目录下生成 dist 文件夹,该文件夹就是最终的发布程序。

六 项目重构

1、提取公共的模型
  1)将所有模型和共有的常量定义提取到一个 Module 里面。
  2)将包含模型和常量定义的 Module 引入到相应的模块里面。
  3)使用模型 Module 里面的定义替代模块中的相应定义。
2、修改程序中的硬编码
  1)通过配置的方式来获取硬编码的值。

6.1 核心模型提取

6.2 通过配置的方式来获取硬编码的值

6.3 项目打包

1、针对每一个项目分别编辑相对应的打包过程
2、运行 Root 项目下的 package 阶段,进行打包

6.3.1 AngularJS 前端文件打包

maven 调用 cmd 命令的的插件

website 的 pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>MovieRecommendSystem</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>website</artifactId>

    <build>
        <finalName>website</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <finalName>website</finalName>
                    <descriptors>
                        <descriptors>assembly/dependencies.xml</descriptors>
                    </descriptors>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <id>ng-build</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                        <configuration>
                            <executable>ng</executable>
                            <arguments>
                                <argument>build</argument>
                            </arguments>
                            <workingDirectory>${basedir}/website/</workingDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

dependencies.xml

<assembly>
    <id>release</id>
    <formats>
        <format>tar.gz</format>
    </formats>

    <!-- 设置要打包的文件 -->
    <fileSets>
        <fileSet>
            <directory>images</directory>
            <outputDirectory>./images</outputDirectory>
            <includes>
                <include>**</include>
            </includes>
        </fileSet>
    </fileSets>

    <fileSets>
        <fileSet>
            <directory>website/dist</directory>
            <outputDirectory>./</outputDirectory>
            <includes>
                <include>**</include>
            </includes>
        </fileSet>
    </fileSets>
</assembly>

6.3.2 businessServer 下的 java web 项目的打包方式

在要打包的项目中的 pom.xml 文件中只需要添加以下内容:
MovieRecommendSystem\businessServer\pom.xml

    <packaging>war</packaging><!-- 说明是一个 web 项目,打的包不是 jar,而是 war 包 -->

6.3.3 核心模型 项目的打包方式

不用任何设置,默认打的是 jar 包,如果单独打 recommender 下项目的包,需要先打核心模型包。

6.3.4 recommender 下的后端文件打包方式

在每一个要打包的子项目中的 pom.xml 文件中添加以下内容:
例如:MovieRecommendSystem\recommender\DataLoader\pom.xml

    <build>
        <finalName>dataLoader</finalName><!-- 名字任意起 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.atguigu.recommender.DataLoader</mainClass><!-- 修改为对应的主类 -->
                        </manifest>
                    </archive>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
        </plugins>
    </build>

在 父 的 pom.xml 文件中,对于不需要打进 jar 中的依赖,使用 <scope>provided</scope> 配置即可。如下:
MovieRecommendSystem\recommender\pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <parent>
        <artifactId>MovieRecommendSystem</artifactId>
        <groupId>com.atguigu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>recommender</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>DataLoader</module>
        <module>StatisticsRecommender</module>
        <module>OfflineRecommender</module>
        <module>StreamingRecommender</module>
        <module>ContentRecommender</module>
        <module>KafkaStreaming</module>
    </modules>

    <!-- 仅申明子项目共有的依赖,并不引入,如果子项目需要此依赖,那么子项目需要声明方可引入 -->
    <dependencyManagement>
        <dependencies>
            <!-- 引入 Spark 相关的 Jar 包 -->
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-core_2.11</artifactId>
                <version>${spark.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-sql_2.11</artifactId>
                <version>${spark.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-streaming_2.11</artifactId>
                <version>${spark.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-mllib_2.11</artifactId>
                <version>${spark.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.apache.spark</groupId>
                <artifactId>spark-graphx_2.11</artifactId>
                <version>${spark.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
            <dependency>
                <groupId>org.scala-lang</groupId>
                <artifactId>scala-library</artifactId>
                <version>${scala.version}</version>
                <!-- provided 如果存在,那么运行时该 jar 包不存在,也不会打包到最终的发布版本中,只是编译期有效 -->
                <!--<scope>provided</scope>-->
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <!-- 父项目已声明该 plugin,子项目在引入的时候,不用声明版本和已经声明的配置 -->
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

6.4 系统部署

1、项目的打包

2、前端的部署
  1)将前端的包 website-release.tar.gz 部署在 /var/www/html 中

3、业务服务器的部署
  1)将后台的代码解压复制到 tomcat/webapps/ROOT 下

4、流式计算的部署
  1)启动 Kafka
  2)编辑 Flume 配置文件
  3)启动 Flume
  4)启动 StreamingRecommender 程序

5、离线计算的部署
  1)azkaban 启动
  2)打包统计服务和离线推荐服务,并放到 Linux 目录下
  3)编写 azkaban 的 job 文件,去调度两个 jar 包

posted @ 2019-05-25 23:14 黑泽君 阅读(...) 评论(...) 编辑 收藏