Introduction
In this post, we report our experience of setting up a new website such as Sample Size Calculator.
Related Readings
Backend
The backend is implemented using Spring Boot. It's quite straightforward to set up a running application. We choose Freemarker as our template engine and use Spring Security to handle web security related tasks.
One of the challenges we ran into is to set up the CSRF. It consists of two parts:
- Enable CSRF protection in Spring Security.
- Inject the CSRF token into the web page.
The first task is straightforward. The second task requires some work.
First, we need to get the csrf token from the request attribute. Here is a utility class that extracts the token injected by Spring Security:
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.reactive.result.view.CsrfRequestDataValueProcessor;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
@Slf4j
public final class CsrfUtils {
public static class EmptyCsrfToken implements CsrfToken {
@Override
public String getHeaderName() {
return "X-CSRF-TOKEN";
}
@Override
public String getParameterName() {
return "_csrf";
}
@Override
public String getToken() {
return "";
}
}
/**
* Gets the current csrf token.
*
* @return The CSRF token if there is one injected by the spring security. Otherwise, return an instance of
* {@link EmptyCsrfToken}.
*/
public static CsrfToken getCsrfToken() {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
Object x = attr.getAttribute(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, RequestAttributes.SCOPE_REQUEST);
if (x instanceof CsrfToken token) {
return token;
} else {
return new EmptyCsrfToken();
}
}
}
Once we have the token, we need to add it to the model and pass it to the template engine:
@RequestMapping(value = "/", method = RequestMethod.GET)
public String homepage(@ModelAttribute("model") final ModelMap model) {
final CsrfToken token = CsrfUtils.getCsrfToken();
model.addAttribute(token.getParameterName(), token);
return "home_page";
}
Now in the template, we add two meta tags:
<head>
<meta name="_csrf_header" content="${(model._csrf.getHeaderName())}"/>
<meta name="_csrf_token" content="${(model._csrf.getToken())}"/>
</head>
At this point, the csrf token is available on the html page and the front-end client needs to include this token in the requests it sends to the server.
Data Layer
We use MySQL to store application data. We use Spring Data JDBC to handle the interaction between the application and the database. The mapping between data models and java objects is also handled by Spring Data JDBC.
At this stage, we decide not to use Hibernate. This means we need to
- Manually converts
java.sql.ResultSet
to java objects. - Track the table dependencies.
The first task means we need to implement some RowMapper
s and the second task means we need to use @DependsOn
annotation to manually mark the dependencies between different (table or data model) beans.
Front-end
We use React to generate the content on the web page and use Axios to send requests from the browser to the server. Chart.js is used to generate graphs.
Because React operates in its own world, if we want to use other libraries, we need to use the react version of them. For example, the plain Char.js does not work seamlessly with our React code and we need to use react-chartjs instead.
To improve the debugging experience with React, we need to enable the source map. This can be done by adding the line below to the webpack.config.js
file:
module.exports={
// ...
devtool: "source-map"
// ...
}
Notice that Chart.js
draws images on a canvas and it's a known issue that the drawing can be blurry. Setting devicePixelRatio
to 4
helps but it does not solve the problem entirely.
Website Hosting and Domain
We registered our domain at Namecheap and the application is hosted on an AWS EC2 instance. DNS configuration is done in AWS Route53.
We manage SSL/TLS certificates via Let's Encrypt and we also enabled https connection.
Build and Deployment
We need to build the code because React code needs to be compiled and we don't want to push compiled code to our repository. We use AWS CodeBuild to build our code and generate a jar
file. The jar
file is uploaded to S3 and is deployed by AWS CodeDeploy.
One thing to notice about CodeDeploy is it provides many hooks. The ApplicationStop
is used to stop the application. Suppose we have a bug in the ApplicationStop related configuration in the appspec.yml
file or in the referenced script in code commit 1 and we make a fix in code commit 2. When the commit 2 is deployed and CodeDeploy tries to stop the application, it uses configurations and scripts from commit 1. This means deploying a bug fix for appspec.yml
cannot solve the issue. Instead, we need to manually fix the issue on the host directly.
Another thing to notice is that we need to request for service limit increase to use CodeBuild and CodeDeploy. You can make the request through AWS Service Quotas
. These two services require access to on-demand computing resources so you need to make sure you have the appropriate limits on those resources.
Metrics and Monitoring
It's crucial to monitor the state of the application. Many tools and services are available. In our case, we use Micrometer to record and publish metrics. Micrometer provides vendor-neutral APIs that allow us to write monitoring system-agnostic code. We choose CloudWatch as our monitoring system because we already use many AWS services to support this website.
It requires minimal configuration to set up Cloudwatch using Micrometer. The Micrometer website provides the instruction. See more details in Micrometer Cloudwatch.
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-cloudwatch2</artifactId>
<version>1.10.2</version>
</dependency>
It seems that there is a bug in the implementation of CloudWatchConfig
: it cannot load properties from application.properties
file.
There are two options to solve the issue:
- Override the
get(String s)
method - Override the getter methods such as
namespace()
.
If you want to use option 1, make sure you print out the parameter string s
and verify it has the expected value. You may need to the prefix()
method too otherwise the properties listed in Micrometer support in Spring Cloud AWS may not be picked. up.
If you use option 2, your code may look like the one below:
@Bean
public MeterRegistry createMeterRegistry(
@Value("${management.metrics.export.cloudwatch.namespace}") final String namespace,
@Value("${management.metrics.export.cloudwatch.stepInSec}") final int stepInSec) {
log.info("CloudWatchMeterRegistry bean is created with namespace={}, stepInSec={}", namespace, stepInSec);
final CloudWatchConfig micrometerCloudWatchConfig = new CloudWatchConfig() {
@Override
public String get(String s) {
return null;
}
@Override
public Duration step() {
return Duration.ofSeconds(stepInSec);
}
@Override
public String namespace() {
return namespace;
}
};
final CloudWatchAsyncClient client = CloudWatchAsyncClient.builder()
.credentialsProvider(this.awsCredentialsProvider)
.build();
return new CloudWatchMeterRegistry(micrometerCloudWatchConfig, Clock.SYSTEM, client);
}
----- END -----
©2019 - 2023 all rights reserved