2020-01-03

Application.java

package com.caoheyang;

import com.caoheyang.config.CorsConfig;
import com.caoheyang.controller.DataController;
import com.caoheyang.controller.HomeController;
import com.caoheyang.service.ShortUrlService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;


/**
 * 系统启动类
 *
 * @author CaoHeYang
 * 

* 1、We use direct @Import instead of @ComponentScan to speed up cold starts * 2、ComponentScan(basePackages = "com.caoheyang.controller") *

* @date 20191221 */ @SpringBootApplication @Import({HomeController.class, DataController.class, ShortUrlService.class, CorsConfig.class}) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

DataController.java

package com.caoheyang.controller;

import com.caoheyang.model.request.GenerateShortUrlDomain;
import com.caoheyang.model.response.ShortUrlDomain;
import com.caoheyang.service.ShortUrlService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;


/**
 * Short主数据处理
 *
 * @author CaoHeYang
 * @date 20191221
 */
@RestController
@EnableWebMvc
public class DataController {

    private final static Logger logger = LogManager.getLogger(DataController.class);

    @Autowired
    private ShortUrlService shortUrlService;


    /**
     * @param requestBody
     * @return
     */
    @PostMapping(path = "/data/shorten")
    public ShortUrlDomain generateShortUrl(@RequestBody GenerateShortUrlDomain requestBody) throws UnsupportedEncodingException {


        logger.debug("解码前的longUrl:" + requestBody.getLongUrl());

        requestBody.setLongUrl(URLDecoder.decode(requestBody.getLongUrl(), "UTF-8"));
        logger.debug("解码后的longUrl:" + requestBody.getLongUrl());

        //生成短链URL
        String shortUrl = shortUrlService.generateShortUrl(requestBody);
        logger.debug("生成的短链shortUrl:" + shortUrl);

        ShortUrlDomain response = new ShortUrlDomain();
        response.setShortUrl(shortUrl);

        return response;
    }

}

HomeController.java

package com.caoheyang.controller;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.caoheyang.model.dynamo.TinyUrlItem;
import com.caoheyang.service.ShortUrlService;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Shorten重定向处理
 *
 * @author CaoHeYang
 * @date 20191221
 */
@RestController
@EnableWebMvc
public class HomeController {

    private final static Logger logger = LogManager.getLogger(HomeController.class);

    @Autowired
    private ShortUrlService shortUrlService;

    /**
     * 根据加密之后的短链编码永久重定向到长链地址
     *
     * @param resource 加密之后的短链编码
     * @param response HttpServletResponse重定向类
     * @throws IOException
     */
    @GetMapping(path = "/{resource}")
    public void redirect(@PathVariable("resource") String resource, HttpServletResponse response) throws IOException {
        //记录次数,这是主要的商业模式
        logger.debug("从请求中获取到的长链resource:" + resource);

        String redirectUrl = shortUrlService.getRedirectUrl(resource);
        logger.debug("重定向地址:" + redirectUrl);

        //短链不存在,重定向到404页面
        response.setStatus(301);
        response.sendRedirect(redirectUrl);
    }
}

ShortUrlService.java

package com.caoheyang.service;

import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.caoheyang.model.dynamo.TinyUrlItem;
import com.caoheyang.model.request.GenerateShortUrlDomain;
import com.caoheyang.util.TinyUrlUtil;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * ShortURL业务逻辑处理
 *
 * @author CaoHeYang
 * @date 20191221
 */
@Service
public class ShortUrlService {

    private final static Logger logger = LogManager.getLogger(ShortUrlService.class);

    @Value("${tiny.url.website}")
    private String tinyUrlWebsite;

    @Value("${tiny.url.api}")
    private String tinyUrlApi;


    private DynamoDBMapper mapper;

    /**
     * 无参构造函数。初始化DynamoDB链接
     */
    public ShortUrlService() {
        AmazonDynamoDB amazonDynamoDB = AmazonDynamoDBClientBuilder.defaultClient();
        mapper = new DynamoDBMapper(amazonDynamoDB);
    }

