- Published on
Java Springboot Boilerplate
- Authors
- Name Ezidev
join our community
Boilerplate for Springboot
==========
Github Repo:
melthaw
Overview
The quick development boilerplate based on Spring (Boot) Framework which covers the general case of Java backend application something like Account Module, Security Foundation, Audit System, File Upload/Download, Message Notification, Role Based Access Control etc.
And we also provide the CRUD sample to show how manage the item list , add new one item , update it or remove it.
We hope this boilerplate can help the users to focus on their business part . Before that , we will explain how it is designed and implemented.
Get Started
Please make sure the Java 8, Gradle 2.x and Mongodb are installed on your development machine.
We support Spring Cloud now, here is the User Guide - Spring Cloud Support
Build
Build using Gradle
gradle clean build
Start up - by Docker
cd openapi
docker-compose build
docker-compose up -d
Start up - by Manual
Start API Server
And here is the minimized application.properties to start the boilerplate.
app.name=spring-backend-boilerplate
app.description=spring-backend-boilerplate
in.clouthink.daas.sbb.account.password.salt=@account.sbb.daas.clouthink.in
in.clouthink.daas.sbb.account.administrator.email=changeit@example.com
in.clouthink.daas.sbb.account.administrator.username=administrator
in.clouthink.daas.sbb.account.administrator.cellphone=13000000000
in.clouthink.daas.sbb.account.administrator.password=Please_change_the_pwd
in.clouthink.daas.sbb.setting.system.name=spring-backend-boilerplate
in.clouthink.daas.sbb.setting.system.contactEmail=support-team@example.com
in.clouthink.daas.sbb.setting.system.contactPhone=13000000000
logging.file=/var/sbb/log/server.log
logging.level.*=INFO
logging.level.in.clouthink.daas=DEBUG
server.port=8081
server.address=127.0.0.1
server.session-timeout=360000
spring.mvc.date-format=yyyy-MM-dd
spring.mvc.favicon.enabled=false
multipart.enabled=true
multipart.max-file-size=20Mb
multipart.max-request-size=20Mb
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss.sss'Z'
spring.data.mongodb.uri=mongodb://localhost:27017/spring-backend-boilerplate
Then we can start it with
> cd openapi/server
> gradle clean bootRun -PjvmArgs="-Dspring.config.location=the_full_path_of_the_application.properties"
Start ApiDoc Server
And here is the minimized application.properties to start the boilerplate.
app.name=spring-backend-boilerplate-api-doc
app.description=spring backend boilerplate api doc
server.port=8082
server.address=127.0.0.1
server.session-timeout=360000
Then we can start it with
> cd openapi/doc
> gradle clean bootRun -PjvmArgs="-Dspring.config.location=the_full_path_of_the_application.properties"
After the swagger doc server booted, open browser and visit the api doc at
http://127.0.0.1:8082/swagger-ui.html
Features
Modularization
First we design a modularized system (Thanks Spring Boot and Gradle), our goal is to simple add or remove one module without changing the Application , except the foundational modules.
All these come to reality are belongs to the features of Spring Boot Starter.
- Provide the auto configuration for each module.
- Tell spring how to enable the auto configuration.
Example
Message module for example
Auto Configuration
@Configuration
@Import({MockSmsModuleConfiguration.class, SmsHistoryModuleConfiguration.class})
public class DummySmsRestModuleConfiguration {
}
Starter by spring.factories
#message/sms/starter/src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
in.clouthink.daas.sbb.sms.DummySmsRestModuleConfiguration
Practise
Remove the message module in the boilerplate won't break the application, because the message is event-driven , The other modules dispatch event to the event bus, the event bus simply discard it if the message module not found.
//openapi/server/build.gradle
dependencies {
...
compile project(':sample/setting/starter')
compile project(':message/sms/starter')
compile project(':storage/starter')
...
}
Simply remove the module configuration from the startup script.
//openapi/server/build.gradle
dependencies {
...
compile project(':sample/setting/starter')
//after
//compile project(':message/sms/starter')
compile project(':storage/starter')
...
}
Without Starter
And we also force the module convention that's separating the abstraction and implementation, then your can switch from one implementation to another easily.
For example, we provide more than one implementation module for the storage abstraction (:storage/core).
@Import({GridfsModuleConfiguration.class})
public class StorageRestModuleConfiguration {
}
If you want upload the file and save it to the local storage ( your runtime server's file system ), just import another one and replace it as follow
@Import({LocalStorageModuleConfiguration.class})
public class StorageRestModuleConfiguration {
}
Security
Foundation
Spring Security is a powerful security framework , it's easy to customize and extend . Based on the flexibility provided by Spring Security , we add more interesting feature to it like multi-factor authentication, user device, audit and pluggable account system.
Context
Yes, Spring Security provides the context named org.springframework.security.core.context.SecurityContext
and the corresponding helper class org.springframework.security.core.context.SecurityContextHolder
.
Why we need another one?
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw some exception here?
}
Every time we retrieve the authentication , we must check the authentication , duplicated code everywhere.
What we supposed is that an exception is thrown automatically if no authentication found in the context,
not check it and throw it by manual.
So we provide one new generic interface in module(:security/core)
public interface SecurityContext<T> {
/**
* @return current user , or null if not authenticated
*/
T currentUser();
/**
* @return the current user
* @throw AuthenticationRequiredException if not authenticated
*/
T requireUser();
}
And we provide one helper named SecurityContexts
to load the implementation. Here is the sample:
User user = (User)SecurityContexts.getContext().requireUser();
As you see, requireUser()
is invoked which means if no authenticated user found in the context, it will throw one AuthenticationRequiredException.
The SecurityContexts
requires that the implementation must follow the Java SPI Spec and provide it in the file as follow
META-INF/services/in.clouthink.daas.sbb.security.SecurityContext
Authentication & Authorization
First let's list the extension points what we implemented for Spring Security.
User
- org.springframework.security.authentication.AuthenticationProvider
- org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider
- org.springframework.security.core.userdetails.User
- org.springframework.security.core.userdetails.UserDetailsService
Login & Logout
- org.springframework.security.web.AuthenticationEntryPoint
- org.springframework.security.web.authentication.AuthenticationFailureHandler
- org.springframework.security.web.authentication.AuthenticationSuccessHandler
- org.springframework.security.web.authentication.logout.LogoutSuccessHandler
Access
- org.springframework.security.web.access.AccessDeniedHandler
- org.springframework.security.web.access.AccessDeniedHandlerImpl
- org.springframework.security.access.expression.SecurityExpressionHandler
- org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler
- org.springframework.security.web.access.expression.WebSecurityExpressionRoot
The backend is designed as a restful service provider, so the security foundation must has the ability to process the restful request and response , not only the web page request and response.
And our implementations are listed as follow:
User
- in.clouthink.daas.sbb.security.impl.spring.UserDetailsAuthenticationProviderImpl
- in.clouthink.daas.sbb.security.impl.spring.UserDetails
- in.clouthink.daas.sbb.security.impl.spring.UserDetailsServiceImpl
Login & Logout
- in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationEntryPointRestImpl
- in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationFailureHandlerRestImpl
- in.clouthink.daas.sbb.security.impl.spring.rest.AuthenticationSuccessHandlerRestImpl
- in.clouthink.daas.sbb.security.impl.spring.rest.LogoutSuccessHandlerRestImpl
Access
- in.clouthink.daas.sbb.security.impl.spring.rest.AccessDeniedHandlerRestImpl
- in.clouthink.daas.sbb.rbac.impl.spring.security.RbacWebSecurityExpressionHandler
- in.clouthink.daas.sbb.rbac.impl.spring.security.RbacWebSecurityExpressionRoot
How to configure Spring Security
Please refer to in.clouthink.daas.sbb.openapi.OpenApiSecurityConfigurer
.
First , export the Spring Security extension points implementation.
@Bean
public AuthenticationProvider authenticationProvider() {
return new UserDetailsAuthenticationProviderImpl();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandlerImpl() {
return new AuthenticationSuccessHandlerRestImpl();
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandlerImpl() {
return new AuthenticationFailureHandlerRestImpl();
}
@Bean
public AccessDeniedHandler accessDeniedHandlerImpl() {
return new AccessDeniedHandlerRestImpl();
}
@Bean
public LogoutSuccessHandler logoutSuccessHandlerImpl() {
return new LogoutSuccessHandlerRestImpl();
}
@Bean
public AuthenticationEntryPoint authenticationEntryPointImpl() {
return new AuthenticationEntryPointRestImpl();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList<>();
decisionVoters.add(new RoleVoter());
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(webExpressionVoter());
return new AffirmativeBased(decisionVoters);
}
@Bean
public WebExpressionVoter webExpressionVoter() {
WebExpressionVoter result = new WebExpressionVoter();
result.setExpressionHandler(rbacWebSecurityExpressionHandler());
return result;
}
@Bean
public SecurityExpressionHandler rbacWebSecurityExpressionHandler() {
return new RbacWebSecurityExpressionHandler();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider())
.eraseCredentials(true)
.userDetailsService(userDetailsService());
}
Then configure the authentication part
private void configLogin(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authenticationSuccessHandlerImpl())
.failureHandler(authenticationFailureHandlerImpl())
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandlerImpl())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.rememberMe()
.key("PLEASE_CHANGE_THIS");
}
Finally configure the authorization part
private void configAccess(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();
http.authorizeRequests()
.accessDecisionManager(accessDecisionManager())
.antMatchers("/", "/static/**", "/login**", "/guest/**")
.permitAll()
.antMatchers("/api/shared/**")
.hasRole("USER")
.antMatchers("/api/_devops_/**")
.hasRole("ADMIN")
.antMatchers("/api/**")
.access("passRbacCheck")
.and()
.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPointImpl())
.accessDeniedHandler(accessDeniedHandlerImpl());
}
Audit
The daas-audit is a simple and quick audit abstraction lib for spring mvc http request. Please go https://github.com/melthaw/spring-mvc-audit to get more detail about it. Here we only explain what we extended and customized.
The module(:audit/impl) implements the daas-audit's AuditEvent SPI including:
- in.clouthink.daas.audit.core.MutableAuditEvent
- in.clouthink.daas.sbb.audit.domain.model.AuditEvent
- in.clouthink.daas.audit.spi.AuditEventPersister
- in.clouthink.daas.sbb.audit.spiImpl.AuditEventPersisterImpl
And the login history is supported which is not covered in daas-audit.
- in.clouthink.daas.sbb.audit.domain.model.AuthEvent
- in.clouthink.daas.sbb.audit.service.AuthEventService
Here is the configuration to enable the audit module
@EnableAudit
public class SpringBootApplication extends SpringBootServletInitializer {
@Bean
public AuditEventPersister auditEventPersisterImpl() {
return new AuditEventPersisterImpl();
}
@Bean
public AuditConfigurer auditConfigurer() {
return result -> {
result.setSecurityContext(new SecurityContextAuditImpl());
result.setAuditEventPersister(auditEventPersisterImpl());
result.setErrorDetailRequired(true);
};
}
public static void main(String[] args) {
AuditRestModuleConfiguration.class,
...
SpringBootApplication.class
}
}
File Storage
The daas-fss is APIs which make storing the blob file easy and simple. Please go https://github.com/melthaw/spring-file-storage-service to get more detail about it. Here we only explain what we extended and customized.
Now we provide three file storage implementation
- aliyun oss (:storage/alioss)
- mongodb gridfs (:storage/gridfs)
- local file system (:storage/localfs)
Because different storage service stores the file in different system , the download url goes to different as well.
Here is the abstraction we supplied to extend.
- in.clouthink.daas.sbb.storage.spi.DownloadUrlProvider
For example, if you choose the :storage/localfs , the download url goes to:
public class LocalfsDownloadUrlProvider implements DownloadUrlProvider {
@Autowired
private LocalfsConfigureProperties localfsConfigureProperties;
@Autowired
private FileObjectService fileObjectService;
@Override
public String getDownloadUrl(String id) {
FileObject fileObject = fileObjectService.findById(id);
if (fileObject == null) {
throw new FileNotFoundException(id);
}
return localfsConfigureProperties.getDowloadUrlPrefix() + fileObject.getFinalFilename();
}
}
Here is the configuration to enable the Storage
module
public class SpringBootApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
StorageModuleConfiguration.class,
...
SpringBootApplication.class
}
}
Message
The daas-edm is a lightweight event driven message framework based on reactor. Please go https://github.com/melthaw/spring-event-driven-message to get more detail about it. Here we only explain what we extended and customized.
SMS
In the boilerplate we choose the aliyun SMS as example . It's simple and easy to integrate your SMS provider , just implement the following interface.
in.clouthink.daas.edm.sms.SmsSender
We also supply one dummy module(:message/sms/mock) which can be used in the development ENV.
//for development
@Import({SmsAliyunModuleConfiguration.class})
//for production
@Import({DummySmsModuleConfiguration.class})
One more thing, the SMS history can be saved if you import the history module (:message/sms/history). By default we enable the SMS history in the boilerplate, if you don't like it, simple remove the import part to disable this feature.
@Configuration
@Import({SmsHistoryModuleConfiguration.class})
public class SmsRestModuleConfiguration {
}
Here is the configuration to enable the Message
module
public class SpringBootApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
SmsRestModuleConfiguration.class,
...
SpringBootApplication.class
}
}
Configuration
application.properties
account
in.clouthink.daas.sbb.account.password.salt=
in.clouthink.daas.sbb.account.administrator.email=
in.clouthink.daas.sbb.account.administrator.username=
in.clouthink.daas.sbb.account.administrator.cellphone=
in.clouthink.daas.sbb.account.administrator.password=
storage
#alioss
in.clouthink.daas.sbb.storage.alioss.keyId=
in.clouthink.daas.sbb.storage.alioss.secret=
in.clouthink.daas.sbb.storage.alioss.ossDomain=
in.clouthink.daas.sbb.storage.alioss.imgDomain=
in.clouthink.daas.sbb.storage.alioss.defaultBucket=
in.clouthink.daas.sbb.storage.alioss.buckets.key1=
in.clouthink.daas.sbb.storage.alioss.buckets.key2=
sms
in.clouthink.daas.sbb.sms.aliyun.area=
in.clouthink.daas.sbb.sms.aliyun.accessKey=
in.clouthink.daas.sbb.sms.aliyun.accessSecret=
in.clouthink.daas.sbb.sms.aliyun.signature=
in.clouthink.daas.sbb.sms.aliyun.smsEndpoint=
in.clouthink.daas.sbb.sms.aliyun.templateId=
setting
in.clouthink.daas.sbb.setting.system.name=
in.clouthink.daas.sbb.setting.system.contactEmail=
in.clouthink.daas.sbb.setting.system.contactPhone=
resource
The resource is the term we called in RBAC, which is protected by authorization system. The resource should be a rest endpoint,or the visible menu item in the GUI , even a Create button in a page.
We design a resource registry SPI as
- in.clouthink.daas.sbb.rbac.spi.ResourceProvider
All the resource provider implementation only required to implement it as a spring bean. The boilerplate scans and discovers it , and register the resources automatically.
The classic resource is the menu and action which are granted to the role and accessed control by role permission. We design the pluggable menu & action module , and supply the annotation to make it easy to use.
Here is the example
@EnableMenu(pluginId = "plugin:menu:sample",
extensionPointId = Menus.ROOT_EXTENSION_POINT_ID,
menu = {@Menu(virtual = true,
code = "menu:dashboard:sample",
name = "sample",
order = 100,
metadata = {@Metadata(key = "icon", value = "fa fa-gear")},
extensionPoint = {@ExtensionPoint(id = "extension:menu:sample")}),
@Menu(virtual = true,
code = "menu:dashboard:system",
name = "system",
order = 200,
metadata = {@Metadata(key = "icon", value = "fa fa-gear")},
extensionPoint = {@ExtensionPoint(id = "extension:menu:system")}),
})
More detail about the usage please check out the description of the Java file.
Sample - new business module
TODO
Appendix - Development ENV
IDEA - how to import the project to IDEA IDE
> gradle cleanIdea
> gradle idea