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/taagmessages.properties国际化app.title=Magnetoconfig/ServletInitializer.java是给要war的同学,也可以在Application.java中直接继承SpringBootServletInitializer,不然打出的war包在tomcat底下是跑不起来的,而你根本不知道出错在哪里,这是个大坑,在spring-boot以前的版本文档里是没有显示的说明的,坑了我很久。


数据库持久层JPA

现在大部分同学用的是Mybatis,而为什么我要在这里用上JPA?我是这样想的:Mybatis的确对于可控的复杂的业务逻辑很擅长,抛开其他不讲,无论是效率还是从需求的角度来说的确比JPA更加适用于现在复杂多变的项目业务需要,但是在中小项目里这种区别并不是那么的大,讲道理,现在NoSQL怎么盛行,sql存储的压力并没有想象中那么大,如果真有那么大也不是Mybatisjpa就可以解决的,我宁愿花钱再买个服务器或者做做数据库优化。考虑到使用spring-boot,我觉得Mybatis的设计逻辑并不契合,相对来说,JPA更加方便,所以选用JPADAO层的工作,当然了,如果你厌倦了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;
    }
}

从代码可以看出isAccountNonExpiredisAccountNonLockedisCredentialsNonExpiredisEnabledgetAuthorities重写这几个方法就可以根据自己的业务逻辑做更细致的权限管理,即是简单定制。
光说不练不是好选手,实际运行效果图:

Paste_Image.png

登录页面:

Paste_Image.png

我知道实际项目肯定不是这样,这里是最基础的登陆示范,自定义登录页面只需要在修改上文提到的HttpSecurity即可,并不难,就当家庭作业了。
权限user/info页面
Paste_Image.png

最后附上github地址https://github.com/kaenry/spring-boot-magneto.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容