    /**
     * 根据长链URL生成短链URL
     *
     * @param model 长链URL领域模型
     * @return 短链地址
     */
    public String generateShortUrl(GenerateShortUrlDomain model) {

        String shortSourceId = TinyUrlUtil.shortUrl(model.getLongUrl());
        logger.debug("生成的短链shortSourceId:" + shortSourceId);

        TinyUrlItem tinyUrlItem = new TinyUrlItem();

        tinyUrlItem.setLongUrl(model.getLongUrl());
        tinyUrlItem.setShortSourceId(shortSourceId);

        //TODO 查询当前shortSourceId+longUrl是否存在
        //存入DDB
        mapper.save(tinyUrlItem);
        logger.debug("存入DynamoDB成功");

        return tinyUrlApi + '/' + shortSourceId;
    }

    /**
     * 根据加密之后的短链编码永久重定向到长链地址
     *
     * @param resource 加密之后的短链编码
     * @return 重定向地址
     */
    @GetMapping(path = "/{resource}")
    public String getRedirectUrl(String resource) {
        //记录次数,这是主要的商业模式
        TinyUrlItem tinyUrlItem = mapper.load(TinyUrlItem.class, resource);
        //查询到数据重定向到长链,否则重定向到404
        return tinyUrlItem != null ? tinyUrlItem.getLongUrl() : tinyUrlWebsite + "/404.html";
    }
}

TinyUrlItem.java

package com.caoheyang.model.dynamo;


import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;


/**
 * DynamoD TinyUrl_Relation 表的记录项
 *
 * @author CaoHeYang
 * @date 20191221
 */
@DynamoDBTable(tableName = "TinyUrl_Relation")
public class TinyUrlItem {

    private String shortSourceId;
    private String longUrl;

    @DynamoDBHashKey(attributeName = "shortSourceId")
    public String getShortSourceId() {
        return shortSourceId;
    }

    public void setShortSourceId(String shortSourceId) {
        this.shortSourceId = shortSourceId;
    }

    @DynamoDBAttribute(attributeName = "longUrl")
    public String getLongUrl() {
        return longUrl;
    }

    public void setLongUrl(String longUrl) {
        this.longUrl = longUrl;
    }

}

GenerateShortUrlDomain.java

package com.caoheyang.model.request;


/**
 * 生成短链加密编码的入参领域模型
 *
 * @author CaoHeYang
 * @date 20191221
 */
public class GenerateShortUrlDomain {

    //长链
    private String longUrl;

    /**
     * 获取长链
     *
     * @return 长链
     */
    public String getLongUrl() {
        return longUrl;
    }

    /**
     * 设置长链
     *
     * @param longUrl 长链
     */
    public void setLongUrl(String longUrl) {
        this.longUrl = longUrl;
    }

}

ShortUrlDomain.java

package com.caoheyang.model.response;


/**
 * 生成短链加密编码返回给客户端的领域模型
 *
 * @author CaoHeYang
 * @date 20191221
 */
public class ShortUrlDomain {

    //加密之后的短链编码
    private String shortUrl;

    /**
     * 加密之后的短链编码
     *
     * @return
     */
    public String getShortUrl() {
        return shortUrl;
    }

    /**
     * 加密之后的短链编码
     *
     * @param shortUrl 短链地址
     */
    public void setShortUrl(String shortUrl) {
        this.shortUrl = shortUrl;
    }
}

Md5Util.java

package com.caoheyang.util;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


/**
 * MD5加密帮助类
 *
 * @author CaoHeYang
 * @date 20191221
 */
public class Md5Util {
    /**
     * 对传入的字符串进行MD5加密
     *
     * @param source 需要进行MD5加密的字符串 TODO Exception处理
     * @return 加密完成之后的32位MD5字符串
     */
    public static String getMd5(String source) {
        try {
            // 生成一个MD5加密计算摘要
            MessageDigest md = MessageDigest.getInstance("MD5");
            // 计算md5函数
            md.update(source.getBytes());
            // digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
            // BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
            return new BigInteger(1, md.digest()).toString(16);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5加密出现错误");
        }
    }
}

TinyUrlUtil.java

package com.caoheyang.util;

/**
 * 短链处理工具类
 *
 * @author CaoHeYang
 * @date 20191221
 */
public class TinyUrlUtil {

