2. spring-boot+thymeleaf(+vuejs)
上一篇其实是简单入门,全是废话,实战开始。友情提示:这篇文章有点长
目前没有发现类似nodejs
里面init
功能的关于spring-boot
的工具,推荐还是去github上面clone
一个吧,方便快捷,也可使用start生成,贡献网址http://start.spring.io/。本文旨在这个目的构建一个仓库供以后使用,目标:
- view层用
thymeleaf
替代jsp - 前端js框架采用
vuejs
- 添加国际化
- 修改banner
- DAO层采用JPA,配置数据库
- 初始化数据
- 添加基础权限认证并且能够实现根据需要简单定制
在上篇项目的基础上修改目录如下:
Paste_Image.png
修改build.gradle
buildscript {
ext {
springBootVersion = '1.3.5.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'spring-boot'
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'war'
version = '0.1'
sourceCompatibility = 1.8
targetCompatibility = 1.8
[compileJava,compileTestJava]*.options*.encoding = 'UTF-8'
repositories {
mavenCentral()
}
dependencies {
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.springframework.boot:spring-boot-starter-data-jpa'
compile 'org.springframework.boot:spring-boot-starter-security'
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-devtools'
// compile 'mysql:mysql-connector-java:5.1.34'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
runtime 'org.hsqldb:hsqldb'
testCompile 'org.springframework.boot:spring-boot-starter-test'
// testRuntime 'org.hsqldb:hsqldb'
}
task wrapper(type: Wrapper) {
gradleVersion = '2.13'
}
//applicationDefaultJvmArgs = [ "-Xmx3550m","-Xms3550m","-Xmn2g","-Xss256k"]
使用hsqldb
只是用于方便测试,记得抹掉,环境采用jdk8
(因为最近在整react-native
不想切环境),application.properties
就一行代码spring.profiles.active=dev
,看字面意思应该就懂了,发布的时候记得改成对应名字即可比如pro
,开发环境配置文件application-dev.properties
:
# dev env
server.port=8090
# Thymeleaf view template config
# disable cache for dev
spring.thymeleaf.cache=false
# basic security
security.basic.enabled=false
# message resource config
# if true use system local and false to use baseName (e.g. 'messages')
spring.messages.fallback-to-system-locale=false
# datasource
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto=update
#spring.datasource.continueOnError=true
#spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
#spring.jpa.database=MySQL
#spring.jpa.show-sql=true
#
#spring.datasource.url=jdbc:mysql://server:3306/dbname?useUnicode=true&characterEncoding=UTF-8&connectTimeout=60000&socketTimeout=60000&autoReconnect=true&autoReconnectForPools=true&failOverReadOnly=false
#spring.datasource.username=name
#spring.datasource.password=pass
#spring.datasource.driverClassName=com.mysql.jdbc.Driver
#spring.datasource.sqlScriptEncoding=utf-8
#
#spring.datasource.max-active=100
#spring.datasource.max-idle=8
#spring.datasource.min-idle=8
#spring.datasource.initial-size=30
#spring.datasource.validation-query=select 1
#spring.datasource.test-on-borrow=true
注释的部分是举例一般mysql
数据库配置,请不要忽视spring.datasource.url
后面的一堆参数,懂的朋友即懂,不懂的朋友一时半会也解释不清,大概意思就是保持数据库连接池通畅不然会出现一个bug:跑得好好的项目不间断时间莫名挂掉
,参数是需要修改的,请自行google。
templates/index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:fragment="head">
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/>
<script src="http://cdnjs.cloudflare.com/ajax/libs/vue/1.0.24/vue.min.js"></script>
<title th:text="#{app.title}">Magneto</title>
</head>
<body>
<nav class="navbar navbar-inverse" th:fragment="header">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/" th:text="#{app.title}"></a>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="/">Home <span class="sr-only">(current)</span></a></li>
<li><a href="/user/info">User</a></li>
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div id="app" class="container">
<span th:text="#{app.title}"></span>
{{message}}
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Test Vue.js!'
}
})
</script>
</body>
</html>
使用vuejs
以及bootstrap
,请自行更换。注意th:fragment
声明模版块,也可另新建文件比如layouts/head.html
,举例用法user/info.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="index::head"></head>
<body>
<nav th:replace="index::header"></nav>
<div class="container">
<span th:text="${name}"></span>
</div>
</body>
</html>
先把次要的讲完,banner.txt
可以替换彩蛋,好人做到底,给你地址http://patorjk.com/software/taag,messages.properties
国际化app.title=Magneto
,config/ServletInitializer.java
是给要war
的同学,也可以在Application.java
中直接继承SpringBootServletInitializer
,不然打出的war
包在tomcat底下是跑不起来的,而你根本不知道出错在哪里,这是个大坑,在spring-boot
以前的版本文档里是没有显示的说明的,坑了我很久。
数据库持久层JPA
现在大部分同学用的是Mybatis
,而为什么我要在这里用上JPA
?我是这样想的:Mybatis
的确对于可控的复杂的业务逻辑很擅长,抛开其他不讲,无论是效率还是从需求的角度来说的确比JPA
更加适用于现在复杂多变的项目业务需要,但是在中小项目里这种区别并不是那么的大,讲道理,现在NoSQL
怎么盛行,sql
存储的压力并没有想象中那么大,如果真有那么大也不是Mybatis
或jpa
就可以解决的,我宁愿花钱再买个服务器或者做做数据库优化。考虑到使用spring-boot
,我觉得Mybatis
的设计逻辑并不契合,相对来说,JPA
更加方便,所以选用JPA
做DAO
层的工作,当然了,如果你厌倦了hibernate
式的各种表连接的不痛快,集成Mybatis
也是很简单的,参考这篇文章(这篇文章已经够长了,这里就不赘述了)
User
实体类:
@Entity
@DynamicUpdate
public class User implements Serializable, UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String role;
...
UserRepo
继承JPA
:
public interface UserRepo extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
通用service
接口ICommonService
public interface ICommonService<T> {
T save(T entity) throws Exception;
void delete(Long id) throws Exception;
void delete(T entity) throws Exception;
T findById(Long id);
T findBySample(T sample);
List<T> findAll();
List<T> findAll(T sample);
Page<T> findAll(PageRequest pageRequest);
Page<T> findAll(T sample, PageRequest pageRequest);
}
最终业务service
--UserService.java
@Service
public class UserService implements IUserService, UserDetailsService {
@Autowired
private UserRepo userRepo;
@Override
public User save(User entity) throws Exception {
return userRepo.save(entity);
}
@Override
public void delete(Long id) throws Exception {
userRepo.delete(id);
}
@Override
public void delete(User entity) throws Exception {
userRepo.delete(entity);
}
@Override
public User findById(Long id) {
return userRepo.findOne(id);
}
@Override
public User findBySample(User sample) {
return userRepo.findOne(whereSpec(sample));
}
@Override
public List<User> findAll() {
return userRepo.findAll();
}
@Override
public List<User> findAll(User sample) {
return userRepo.findAll(whereSpec(sample));
}
@Override
public Page<User> findAll(PageRequest pageRequest) {
return userRepo.findAll(pageRequest);
}
@Override
public Page<User> findAll(User sample, PageRequest pageRequest) {
return userRepo.findAll(whereSpec(sample), pageRequest);
}
private Specification<User> whereSpec(final User sample){
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (sample.getId()!=null){
predicates.add(cb.equal(root.<Long>get("id"), sample.getId()));
}
if (StringUtils.hasLength(sample.getUsername())){
predicates.add(cb.equal(root.<String>get("username"),sample.getUsername()));
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
};
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User sample = new User();
sample.setUsername(username);
User user = findBySample(sample);
if( user == null ){
throw new UsernameNotFoundException(String.format("User with username=%s was not found", username));
}
return user;
}
}
whereSpec
方法是使用Specification
做通用封装,没有使用泛型来更加通用,我觉得这样已经差不多了吧,要求不要太高,看字面意思应该能懂是在做什么,不多说,还是那句话--不懂的自己谷歌。大概生成的sql
可能是select u from user u where u.id=? and u.username=?
最难的权限部分
对于权限的详细说明会在下面的文章里介绍,这里只取一般而言需要注册登录模块的同学,集成这一部分是因为这是90%的项目都会使用的方式,故为之。
spring-boot
采用spring-security
做权限的验证工作,不了解的同学自己谷歌吧。
基础配置WebSecurityConfig.java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// Configure spring security's authenticationManager with custom
// user details service
auth.userDetailsService(this.userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
.and()
.httpBasic()
;
}
}
配置userDetailsService
将用户管理转交给我们自己,因为我觉得spring
自己的那套不一定适于用一般项目,因为一般项目的User
表一般会和业务关系比较紧密,设计初衷一定优先考虑自己的业务而不是框架,HttpSecurity
做权限配置,看字面意思应该就懂了,其他一般配置参考这篇文章。自己写就需要码更多代码了,依次需要实现的接口如下:
UserService.java
继承UserDetailsService
重写loadUserByUsername
:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User sample = new User();
sample.setUsername(username);
User user = findBySample(sample);
if( user == null ){
throw new UsernameNotFoundException(String.format("User with username=%s was not found", username));
}
return user;
}
从代码中不难看出,spring-security
内部使用的user
是封装过的UserDetails
,所以User.java
修改如下:
@Entity
@DynamicUpdate
public class User implements Serializable, UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String role;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority(getRole()));
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
}
从代码可以看出isAccountNonExpired
、isAccountNonLocked
、isCredentialsNonExpired
、isEnabled
、getAuthorities
重写这几个方法就可以根据自己的业务逻辑做更细致的权限管理,即是简单定制。
光说不练不是好选手,实际运行效果图:
登录页面: Paste_Image.png
我知道实际项目肯定不是这样,这里是最基础的登陆示范,自定义登录页面只需要在修改上文提到的
HttpSecurity
即可,并不难,就当家庭作业了。权限user/info页面
Paste_Image.png
最后附上github地址https://github.com/kaenry/spring-boot-magneto.git