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
* <p>
* 1、We use direct @Import instead of @ComponentScan to speed up cold starts
* 2、ComponentScan(basePackages = "com.caoheyang.controller")
* </p>
* @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<AwsProxyRequest, AwsProxyResponse> 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
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.caoheyang</groupId>
<artifactId>tiny-url</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Serverless Spring Boot 2 API</name>
<url>https://github.com/awslabs/aws-serverless-java-container</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.amazonaws.serverless</groupId>
<artifactId>aws-serverless-java-container-springboot2</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>aws-java-sdk-dynamodb</artifactId>
<groupId>com.amazonaws</groupId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-easymock</artifactId>
<version>1.7.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-bom</artifactId>
<version>1.11.386</version>
<type>pom</type>
<scope>import</scope></dependency>
</dependencies>
</dependencyManagement>
<profiles>
<profile>
<id>shaded-jar</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>org.apache.tomcat.embed:*</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>assembly-zip</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<!-- don't build a jar, we'll use the classes dir -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>default-jar</id>
<phase>none</phase>
</execution>
</executions>
<configuration>
<finalName>${project.artifactId}</finalName>
</configuration>
</plugin>
<!-- select and copy only runtime dependencies to a temporary lib folder -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}${file.separator}lib</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>zip-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<finalName>${project.artifactId}</finalName>
<descriptors>
<descriptor>src${file.separator}assembly${file.separator}bin.xml</descriptor>
</descriptors>
<attach>false</attach>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
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"