    /**
     * 对长链进行加密的得到6位短链编码
     *
     * @param url 长链
     * @return 短链编码
     */
    public static String shortUrl(String url) {
        //为防止重复,定义MD5加密的盐
        String key = "@(IfxHG#$";
        // 要使用生成 URL 的字符
        String[] chars = new String[]{"a", "b", "c", "d", "e", "f", "g", "h",
                "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
                "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5",
                "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H",
                "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
                "U", "V", "W", "X", "Y", "Z"

        };
        // 对传入网址进行 MD5 加密,并截取前8位,用作计算tinyUrl resourceId的种子
        String sourceUrlInfo = Md5Util.getMd5(url + key).substring(0, 8);
        // 这里需要使用 long 型来转换,因为 Inteper .parseInt() 只能处理 31 位 , 首位为符号位 , 如果不用 long ,则会越界
        long lHexLong = 0x3FFFFFFF & Long.parseLong(sourceUrlInfo, 16);
        String tinyUrl = "";
        for (int j = 0; j < 6; j++) {
            // 把得到的值与 0x0000003D 进行位与运算,取得字符数组 chars 索引
            long index = 0x0000003D & lHexLong;
            // 把取得的字符相加
            tinyUrl += chars[(int) index];
            // 每次循环按位右移 5 位
            lHexLong = lHexLong >> 5;
        }
        return tinyUrl;
    }
}

CorsConfig.java

package com.caoheyang.config;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * 服务器端的跨域配置类
 *
 * @author CaoHeYang
 * @date 20191221
 */
@Configuration
public class CorsConfig {

    private final Logger logger = LogManager.getLogger(this.getClass());

    /**
     * 服务器端的允许跨域设置
     * 当不存在 {@link org.springframework.web.filter.CorsFilter}的Bean时,激活用户自定义跨域配置
     *
     * @return 跨域请求bean
     */
    @Bean
    @ConditionalOnMissingBean(CorsFilter.class)
    public CorsFilter corsFilter(@Value("${tiny.url.website}") String tinyUrlWebsite) {

        logger.debug("Cors SET");
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        // 允许cookies跨域
        config.setAllowCredentials(true);
        // 允许向该服务器提交请求的URI,*表示全部允许。。这里尽量限制来源域,比如http://xxxx:8080 ,以降低安全风险。。
        config.addAllowedOrigin(tinyUrlWebsite);
        logger.debug(String.format("AllowedOrigin=%s", tinyUrlWebsite));
        // 允许访问的头信息,*表示全部
        config.addAllowedHeader("Content-Type");
        config.addAllowedHeader("X-Amz-Date");
        config.addAllowedHeader("Authorization");
        config.addAllowedHeader("X-Api-Key");
        config.addAllowedHeader("X-Amz-Security-Token");

        logger.debug(String.format("AllowedHeader=%s", config.getAllowedHeaders()));
        // 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
        config.setMaxAge(18000L);
        // 允许提交请求的方法,*表示全部允许,也可以单独设置GET、PUT等
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        logger.debug("Cors SET End");
        return new CorsFilter(source);
    }

}

StreamLambdaHandler.java

package com.caoheyang.serverless;


import com.amazonaws.serverless.exceptions.ContainerInitializationException;
import com.amazonaws.serverless.proxy.model.AwsProxyRequest;
import com.amazonaws.serverless.proxy.model.AwsProxyResponse;
import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler;
import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.caoheyang.Application;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Instant;

/**
 * AWS Lambda 集成代理请求处理类
 *
 * @author CaoHeYang
 * @date 20191221
 */
public class StreamLambdaHandler implements RequestStreamHandler {

    private static final Logger logger = LogManager.getLogger(StreamLambdaHandler.class);

    private static SpringBootLambdaContainerHandler handler;

    static {
        try {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);
            logger.warn("For applications that take longer than 10 seconds to start, use the async builder");
            // For applications that take longer than 10 seconds to start, use the async builder:
            long startTime = Instant.now().toEpochMilli();
            handler = new SpringBootProxyHandlerBuilder()
                    .defaultProxy()
                    .asyncInit(startTime)
                    .springBootApplication(Application.class)
                    .buildAndInitialize();
        } catch (ContainerInitializationException e) {
            // if we fail here. We re-throw the exception to force another cold start
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring Boot application", e);
        }
    }

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        handler.proxyStream(inputStream, outputStream, context);
    }
}

Jenkinsfile

