晚期代码癌患者java核心知识springboot

2. spring-boot+thymeleaf(+vuejs)

2016-06-28  本文已影响22509人  kaenry

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

目前没有发现类似nodejs里面init功能的关于spring-boot的工具,推荐还是去github上面clone一个吧,方便快捷,也可使用start生成,贡献网址http://start.spring.io/。本文旨在这个目的构建一个仓库供以后使用,目标:

在上篇项目的基础上修改目录如下:


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

上一篇下一篇

猜你喜欢

热点阅读