2. spring-boot+thymeleaf(+vuejs)

作者: kaenry | 来源:发表于2016-06-28 17:23 被阅读22509次

    上一篇其实是简单入门,全是废话,实战开始。友情提示:这篇文章有点长

    目前没有发现类似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

    相关文章

      网友评论

      • 狂奔的蜗牛QAQ:大佬 为什么我的thymeleaf和element ui用了element ui就没有js效果了呢?
      • 169537fe92e8:不错不错,收藏了。

        推荐下,分库分表中间件 Sharding-JDBC 源码解析 17 篇:http://www.yunai.me/categories/Sharding-JDBC/?jianshu&401


      • weixk:“大概生成的sql可能是select u from user u where u.id=? and u.username=?”,那个语句的风格应该是hql吧
        kaenry:@weixk 是的,也可以直接写hql,都是jpa里面的东西
      • Angeladaddy:也觉得用vue是多余,或者Thymeleaf多余,文章受教了
        Angeladaddy:@集成显卡 其实我也一直想这个问题,spa做服务端渲染最大的需求就是seo ,和性能优化,那如果我这个应用将来要打包成独立的应用程序,比如electron 的话,该怎么办呢?
        da94bbd0b67d:不算多余。vue是用于开发SPA的,但是单页面经常在初始化阶段就需要一些后台的数据,此时就能通过后台的模板引擎来赋值。

        相辅相成
      • 黑桃29:@Controller那个地方 加@ResponseBody然后直接返回一个String也可以吧
      • 黑桃29:赞,学习了
      • 没事找抽:既然用了Vue. js为什么还要用模板技术,直接static下index.html全部静态化了
        kaenry: @没事找抽 只是随便找个流行js框架,不是针对前台

      本文标题:2. spring-boot+thymeleaf(+vuejs)

      本文链接:https://www.haomeiwen.com/subject/udytjttx.html