@Library('shared-pipeline-library') _
JavaLambdaCI {
  project = 'tiny-url'
}

pom.xml



    4.0.0

    com.caoheyang
    tiny-url
    1.0-SNAPSHOT
    jar

    Serverless Spring Boot 2 API
    https://github.com/awslabs/aws-serverless-java-container

    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.8.RELEASE
    

    
        1.8
        1.8
    

    
        
            com.amazonaws.serverless
            aws-serverless-java-container-springboot2
            1.4
        

        
            org.springframework.boot
            spring-boot-starter-web
            
                
                    org.springframework.boot
                    spring-boot-starter-tomcat
                
            
        

        
            aws-java-sdk-dynamodb
            com.amazonaws
        

        
            junit
            junit
            4.12
            test
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            org.powermock
            powermock-api-easymock
            1.7.4
            test
        

        
            com.google.code.gson
            gson
            2.8.6
        
    
    
        
            
                com.amazonaws
                aws-java-sdk-bom
                1.11.386
                pom
                import
        
    
    
        
            shaded-jar
            
                
                    
                        org.apache.maven.plugins
                        maven-shade-plugin
                        2.3
                        
                            false
                        
                        
                            
                                package
                                
                                    shade
                                
                                
                                    
                                        
                                            org.apache.tomcat.embed:*
                                        
                                    
                                
                            
                        
                    
                
            
        
        
            assembly-zip
            
                true
            
            
                
                    
                    
                        org.apache.maven.plugins
                        maven-jar-plugin
                        3.1.1
                        
                            
                                default-jar
                                none
                            
                        
                        
                            ${project.artifactId}
                        
                    
                    
                    
                        org.apache.maven.plugins
                        maven-dependency-plugin
                        3.1.1
                        
                            
                                copy-dependencies
                                package
                                
                                    copy-dependencies
                                
                                
                                    ${project.build.directory}${file.separator}lib
                                    runtime
                                
                            
                        
                    
                    
                        org.apache.maven.plugins
                        maven-assembly-plugin
                        3.1.0
                        
                            
                                zip-assembly
                                package
                                
                                    single
                                
                                
                                    ${project.artifactId}
                                    
                                        src${file.separator}assembly${file.separator}bin.xml
                                    
                                    false
                                
                            
                        
                    
                
            
        
    

swagger.yaml

openapi: "3.0.1"
info:
  title: "TinyUrlGW"
  version: "2019-12-27T01:21:13Z"
servers:
  - url: "https://fpzox03era.execute-api.us-east-1.amazonaws.com/{basePath}"
    variables:
      basePath:
        default: "/beta"
paths:
  /{resource}:
    options:
      parameters:
        - name: "resource"
          in: "path"
          required: true
          schema:
            type: "string"
            description: "短链编码"
      responses:
        301:
          description: "301 response"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
            Access-Control-Allow-Methods:
              schema:
                type: "string"
            Access-Control-Allow-Headers:
              schema:
                type: "string"
          content: {}
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:444668010846:function:tiny-url-lambda/invocations"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
    x-amazon-apigateway-any-method:
      parameters:
        - name: "resource"
          in: "path"
          required: true
          schema:
            type: "string"
            description: "短链编码"
      responses:
        301:
          description: "301 response"
          content: {}
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:444668010846:function:tiny-url-lambda/invocations"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
  /data/shorten:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateShortUrlDomain"
        required: true
      responses:
        200:
          description: "200 response"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShortUrlDomain"
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:444668010846:function:tiny-url-lambda/invocations"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
    options:
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateShortUrlDomain"
        required: true
      responses:
        200:
          description: "200 response"
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: "string"
            Access-Control-Allow-Methods:
              schema:
                type: "string"
            Access-Control-Allow-Headers:
              schema:
                type: "string"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShortUrlDomain"
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:444668010846:function:tiny-url-lambda/invocations"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        contentHandling: "CONVERT_TO_TEXT"
        type: "aws_proxy"
components:
  schemas:
    ShortUrlDomain:
      type: "object"
      properties:
        shortUrl:
          type: "string"
          description: "短链URL"
    GenerateShortUrlDomain:
      type: "object"
      properties:
        longUrl:
          type: "string"
          description: "长链URL"

你可能感兴趣的:(2020-01-03)