springboot操作步骤

springBoot操作步骤

idea资源热更新

image-20190517170148559

实现注册功能, 写入数据到数据库中

新建一个controller, 命名为RegisterConroller

@Controller
public class RegistController {
    User user = new User();
    
    @RequestMapping(value = "/reg", method = RequestMethod.GET)
    public ModelAndView toRegist() {
        return new ModelAndView("register");
    }

    @RequestMapping(value = "/register", method =RequestMethod.GET)
    public ModelAndView Register(){
        User user =new User();
        return new ModelAndView("register");
    }
}

在pom.xml引入Thymeleaf的依赖, 由于我在开始初始化springboot的时候已经加入了依赖, 所以不用加入

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-thymeleaf</artifactid>
        </dependency>

然后再去application.properties里面

image-20190517144755784

添加Thymeleaf配置, 意思是view视图地址在/templates/文件夹里面, 后缀名为.html, 类型为html5, 等等意思

#thymelea
spring.thymeleaf.prefix= classpath:/templates/
spring.thymeleaf.suffix= .html
spring.thymeleaf.mode= HTML5
spring.thymeleaf.encoding= UTF-8
spring.thymeleaf.servlet.content-type= text/html
spring.thymeleaf.cache= false

为了避免static文件夹里面的静态资源文件能被Thymeleaf获取到, 就要去写一个资源配置类, 新建一个config包, 新建一个WebConfig类

image-20190517144934590

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {

    public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {
        resourceHandlerRegistry.addResourceHandler("/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX+"/static/");
        super.addResourceHandlers(resourceHandlerRegistry);
    }
}

然后在前端资源文件/templates/文件夹下面新建一个register.html

image-20190517145146073

要让html中使用Thymeleaf就要在html的head头部中引入Thymeleaf, 在html标签中加入, 改为:


然后下载Bootstraphttps://v3.bootcss.com/getting-started/#download引入到静态资源文件/static/文件夹内

image-20190517145744729

然后把下载的三个文件夹引入到static中

image-20190517145927204

register.html页面代码:




    <meta charset="UTF-8">
    <title>Title</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">


<div class="container" style="text-align:center;margin-top:50px;">
    <div class="row col-md-6 col-md-offset-3">
        <div class="panel panel-default">
            <div class="panel-heading" th:text="${title}">注册页面</div>
            <div class="panel-body">
                <form id="registerForm" th:action="@{/register}" th:object="${user}" method="post">
                    <div class="input-group">
                        <span class="input-group-addon">username</span>
                        <input id="username" type="text" th:field="*{username}" class="form-control " placeholder="用户名">
                    </div>
                    <div><span th:if="${#fields.hasErrors('username')}" th:errors="*{username}" style="color:red;"></span></div>
                    <br>
                    <div class="input-group">
                        <span class="input-group-addon">password</span>
                        <input id="password" type="password" name="password" class="form-control" placeholder="密码">
                    </div>
                    <div><span th:if="${#fields.hasErrors('password')}" th:errors="*{password}" style="color:red;"></span></div>
                    <br>
                    <div class="input-group">
                        <span class="input-group-addon">re-password</span>
                        <input id="repassword" type="password" th:field="*{repassword}" class="form-control" placeholder="确认密码">
                    </div>
                    <br>
                    <button type="submit" class="btn btn-primary register-btn">注册</button>
                    <button type="reset" class="btn btn-warning">重置</button>
                </form>
            </div>
        </div>
    </div>
</div>


然后去RegisterController.class里面去修改代码为:

@Controller
public class RegistController {
    // 引入日志文件
    private static Logger logger = LoggerFactory.getLogger(RegistController.class);

    @Autowired
    public UserService userService;

    @RequestMapping(value = "/reg", method = RequestMethod.GET)
    public ModelAndView toRegist() {
        User user = new User();
        return new ModelAndView("register").addObject(user);
    }

    @RequestMapping(value = "/register", method =RequestMethod.POST)
    public ModelAndView Register(@ModelAttribute(value = "user") User user){

        logger.info("username="+user.getUsername()+";password="+user.getPassword());
        user.setId((long) 2019);
        userService.rigist(user);
        return new ModelAndView("register");
    }
}

然后在页面输入账号密码点击注册就可以增加数据到数据库中了

image-20190517162818471

验证

客户端验证

前端用JQuery validation来验证, 去下载https://jqueryvalidation.org/

https://jqueryvalidation.org/documentation/查看使用方法

然后搜索引入下面四个文件:

  • additional-methods.min.js
  • jquery.validate.min.js
  • jquery-3.1.1.js
  • messages_zh.min.js

放到静态资源目录/static/下

image-20190517164252504

然后在我们要的页面去引入这四个文件, 加入js验证, required字段加入到iinput中, 单后js代码写如下代码, 就可以进行绑定, 输入为空就不可以进行提交表单, 这个在layui中也是这样

注意代码引入顺序不能错, 他们是依赖关系, 顺序不对就会报错

<script th:src="@{/js/jquery/jquery-3.1.1.js}"></script>
<script th:src="@{/js/jquery/jquery.validate.min.js}"></script>
<script th:src="@{/js/jquery/additional-methods.min.js}"></script>
<script th:src="@{/js/jquery/messages_zh.min.js}"></script>
<script>
    $("#registerForm").validate({
        rules: {
            username: "required",
            password: {
                required:true,
                minlngth:6
            },
            repassword: {
                equalTo: "#password"
            }
        }
    });
</script>

image-20190517170803596

服务端验证

使用JSR303来进行服务端验证, 参考文档: https://www.ibm.com/developerworks/cn/java/j-lo-jsr303/index.html

这个在javax-validation里面就自带了

具体使用如下, 比如在model: User对username进行验证, 加入@NotBlank(..)不为空

    @Id
    @Column(name="username")
    @NotBlank(message = "用户名不能为空")
    private String username;

然后修改控制层进行验证处理, 这里是RegisterController, 修改为:

@Controller
public class RegistController {
    // 引入日志文件
    private static Logger logger = LoggerFactory.getLogger(RegistController.class);

    @Autowired
    public UserService userService;

    @RequestMapping(value = "/reg", method = RequestMethod.GET)
    public ModelAndView toRegist() {
        User user = new User();
        return new ModelAndView("register").addObject(user);
    }

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public ModelAndView Register(@ModelAttribute(value = "user") @Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return new ModelAndView("register");
        }
        logger.info("username=" + user.getUsername() + ";password=" + user.getPassword());
        user.setId((long) 2019);
        userService.rigist(user);
        return new ModelAndView("register");
    }
}

即增加: image-20190517225751366

加密

前端加密MD5, 需要进行加盐来提高安全性, 加盐就是通过密码任意固定位置插入特定的字符, 再进行md5加密这样散列的结果就和原始密码不同, md5也是可以破解的, 需要大量的计算, 如果怕被找出原始密码就可以进行多次md5再加盐处理

private static final String salt = "springboot";//特定盐 
public static String inputPassToFormPass(String inputPass) {
  String str = inputPass + salt; //加盐 //更多是下面这种加盐
  String str = salt.charAt(0)+inputPass + salt.charAt(2);   
  return md5(str);
}

做法是, 前端传输密码的时候加盐加密, 后端拿到密码后也加盐加密再存储到数据库中

下载https://github.com/placemarker/jQuery-MD5, jquery.md5.js放到资源文件夹中

使用的话直接:

var md5 = $.md5('value');

然后写入到前端代码中, submitHandler: function(form)...是在验证完毕之后, 表单提交之前

image-20190518093546321

  $("#registerForm").validate({
        rules: {
            username: "required",
            password: {
                required: true,
                minlength: 6
            },
            repassword: {
                equalTo: "#password"
            }
        },
        submitHandler: function (form) {
            debugger;
            var salt = "bolg";
            var newPassword = $.md5($("#password").val() + salt);
            $("#password").val(newPassword);
            form.submit();
        }
    });

然后再对实体类User.java进行加盐处理

先在pom.xml加入依赖:

        <!-- MD5 start -->
        <dependency>
            <groupid>commons-codec</groupid>
            <artifactid>commons-codec</artifactid>
        </dependency>
        <dependency>
            <groupid>org.apache.commons</groupid>
            <artifactid>commons-lang3</artifactid>
        </dependency>
        <!-- MD5 END -->

然后在User.java实体类里面加入盐字段, 盐这里字段不用salt命名是因为为了避免别人拿到数据库后知道这是盐???

image-20190518144947273

然后再去创建util包, 新建md5util类用于验证封装

image-20190518151207576

import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util {

    // salt要和前端salt相同, 因为是用来判断输入密码和传入后台是不是相似的
    public static String salt = "blog";

    // 封装md5加密
    public static String md5(String str) {
        return DigestUtils.md5Hex(str);
    }

    // 第一次前端加密
    public  static String inputToBack(String str) {
        return md5(str+salt);
    }

    // 第二次加密: 后台加密, 这里的盐是后台的盐可以通过自行进行选择
    public static String toDb(String str, String dbSalt){
        return md5(str+dbSalt);
    }

    // 用来和数据库密码做比较
    public static String inputCompareWithDb(String str, String dbSalt){
        return toDb(inputToBack(str),dbSalt);
    }
}

然后再去RegistController类里面去加入后台加密代码

image-20190518152144963

@Controller
public class RegistController {
    // 引入日志文件
    private static Logger logger = LoggerFactory.getLogger(RegistController.class);

    @Autowired
    public UserService userService;

    @RequestMapping(value = "/reg", method = RequestMethod.GET)
    public ModelAndView toRegist() {
        User user = new User();
        return new ModelAndView("register").addObject(user);
    }

    @RequestMapping(value = "/register", method = RequestMethod.POST)
    public ModelAndView Register(@ModelAttribute(value = "user") @Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return new ModelAndView("register");
        }
        logger.info("username=" + user.getUsername() + ";password=" + user.getPassword());
        String salt = "china";
        String newPassword = MD5Util.BackToDb(user.getPassword(), salt);
        logger.info("newPassword=" + newPassword);
        user.setPassword(newPassword);
        user.setDbfalg(salt);
        user.setId((long) 2019);
        userService.rigist(user);
        return new ModelAndView("register");
    }
}

提升springboot应用性能的技巧

分为两个方面, springboot加载会自动配置, 但是根据我们自身所需, 并不是所有配置都用的上的, 因此可以分析我们应用启动加载了哪些项目, 只要加载我们要的项目就可以了, 其他没有用到的就不用加载进来, 免得影响性能

首先我们要求控制台可以看到所有的日志, 不然控制台默认就1024行, 超过就会清掉了

在idea中新版本不可以取消console行数限制, 要到软件目录下找到idea.properties然后修改idea.cycle.buffer.size项值为disabled, 然后修改console的history size, 默认是300, 那就在后面多加两个0就好了,重启idea就可以了

优化方法一: 修改启动自动配置为手动选择加载项

image-20190518153351893

image-20190518153644864

然后再去idea的run配置中勾选 enable debug output

image-20190518153837357

然后重启运行下idea, 看下控制台输出有哪些我们要用到的

image-20190518154235605

pisitive matches是我们有用到的, negative matches是springboot自动加载的, 但是我们没有用到的

image-20190518154337908

然后就是把自动配置改为手动配置, 具体看下图, 每一个都要引入, 图中不小心跳过一个了

image-20190518155206524

优化方法二: 修改tomcat为Jetty

Jetty相比于Tomcat更加小巧, 只要修改pom.xml配置就可以了, 移除tomcat加入jetty

springboot默认启动的是tomcat, 启动web, 找到

        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-web</artifactid>
        </dependency>

加入exclutions, 加入Jetty依赖

<!--移除tomcat依赖-->
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-web</artifactid>
            <exclusions>
                <exclusion>
                    <groupid>org.springframework.boot</groupid>
                    <artifactid>spring-boot-starter-tomcat</artifactid>
                </exclusion>
            </exclusions>
        </dependency>
        <!--添加Jetty依赖-->
        <dependency>
            <groupid>org.springframework.boot</groupid>
            <artifactid>spring-boot-starter-jetty</artifactid>
        </dependency>

image-20190518160231186

然后再取消之前的springboot启动配置文件的enable debugg output就好了

> 注意修改依赖都需要update maven

完成基本的登录功能

其实功能上实现方式和注册是一样的, 直接看代码就好了, 首先新建一个LoginController的控制类, 由于http是无状态协议, 所以用户的登录状态都要保存在session会话里面

防止大量重复请求、机器人暴力访问等情况的,最有效的手段就是加入验证码, 同时记录某个用户在或ip在某个时段内如果尝试的失败登录次数超过一定阈值就限制其在几分钟内不能继续登录。

  1. 完成基本的登录功能: ⚫ Login页面采集username和Password ⚫ 根据 useraname 判断 User 使用存在 ⚫ 如果存在,使用 MD5 的两次加盐加密比对数据库中密码是否行一致

    ⚫ 将User保存到Session中

URL 请求分析:

  1. get:/login 逻辑:跳转到注册页面 参数:username,password 返回值:ModelAndView

  2. post:/login 逻辑: 保存提交的用户名和密码

单元测试之军工六性

稳定性、适应性、安全性、保障性、维修性、测试性

> 出现org.hibernate.LazyInitializationException: could not initialize proxy… no session

解决方法有几种,

  1. 关掉懒加载 spring.jpa.properties.hibernate.enable_lazy_load_no_trans= true

    image-20190518213643308

  2. 在service层把entity转成vo (推荐)

    vo和页面需要用到的属性对应,实体是和数据表对应

    实体受持久化层的session管理,vo不受

  3. 在service层调用或getter方法

后面是通过https://stackoverflow.com/questions/32264758/why-does-getone-on-a-spring-data-repository-not-throw-an-entitynotfoundexcept

来解决, 有两种方法, 一种是使用Optional.ofNullable()

  return Optional.ofNullable(userReository.getOne(username));

另一种是使用findById(), 但是要记得try, catch,

return userRepository.findById(id).orElse(null);

返回的都是Optional<实体类>

> 我用第一种方法还是不可以, 因为getOne是属于懒加载, 返回的不是实体类,是一个代理, findById返回的就是实体类, 是可以的 > > image-20190519093248526 > > image-20190519093322653 > > image-20190519093401631

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。举个简单的例子:

img

静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。

Optional.of()或者Optional.ofNullable():创建Optional对象,差别在于of不允许参数是null,而ofNullable则无限制。

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类。


后面看到可以通过在Repository层编写获取数据方式来解决getOne()懒加载问题

image-20190519151103404

image-20190519151131171

> 这是jpa封装好的一个模板, 就是用findBy开头可以去找前面实体类的属性, 比如这里的 > > public User findByUsername(...) > > 就会去User里面找username的属性值


编写登录junit测试

image-20190519100309185

    @Test
    public void testGetUser() {
        Assert.assertNotNull(userService.getUserNew("001"));
    }

    @Test
    public void testPassword() {
        User user = userService.getUserNew("001").get();
        String inputPassword = MD5Util.inputToDb("123456", user.getDbfalg());
        Assert.assertEquals(inputPassword, user.getPassword());
    }

增加图形验证码

参考: https://blog.csdn.net/cjm1103/article/details/71171842

新增ValidateCode图形验证工具类

image-20190519101941045

package com.sansenBlog.blog.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Date;
import java.util.Random;
/**
 * 验证码生成器
 *
 * @author
 */
public class ValidateCode {
        // 图片的宽度。
        private int width = 160;
        // 图片的高度。
        private int height = 40;
        // 验证码字符个数
        private int codeCount = 5;
        // 验证码干扰线数
        private int lineCount = 150;
        // 验证码
        private String code = null;
        // 验证码图片Buffer
        private BufferedImage buffImg = null;

        // 验证码范围,去掉0(数字)和O(拼音)容易混淆的(小写的1和L也可以去掉,大写不用了)
        private char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
                'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
                'X', 'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8', '9'};

        /**
         * 默认构造函数,设置默认参数
         */
        public ValidateCode() {
            this.createCode();
        }

        /**
         * @param width  图片宽
         * @param height 图片高
         */
        public ValidateCode(int width, int height) {
            this.width = width;
            this.height = height;
            this.createCode();
        }

        /**
         * @param width     图片宽
         * @param height    图片高
         * @param codeCount 字符个数
         * @param lineCount 干扰线条数
         */
        public ValidateCode(int width, int height, int codeCount, int lineCount) {
            this.width = width;
            this.height = height;
            this.codeCount = codeCount;
            this.lineCount = lineCount;
            this.createCode();
        }

        public void createCode() {
            int x = 0, fontHeight = 0, codeY = 0;
            int red = 0, green = 0, blue = 0;

            x = width / (codeCount + 2);//每个字符的宽度(左右各空出一个字符)
            fontHeight = height - 2;//字体的高度
            codeY = height - 4;

            // 图像buffer
            buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = buffImg.createGraphics();
            // 生成随机数
            Random random = new Random();
            // 将图像填充为白色
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, width, height);
            // 创建字体,可以修改为其它的
            Font font = new Font("Fixedsys", Font.PLAIN, fontHeight);
//        Font font = new Font("Times New Roman", Font.ROMAN_BASELINE, fontHeight);
            g.setFont(font);

            for (int i = 0; i &lt; lineCount; i++) {
                // 设置随机开始和结束坐标
                int xs = random.nextInt(width);//x坐标开始
                int ys = random.nextInt(height);//y坐标开始
                int xe = xs + random.nextInt(width / 8);//x坐标结束
                int ye = ys + random.nextInt(height / 8);//y坐标结束

                // 产生随机的颜色值,让输出的每个干扰线的颜色值都将不同。
                red = random.nextInt(255);
                green = random.nextInt(255);
                blue = random.nextInt(255);
                g.setColor(new Color(red, green, blue));
                g.drawLine(xs, ys, xe, ye);
            }

            // randomCode记录随机产生的验证码
            StringBuffer randomCode = new StringBuffer();
            // 随机产生codeCount个字符的验证码。
            for (int i = 0; i &lt; codeCount; i++) {
                String strRand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
                // 产生随机的颜色值,让输出的每个字符的颜色值都将不同。
                red = random.nextInt(255);
                green = random.nextInt(255);
                blue = random.nextInt(255);
                g.setColor(new Color(red, green, blue));
                g.drawString(strRand, (i + 1) * x, codeY);
                // 将产生的四个随机数组合在一起。
                randomCode.append(strRand);
            }
            // 将四位数字的验证码保存到Session中。
            code = randomCode.toString();
        }

        public void write(String path) throws IOException {
            OutputStream sos = new FileOutputStream(path);
            this.write(sos);
        }

        public void write(OutputStream sos) throws IOException {
            ImageIO.write(buffImg, "png", sos);
            sos.close();
        }

        public BufferedImage getBuffImg() {
            return buffImg;
        }

        public String getCode() {
            return code;
        }

        /**
         * 测试函数,默认生成到d盘
         * @param args
         */
        public static void main(String[] args) {
            ValidateCode vCode = new ValidateCode(160,40,5,150);
            try {
                String path="/Users/achunorigin/Downloads/"+new Date().getTime()+".png";
                System.out.println(vCode.getCode()+" &gt;"+path);
                vCode.write(path);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

}

加入图形验证流

image-20190519102240570

    /**
     * 响应验证码页面
     * @return
     */
    @RequestMapping(value="/validateCode")
    public String validateCode(HttpServletRequest request, HttpServletResponse response) throws Exception{
        // 设置响应的类型格式为图片格式
        response.setContentType("image/jpeg");
        //禁止图像缓存。
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        HttpSession session = request.getSession();

        ValidateCode vCode = new ValidateCode(120,40,5,100);
        session.setAttribute("code", vCode.getCode());
        vCode.write(response.getOutputStream());
        return null;
    }

添加页面调用

image-20190519102905517

<div class="input-group">
  <span class="input-group-addon">验证码</span>
  <input type="text" id="code" name="code" class="form-control" style="width:250px;">
  <img id="imgObj" alt="验证码" src="/validateCode" class="img-responsive" onclick="changeImg()">
</div>
<br>
    /*------------------------ 图形验证开始 -----------------------------------*/
    // 刷新图片
    function changeImg() {
        var imgSrc = $("#imgObj");
        var src = imgSrc.attr("src");
        imgSrc.attr("src", changeUrl(src));
    }
    //为了使每次生成图片不一致,即不让浏览器读缓存,所以需要加上时间戳
    function changeUrl(url) {
        var timestamp = (new Date()).valueOf();
        var index = url.indexOf("?",url);
        if (index &gt; 0) {
            url = url.substring(index, url.indexOf(url, "?"));
        }
        if ((url.indexOf("&amp;") &gt;= 0)) {
            url = url + "×tamp=" + timestamp;
        } else {
            url = url + "?timestamp=" + timestamp;
        }
        return url;
    }
    /*------------------------ 图形验证结束 -----------------------------------*/

修改登录控制类, 添加图形验证

image-20190519103540870

        // 图形验证
        String sessionCode = (String)httpSession.getAttribute("code");
        if(!StringUtils.equalsIgnoreCase(sessionCode, code)){
            model.addAttribute("message","验证码不匹配");
            return "login";
        }

model.assAttribute("message",…)要在前端进行接收匹配

image-20190519155134264

单元测试的军工六性

稳定性, 适应性, 安全性, 保障性, 维修性, 测试性

主要是进度和质量, 比如别人完成这件事情要五天, 你三天就可以完成了, 说明效率很不错在绝对优势上

是程序猿很重要的考量标准

编写测试需要编写测试用例

image-20190519153147493

springboot调用redis

  1. redis基础配置文件
  2. redis基础配置类
  3. 公共方法的定义
  4. User的redis读取
  5. junit测试验证

redis:

  • 性能极高, 因为实在内存中的

  • 支持丰富的数据类型

  • 所有操作都是原子性, 意思是要么成功要么失败完全不执行, 不存在中间状态.

    单个操作是原子性 的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。

  • 有丰富的特性, 支持publish/subscribe通知, key过期等等

  1. 需掌握:

    1. Redis安装:单机和集群

    2. Redis的5种数据类型的使用:String / Hash / List / Set / Ordered Set

    3. Redis事物:注意和我们之前接触的不同,单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加

      任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

    4. Redis持久化:RDB和AOF

    5. Redis配置:主从、性能测试等

参考学习:http://www.runoob.com/redis/redis-tutorial.html


Spring Data Redis(SDR)

SDR是Spring官方推出,可以算是Spring框架集成Redis操作的一个子框架,封装了Redis的很多命令,可以很方便的使用Spring操作Redis数据库。 ⚫ Redis是用ANSI C写的一个基于内存的Key-Value数据库; ⚫ Jedis是Redis官方推出的面向Java的Client,提供了很多接口和方法,可以让Java操作使用Redis, ⚫ Spring Data Redis是对Jedis进行了封装,集成了Jedis的一些命令和方法,可以与Spring整合。


RedisTemplate

Spring封装了RedisTemplate对象来进行对Redis的各种操作,它支持所有的Redis原生的api

⚫ Redis的5种数据结构类型:

  1. String
  2. Hash
  3. List : 一个链表, 链表上的每个节点都包含一个子字符串
  4. Set : 和JavaScript类似, 不允许重复
  5. Zset: 字符串成员与浮点数分值之间有序映射, 排列顺序由分值的大小决定

RedisTemplate中定义了对5种数据结构操作

  1. redisTemplate.opsForValue();//操作字符串
  2. redisTemplate.opsForHash();//操作hash
  3. redisTemplate.opsForList();//操作list
  4. redisTemplate.opsForSet();//操作set
  5. redisTemplate.opsForZSet();//操作有序set

StringRedisTemplate继承RedisTemplate

两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据, RedisTemplate只能管理RedisTemplate中的数据。

SDR默认采用的序列化策略有两种:

  • 一种是String的序列化策略
  • 一种是JDK的序列化策略。

StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。 RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。

参考:https://www.cnblogs.com/EasonJim/p/7803067.html


redis编码配置

redis基础配置

首先是引入redis在pom.xml中

<dependency>
  <groupid>org.springframework.boot</groupid>
  <artifactid>spring-boot-starter-data-redis</artifactid>
</dependency>

然后再配置redis连接池

在resouces包下新建一个application.yml, 然后配置redis连接池, 注意缩进, 1,2,3位置

至于本地端口号要看自己电脑安装的redis端口号是多少, 并且要启动redis服务默认端口号为6379, 我们一般不进行修改

host是要连接的ip地址如果是连接虚拟机就连虚拟机的ip地址就好了

server:
  port: 8080
#redis
spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    jedis:
      pool:
        max-wait: -1ms
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 3000ms

application.yml和application.properties两种配置都可以正常加载, 只不过内容有点不同, 也可以在application.properties里面进行配置

########################################################
###redis
########################################################
spring.redis.database= 0
#redis
spring.redis.host= 127.0.0.1
spring.redis.port= 6379
spring.redis.jedis.pool.max-active= 8
spring.redis.jedis.pool.max-wait= -1ms
spring.redis.jedis.pool.max-idle= 8
spring.redis.jedis.pool.min-idle= 0
spring.redis.timeout= 3000ms

我使用的是mac, 用medis来可视化管理redis

image-20190519171111166


配置redisConfig, 就是公共类

新建一个名为redis的包, 然后添加RedisConfig类, 这个类目的是为了注入redisConnectionFectory, 也就是注入redis连接的工厂类, 设置数据序列化的方式

最后返回数据bean的RedisTemplate

代码逻辑是:

首先, 自动注入 RedisConnectionFactory 工厂类, 然后进行初始化初始化redisTemplate

/*
 * 设置数据存入数控的序列化方式, 初始化redisTemplate, 设置了一些序列化的存储方式
 * 有两种方式:
 *      1. StringRedisSerializer
 *      2. JdkSerializationRedisSerializer
 * */

最后返回注册一个bean叫做RedisTemplate

image-20190519170851127

package com.sansenBlog.blog.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableAutoConfiguration
public class RedisConfig {
    /*自动注入 RedisConnectionFactory 工厂类*/
    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<string, object> mainRedisTemplate() {
        RedisTemplate<string, object> redisTemplate = new RedisTemplate&lt;&gt;();
        initRedisTemplate(redisTemplate, redisConnectionFactory);
        return redisTemplate;
    }
    /*
     * 设置数据存入数控的序列化方式
     * */

    public void initRedisTemplate(RedisTemplate<string, object> redisTemplate, RedisConnectionFactory
            redisConnectionFactory) {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setStringSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory);
    }

    @Bean
    public HashOperations<string, string, object> hashOperations(RedisTemplate<string, object>
                                                                         redisTemplate) {
        return redisTemplate.opsForHash();
    }

    @Bean
    public ListOperations<string, object> listperations(RedisTemplate<string, object> redisTemplate) {
        return redisTemplate.opsForList();
    }

    @Bean
    public SetOperations<string, object> setOperations(RedisTemplate<string, object> redisTemplate) {
        return redisTemplate.opsForSet();
    }

    @Bean
    public ValueOperations<string, object> valueOperations(RedisTemplate<string, object> redisTemplate) {
        return redisTemplate.opsForValue();
    }

    @Bean
    public ZSetOperations<string, object> zSetOperations(RedisTemplate<string, object> redisTemplate) {
        return redisTemplate.opsForZSet();
    }
}

编写redis公共类和具体实现

编写公共类, 定义redis公共抽象类来完成一些常规操作

新建一个抽象类叫BaseRedis

> 题外: 用@Resource 也可以引入bean, 和@AutoWired差不多, 方法引入bean才可以调用需要的方法, 什么意思呢, 比如我一个方法需要调用其他函数的方法怎么调用呢 > > 类似JavaScript的export暴露出去, bean就是这样 > > 用bean可以把一个类的方法暴露到bean池中, 然后我们其他类要用到这些方法的话就直接@Autowired或@Resource就可以引入我们要的bean > > image-20190519203631713 > > image-20190519203732222

image-20190519205138709

具体代码如下:

package com.sansenBlog.blog.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/*
 * 先定义一个抽象类, 用于模板
 * */
public abstract class BaseRadis<t> {

    // 1. 引入模板
    @Autowired
    protected RedisTemplate<string,object> redisTemplate;

    // 2. 写一个模板类hashOptionas
    @Resource
    protected HashOperations<string, string, t> hashOperations;

    // 3. 定义一个getRedisKey, 来解决获取公共redisKey的问题, 基本上都要在第一个参数用到
    protected abstract String getRedisKey();

    /*
    * 4. 再写一个put方法用于把值放入到表里面。expire是什么时候过期, -1表示不过期
    * */
    public void put(String key, T domain, long expire) {
        // hashOperations有三个值,第一个是主要的key, 第二个key是hash里面对应的真正的key, 第三个就是值
        hashOperations.put(getRedisKey(), key,domain);
        if(expire!=-1){
            // 设置模板的有效时间,获取表, 设置时间,设置时间类型为秒
            redisTemplate.expire(getRedisKey(),expire,TimeUnit.SECONDS);
        }
    }
}


编写UserRedis来做具体的实现

先去实体类user出去修改为客串行, 意思是变成了接口, 其他类可以接到这个类上面去

即public class User implements Serializable

image-20190519205532701

然后在redis包里新建UserRedis

需要进行序列化存储, 继承BaseRedis<user>,然后来实现User的serializable

记得要写@Repository, 写Repository其实也是注入到bean里面, 只不过我们自己为了更好区分而写成@Repository

因为这是数据层操作, 我们放入到Repository层

别的地方通过@Autowired也可以找到它

image-20190519212036115

package com.sansenBlog.blog.redis;

import com.sansenBlog.blog.model.User;

/*
* 需要进行序列化存储, 继承BaseRedis<user>,然后来实现User的serializable
* */
@Repository
public class UserRedis extends BaseRedis<user>{
    // 主要是用来存储key的
    private static final String REDIS_KEY = "com.sansenBlog.blog.redis.UserRedis";
    @Override
    protected String getRedisKey() {
        return REDIS_KEY;
    }
}

> 再说一遍static和final > > static:static的存在是为了优化性能 > > 1. **静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。**而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。 > > 2. 静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this,因为它不依附于任何对象,既然都没有对象,就谈不上this了。 > > 静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。 > > Static方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问static成员方法/变量的。也就是static处于访问的弱势群体 > > final:是为了定义不被修改而存在的 > > 1. 对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 > > 在使用final修饰类的时候,要注意谨慎选择,除非这个类真的在以后不会用来继承或者出于安全的考虑,尽量不要将类设计为final类 > > 所以static final …可以理解为: 在内存中只占用一个坑位, 并且这个坑位就是我的, 谁也不能再修改了


写完后再写一个测试类来测试UserRedis

我们要测试的都是一些自己定义的最后的类, 比如这里的UserRedis是可以用来测试的

image-20190519212957040

@Autowired
public UserRedis userRedis; 
@Test
public void testPutRedis() {
  User user = new User("blog","123456");
  // 按照自定义的方法会先放到hashOperations里面,表名字为自定义的包名字,然后在表里面放如key和value
  userRedis.put(user.getUsername(),user,-1);
}

然后运行我们的测试类, 发现是ok的

image-20190519213128331


ok的话我们就去稍微补全redis公共类方法, BaseRedis

package com.sansenBlog.blog.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/*
 * 先定义一个抽象类, 用于模板
 * */
public abstract class BaseRedis<t> {

    // 1. 引入模板
    @Autowired
    protected RedisTemplate<string,object> redisTemplate;

    // 2. 写一个模板类hashOptionas
    @Resource
    protected HashOperations<string, string, t> hashOperations;

    /**
     * 存入redis中的key *
     * 定义一个getRedisKey, 来解决获取公共redisKey的问题, 基本上都要在第一个参数用到 *
     * @return
     * */
    protected abstract String getRedisKey();

    /**
     * 添加 *
     * @param key key
     * @param domain 对象
     * @param expire 过期时间(单位:秒),传入 -1 时表示不设置过期时间
     * */
    public void put(String key, T domain, long expire) {
        // hashOperations有三个值,第一个是主要的key, 第二个key是hash里面对应的真正的key, 第三个就是值
        hashOperations.put(getRedisKey(), key,domain);
        if(expire!=-1){
            // 设置模板的有效时间,获取表, 设置时间,设置时间类型为秒
            redisTemplate.expire(getRedisKey(),expire,TimeUnit.SECONDS);
        }
    }
    
    /**
     * 删除
     *
     * @param key 传入key的名称 */
    public void remove(String key) { hashOperations.delete(getRedisKey(), key);
    }
    /**
     * 查询 *
     * @param key 查询的key * @return
     */
    public T get(String key) {
        return hashOperations.get(getRedisKey(), key);
    }
    /**
     * 获取当前redis库下所有对象 *
     * @return
     */
    public List<t> getAll() {
        return hashOperations.values(getRedisKey());
    }
    /**
     * 查询查询当前redis库下所有key
     *
     * @return */
    public Set<string> getKeys() {
        return hashOperations.keys(getRedisKey());
    }
    /**
     * 判断key是否存在redis中
     *
     * @param key 传入key的名称 * @return
     */
    public boolean isKeyExists(String key) {
        return hashOperations.hasKey(getRedisKey(), key);
    }
    /**
     * 查询当前key下缓存数量 *
     * @return
     */
    public long count() {
        return hashOperations.size(getRedisKey());
    }
    /**
     * 清空redis */
    public void empty() {
        Set<string> set = hashOperations.keys(getRedisKey()); set.stream().forEach(key -&gt; hashOperations.delete(getRedisKey(), key));
    } 
}

然后再添加UserRedis的一些操作方法, 需要先引入com.google.code.gson

GSON是Google开发的Java API,用于转换Java对象和Json对象。

<dependency>
    <groupid>com.google.code.gson</groupid>
    <artifactid>gson</artifactid>
</dependency>

然后在UserRedis修改为

image-20190519215143929

package com.sansenBlog.blog.redis;

import com.google.gson.Gson;
import com.sansenBlog.blog.model.User;
import org.springframework.stereotype.Repository;

import java.util.concurrent.TimeUnit;

/*
* 需要进行序列化存储, 继承BaseRedis<user>,然后来实现User的serializable
* */
@Repository
public class UserRedis extends BaseRedis<user>{
    // 主要是用来存储key的
    private static final String REDIS_KEY = "com.sansenBlog.blog.redis.UserRedis";
    @Override
    protected String getRedisKey() {
        return REDIS_KEY;
    }

    public void add(String key, Long time, User user){
        Gson gson = new Gson();
        redisTemplate.opsForValue().set(key, gson.toJson(user), time, TimeUnit.SECONDS);
    }
}

基于Redis的登录功能

保持登录状态一致性

理一下逻辑, 我们之前是从数据库中直接获取user, 现在是先从redis内存中去获取数据, 没有找到再去数据库找

所以要先修改UserServiceImpl的getUser()方法, 在其中去引入redis


先说下怎么保持登录一致性问题

例如, 我们有三台tomcat服务器, 我们客户端发送一个请求到其中一台服务器,该服务器会保存一个session来记录登录状态, 但是其他两台tomcat是没有收到这个session的, 这样就没办法保持登录一致性

对于这个问题有很多解决方案, 其中一种就是使用redis, 就是相当于第三方中介, session可以放入到redis里面, 那么这三台就都可以读取了


cookie是客户端前端保持方案, session是后端保持方案

保存 Session id 的几种方式:

  1. 保存 session id 的方式可以采用 cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给 服务器。

  2. 最常用的方案由于 cookie 可以被人为的禁止,必须有其它的机制以便在 cookie 被禁止时仍然能够把 session id 传递回 服务器,经常采用的一种技术叫做 URL 重写,就是把 session id 附加在 URL 路径的后面,附加的方式也有两种, 一种是作为 URL 路径的附加信息,另一种是作为查询字符串附加在 URL 后面。网络在整个交互过程中始终保持状 态,就必须在每个客户端可能请求的路径后面都包含这个 session id。

  3. 也很常用,更加推荐另一种技术叫做表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够 把 session id 传递回服务器。


JSESSIONID 与 cookie 是什么关系,session 与 cookie 到底有什么关系

简单来说,当第一次 request server 时,server 产生 JSESSIONID 对应的值 1,通过 http header set- cookie,传递给 browser,browser 检测到 http response header 里带 set-cookie,那么 browser 就会 create 一个 cookie,key=JSESSIONID,value=值 1,而后的每次请求,browser 都会把 cookie 里的键值对,

放到 http request header 里,传递给 server。 当在 server 端调用 http.getSession()方法时,server 会先从 http request header 里解析出来

JSESSIONID 的值,再从一个 Map 容器里去找有没有 value,如果没有,就会产生一个 HttpSessioon 对象, 放到这个 Map 容器里,同时设置一个最大生存时间。HttpSession 你也可以把它想象成是一个 Map,可以 getAttribute(),可以 setAttribute().


分布式 Session 如何实现

最前端的负载均衡器,如 nginx,会把 request 原封不动的平均分配到集群中的一台机器,所喂原封不动,就 是指的 request header 里的内容不变。 Tomcat 处理单机 session 其实就是JSESSIONID来获取 session 信息,所以要实现分布式 Session,只要得 到 JSESSIONID 的值就可以了,剩下的操作无非就是根据 key 去 redis 或 memcache 中存取值,并缓存到本机内存中。


下面开始进行步骤演示

首先去新建一个VO包, VO是对model对象进行映射的, 然后在VO下新建一个UserVO , 里面存的值就应该是User对象里面的值, 创建serializable接口, 再引入getter和setter方法和seriaVersionUID, 在idea中要打开seriaVersionUID就要去设置中先打开才可以,因为默认是关闭的, setting->Inspections->Serialization issues,将serialzable class without "serialVersionUID"打上勾, 然后option+enter在类上就可以了.

image-20190521094105731


image-20190521094515147

先去UserService把返回对象改成VO, 再去impl具体实验类去修改细节

image-20190521100916304

    @Autowired
    public UserRedis userRedis;

    @Override
    public UserVO getUser(String username) {
        // return userReository.getOne(username);
        UserVO userVO = new UserVO();
        User user = userRedis.get("username");

        if (user == null) {
            user = userReository.findByUsername(username);
            if (user != null) {
                userRedis.put(user.getUsername(), user, -1);
            } else {
                return null;
            }
        }
        BeanUtils.copyProperties(user, userVO);
        return userVO;
    }

然后在util包里面新建一个UUIDUtil类用于产生一个uuid然后存放到redis,当作是resis中的数据id

image-20190521104611156

package com.sansenBlog.blog.util;

import java.util.UUID;

public class UUIDUtil {
    public static String getUUID(){
        return UUID.randomUUID().toString().replaceAll("-","");
    }
}

然后就是去修改LoginController,

> 跳转到home使用的是redirect:/home来进行跳转到根目录下的home, 跳转到home会被requestMapping的"/home"拦截, 而不是直接跳转到home.html

image-20190521111907069

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public String Login(@Valid User user, BindingResult bindingResult, HttpSession httpSession, Model model, String code,HttpServletResponse httpServletResponse) {
        logger.info("login............username: "+user.getUsername()+" password: "+user.getPassword());
        if (bindingResult.hasErrors()) {
            return "login";
        }
        // 图形验证, equalsIgnoreCase表示忽略大小写
        String sessionCode = (String)httpSession.getAttribute("code");
        if(!StringUtils.equalsIgnoreCase(sessionCode, code)){
            model.addAttribute("message","验证码不匹配");
            System.out.println("........验证码不匹配");
            return "login";
        }
        // 判断用户是否存在
        UserVO userVO = userService.getUser(user.getUsername());
        if(userVO !=null){
            if(userVO.getPassword().equals(MD5Util.inputToDb(user.getPassword(),userVO.getPassword()))){
                // httpSession.setAttribute("user",userVO);
                String token = UUIDUtil.getUUID();
                userService.seveUserToRedisByToken(token,user);

                // token其实就是sessionId, 要把token放入到cookie中
                Cookie cookie = new Cookie("token",token);
                cookie.setMaxAge(3600);
                cookie.setPath("/"); // 放在根目录
                httpServletResponse.addCookie(cookie);
                
                return "redirect:/home";
            } else {
                return "login";
            }
        }else {
            return "login";
        }
    }

为了更好验证我们的代码是否正确, 去新建一个HomeController

> 这里解释下model操作, 参考: https://blog.csdn.net/a67474506/article/details/46362783

image-20190521113212098

@Controller
public class HomeController {

    @RequestMapping(value = "/home", method = RequestMethod.GET)
    public String toLogin(Model model, User user) {
        model.addAttribute("user", new User());
        return "home";
    }
}

如果我们要获取redis里面的token再获取到对应的user对象, 意味着我们每一次都要去进行redis.get("token"), 这样写会很不方便

因此这里引入了spring的自定义参数解析器HandlerMethodArgumentResolver

(方法参数分解管理器)

可以参考: https://blog.csdn.net/lovesomnus/article/details/73650336

为什么Controller方法上竟然可以放这么多的参数?而且都能得到想要的对象,比如HttpServletRequest或

HttpServletResponse, 各种注解@RequestParam、@RequestHeader、@RequestBody、 @PathVariable、@ModelAttribute等。 这其实都是org.springframework.web.method.support.HandlerMethodArgumentResolver的功劳。

核心: ⚫ supportsParameter 通过该方法我们如果需要对某个参数进行处理 只要此处返回true即可, 通过MethodParameter可以获取该方法参数上的一些信息, 如方法参数中的注解信息等 ⚫ resolveArgument 该方法就是对参数的解析,返回的Object会自动赋值到参数对象中


对此我们要先编写一个自定义参数解析器命名为UserArgumentResolver放在config包里面

image-20190521113541431

package com.sansenBlog.blog.config;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return false;
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        return null;
    }
}

它会新建两个默认的基本方法, 然后我们再对这个Resolver来进行加入自己要的代码

image-20190521204817667

import com.sansenBlog.blog.model.User;
import com.sansenBlog.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

public class UserArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    public UserService userService;

    public String getParameterCokies(HttpServletRequest request, String tokenName) {
        // 从request去找cookies,找到后然后去遍历它,直到找到token的值
        Cookie[] cookies = request.getCookies();
        for (Cookie ck : cookies) {
            if (ck.getName().equals(tokenName)) {
                return ck.getValue();
            }
        }
        return null;
    }

    /*
    * 定义对象获取到的方式, 这里可以写我们要的自定义方法进去就不用每次都redis.get("token")
    * */
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer,
                                  NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = nativeWebRequest.getNativeRequest(HttpServletRequest.class);
        /*
        * 这里自定义了两个变量, 如果cookie被禁用了就会以requestParameter_token传过来
        * token是存在cookie里面的
        * */
        String requestParameter_token = request.getParameter("token");
        String Cokies_token = getParameterCokies(request, "token"); // 去调用getParameterCokies方法
        if (requestParameter_token == null &amp;&amp; Cokies_token == null) {
            return null;
        }
        return userService.getUserFromRedisByToken((requestParameter_token != null ? requestParameter_token : Cokies_token));
    }

    /*
    * 用来判断参数是否一致的, 判断引入的参数类型是不是User.class
    * */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        Class<!--?--> p_class = methodParameter.getParameterType();
        return p_class == User.class;
    }
}

Resolver代码里面哟啊新建userService.getUserFromRedisByToken, 对应也要写impl方法

image-20190522092035700

UserServiceImpl

然后再去webconfig里面去吧userArgumentResolver加载进来, 步骤是: 首先进行配置, 告诉说我要使用这个参数解析器

第二件事, 去判断参数支持的类型

  1. 解析参数的时候去获取值

运行时候会报错

例如: java.lang.ClassCastException: com.dayup.seckil.model.User cannot be cast to com.dayup.seckil.model.User

image-20190522101953954

这个是热部署问题, 解决方式是注释掉热部署代码配置部分, 在pom.xml中进行配置

image-20190522104744544


问题Caused by: java.lang.IllegalStateException: An Errors/BindingResult argument is expected to be declared immediately after the model attribute, the @RequestBody or the @RequestPart arguments to which they apply: public java.lang.String com.sansenBlog.blog.controller.LoginController.Login(com.sansenBlog.blog.model.User,org.springframework.validation.BindingResult,javax.servlet.http.HttpSession,org.springframework.ui.Model,java.lang.String,javax.servlet.http.HttpServletResponse)…


解决:

image-20190522113411966

参数最前面加入: @ModelAttribute(value = "user")


因为在redis中取出的对象不能转换为user

> 题外话: @Autowired与@Resource的用法和区别 > > 一、@Autowired: > > 1. Spring 2.5 引入了 @Autowired 注释,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 通过 @Autowired的使用来消除 set ,get方法。 > > 这个注解就是spring可以自动帮你把bean里面引用的对象的setter/getter方法省略,它会自动帮你set/get。 > > xml &gt; <bean id="userDao" class="..." /> &gt; <bean id="userService" class="..."> &gt; <property name="userDao"> &gt; <ref bean="userDao" /> &gt; </property> &gt; </bean> &gt; > > 这样你在userService里面要做一个userDao的setter/getter方法。 > 但如果你用了@Autowired的话,你只需要在UserService的实现类中声明即可。 > @Autowired > private IUserDao userdao; > > PS: > > 1. @Autowired是根据类型进行自动装配的。 > > 2. @Autowired也可以手动指定按照byName方式注入,使用@Qualifier标签,例如: > > java &gt; @Autowired() @Qualifier("baseDao") &gt; private BaseDao baseDao; &gt; > > 二、@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入。 > @Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。 > @Resource装配顺序   > > 1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常   > > 2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常 > 3.如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常   > 4.如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配; > > 三、@Autowired与@Resource的区别 > 1、@Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上。 > 2、@Autowired默认按类型装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null 值,可以设置它的required属性为false。 > 3、@Resource(这个注解属于J2EE的),默认安照名称进行装配,名称可以通过name属性进行指定, > 如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在setter方法上默认取属性名进行装配。 当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。 > > ps: @Resource注解在字段上,这样就不用写setter方法了,并且这个注解是属于J2EE的,减少了与spring的耦合。 > > 参考: https://blog.csdn.net/u011067360/article/details/38873755


前端分离

image-20190522151940167

jsp阶段时代, 会有很多业务逻辑写到jsp导致页面很臃肿

维护性很差

后面出现了mvc模式, 复杂度降低了, 因为稍微解耦了下

image-20190522152042389

但是mvc也有缺点, 比如页面跳转的时候, 为了解决页面要频繁跳转,就出现了ajax技术用于异步请求

image-20190522152220613

但是ajax也有很多缺点, 例如有大量的业务逻辑写在js代码里面, 导致页面越来越复杂, 复杂度变高后维护性就会变差

之后为此就出现了前后端分离技术

image-20190522152402111

其实就是总结来说, 一步步地解耦, 慢慢进行独立功能拆分, 各自自责清晰


前后端分离优劣

image-20190522152654965

但是如果只是在做一个小项目, 业务逻辑不复杂, 后期扩展的几率又不大, 人员又少,就不一定用前后端分离

image-20190522154422370

前后端分离:

bowser —> node server —> spi service

很重要的一点是, 如果我们进行前后端分离后, 我们的前端服务器和后端服务器可能不在同一个域, 跨域访问很重要

image-20190522160445522


> 编写代码这堂课是干货满满, 也就是第五节课, 讲了很多的编程方法和封装方法, 很实用, 包括返回之类的, 可以看出来该讲师技术不错 > > 例如Result<object> 不写Result<uservo> 是为了避免出现一些黄色警告, 还是要写通用的返回类型才可以 > > 包括通用的模板返回写法顺序 > > image-20190522170323658

开始编写登录代码

首先在controller包下面新建一个api包, 用于我们这次的代码controller编写,

> 不写@RequestMapping(value = "/login", method = RequestMethod.POST)的method = RequestMethod.POST的话默认就是get方法

如果我们返回的代码要返回一个JSON格式的结果, 有如下两个方法:

  1. 在方法前面加入@ResponseBody
  2. 在类前面把@Controller改为@RestController, 因为RestController里面已经放了@ResponseBody, 相当于该类下面的所有方法都自动加上了 @ResponseBody

运行代码, 输入http://localhost:8080/api/login?username=001&password=123456

发现返回的页面地址没有变化, 但是里面有了数据, 是我们返回的JSON数据

image-20190522171420840


开放跨域访问

开放跨域是在我们对应的方法上进行添加, 例如:

image-20190522172138931

CrossOrigin代表跨域,origin代表允许跨域地址,*代表都可以,allowCredentials代表是否允许跨域, value ={""}代表白名单, 写上去的话就代表只有这些地址才可以进行访问我, 不写的话就都可以


有一点优化就是, 发现requestmapping都是有加上api路径, 但是我们希望我们在编写的时候就不写api路径, 对此怎么解决?

解决可以通过继承一个封装好的api路径截取的类就可以了

image-20190522210425939

image-20190522210447358

写上继承以后, 这个类之后的地址就不要加api路径地址了, 直接写后面的地址就可以

然后因为我们是api, 所以可以直接在BaseApiController直接加上@CrossOrigin(origins = "*", allowCredentials = "true")来开放继承他的api

image-20190522211100671


用mock用具测试后端接口api

image-20190522211313475

mock是用来模拟浏览器进行的一些http数据请求

mock我们一般也是用单元测试来做的,

在测试中, mock需要进行初始化上下文, 通过mockmvcbuilders来进行创立webapplicationContext上下文

mockMvc.perform(RequestBuilder)

mockMvc.perform()用来发送请求, 传入的参数是一个RequestBuilder, 也可以传入一个静态请求可以是get也可以是post, 传入的参数用param键值对

image-20190522222133257

为了确保传入的参数统一, 我们需要进行contentType()进行转换,以JSON的格式进行传输

.andExpect(status().isOk())是设置一个预期, 只有在登录成功的时候才返回结果,status()也是一个静态方法,是mock的, isOk()是200值的时候返回

同时也可以预期设置为接受JSON格式, content有两个方法, 一个是request一个是result, 我们这里做的是result筛选, JSON可以返回UTF8格式

package com.sansenBlog.blog;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;


import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest
public class LoginApiControllerTest {

    // --------------  初始化mock-start ----------------------
    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context; // 用于上下文

    // user for initializing some properties
    @Before
    public void setUp(){
           this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }
    // ------------- 初始化mock-end ---------------------------

    @Test
    public void contextLoads() throws Exception {
        MvcResult result = mockMvc.perform(post("/api/login").param("username", "001").param("password", "123456")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andReturn(); // 用来发送http请求, 可以传入静态方法post或者get
        System.out.println(result.getResponse().getContentAsString());
    }
}

用andReturn()来返回结果, 我们用MvcResult来接受返回结果

运行测试结果如下:

image-20190522222310561


前端部分

vue中文文档查看: https://vuejs.bootcss.com/v2/guide/syntax.html

花费点时间全部看一遍

vue-cli脚手架安装和目录讲解

在想要放前端目录的文件夹下去打开终端

image-20190522222553150

然后运行命令, 后面的blog-front是项目名称

vue init webpack-simple blog-front

image-20190522222929509

然后为了能把项目先跑起来可以按照他下面给的命令来做

 cd blog-front
 npm install
 npm run dev // 热部署

> 国内下载速度比较慢 > 查看npm当前下载镜像,命令为:npm get registry > 结果> https://registry.npmjs.org/ > 如果嫌弃下载慢可以切换为淘宝镜像或者直接安装cnpm并更换淘宝镜像,命令如下: > > &gt; npm config set registry http://registry.npm.taobao.org/ &gt; npm install -g cnpm --registry=https://registry.npm.taobao.org &gt;

然后编码的话可以在atom里面编码也可以在webstorm里面进行编码, 这里如果是atom就要去安装下插件, 不然没那么方便

  • language-vue
  • Vue-autocomplete
  • Platformio-ide-terminal

这里我选择使用webstorm来进行开发

![image-20190522225320213](/Users/achunorigin/Library/Application Support/typora-user-images/image-20190522225320213.png)

在vue里面, 我们的入口只有一个index.html

然后运行npm run dev后进入页面

image-20190522225552376

页面位置是App.vue, main.js是我们整个vue的js入口

image-20190522225525624

.vue文件有三大模块, 分别是template, script, style

一个template表示一个主键

他会把类似复制粘贴形式放入到index.html里面

image-20190523083759480

放入到

image-20190523083939836

对接方式是通过他们的id来一一对应


asserts文件夹是放入的静态的资源文件, 例如图片


Vue入门之数据处理

image-20190523090029784

  1. vue数据绑定 v-bind v-model

数据绑定获取script的值可以使用双大括号{{}}来实现

v-bind是单向绑定, 就是可以读取值, 但是不能写入值, 有点像${}

作用是可以在script里面去修改html的属性

image-20190523091210701

image-20190523091136621

v-bind也有简写形式, 直接:后面跟上properties就可以了, 例如, :value="msg"


有单向绑定就有双向绑定, 使用的是v-model

不同与v-bind的是, 里头的数据发生改变的时候就会发生同步变化, 比如input的text里面的值发生改变, 我们的绑定的value值也会发生改变

image-20190523092702534

  1. 事件和方法 methods v-on

html内模板属性的书写方式要发生一点点改变, 例如

原来是 onclick="" 写法 修改成v-on:click=""

v-on:也有简写, 简写为符号@

image-20190523092222083

结果如下:

image-20190523092243270

  1. 计算属性 computed

计算操作, 可以返回一个function, 返回一个计算后的结果, 调用方式是

fullname computed: {{fullname}}<br>
...
  computed:{
    fullname: function () {
      return this.firstname + ' ' + this.lastname;
    }
  }

image-20190523100213197

结果如下: image-20190523093626148

> computed 和 methods的区别是 > > computed会缓存, 重复调用相同计算的时候可以节省性能 > > methods没有缓存 > > 我们日常用到最多的会是计算属性, 所以叫计算属性, 计算属性做不到的时候再用监听属性 > >

  1. 监听属性 watch

watch是监听改变, 其他属性是直接调用, watch是监听变化, 例如可以监听data属性内的值

image-20190523094450910

vue条件语法

v-if, v-else-if, v-else

image-20190523101621694


v-show

> v-show相当于在html里面定义一个css属性, 设置display:none > > v-if元素完全就不存在了, 这是vue的机制, 只加载有效的dom > > image-20190523104023453

image-20190523104144739

v-for的用法

:key的话要求是不能重复的,可以绑定主键, 要写上, 可以在后台提高我们的遍历性能

<li v-for="(item, index) in userList" :key="index">
  {{item}}
</li>

image-20190523103106410


vue的链接颜色是color: #42b983

也可以通过if-else选取style

image-20190523111743895

vue组件

组件一般放在src目录下, 命名为components文件夹, vue的文件后缀名都是vue, 这里我们新建一个button的vue组件

image-20190523113020158

@click=""里面是可以直接使用表达式的

image-20190523113404067

image-20190523113721499

组件的定义与使用

> JavaScript &gt; export default { &gt; name: '...' &gt; } &gt; > > 中name的作用有三种 > > 1. 可以递归调用自己, 组件调用自己, 依靠的就是那么 > 2. 方便调试, 调试里看到的组件就是根据name显示 > 3. include和exclude指定需要缓存和不需要缓存的组件,指定的依据就是组件的name

template标签下只能有一个跟元素标签, 有且只能有一个

组件插槽slot

用组件也可以实现插槽的效果, 但是是大材小用了, 主要是很不方便

有时候我们只要引入一两句组件的话就可以用到插槽

比如在app.vue里面有我们要的一个span, 然后放入到buttonCounter.vue里面

就可以在app.vue里面直接写入一个span

然后在ButtonCounter.vue里面直接进行slot插槽调用

slot插槽相当于一个占位用的标签

image-20190523144921398

ButtonCounter.vue

image-20190523145247101

组件传参props

需要用到一个props, 是properties的简写

要改变组件内部的信息除了使用插槽外, 还可以使用组件传参

例如把一个参数传入到组件内部的话就要用到组件传参, 因为组件之间是不互通的

> 小知识, 在HTML标签里面用的是例如, props-name > > 在js里面一般用的是大写的 , propsName

image-20190523150821583

> 并且如果在html中没有加v-bind或者: 就会认为是原样输出, > > 加了才会以变量的形式输出

image-20190523150911846


使用vue-router

是一个路由管理器, 类似后端controller层

首先进行安装

npm i vue-router -d

其中i代表install, -d代表安装到我们的目前开发项目中去, 不然就安装到系统了

安装完成后就在main.js里面进行引入

image-20190523200200303

具体步骤是:

  1. 引入组件
  2. 使用路由模块
  3. 定义路由
  4. 根据路由创建实例
  5. 引入实例

然后在app.vue增加路由引用

只有用到<router-view></router-view>路由才能生效

image-20190523200701130

image-20190523200829794

如果觉得#号不美观,可以在配置中去掉它

image-20190523201005298

通过在定义实例的时候加入mode:'history'可以去掉/#/


如果想要在界面内进行跳转就要使用

<router-link to="/foo">GO to foo</router-link>

这种方式

image-20190523201536465

如果想要在跳转的时候做点事情,就可以使用编程式的路由跳转, 就可以使用router.push()的方法来进行跳转, 这也是最常用的方法

image-20190523202340159


然后去编写登录功能组件的跳转

先创建一个Login组件

然后在main.js里面去加入路由跳转, 但是可以发现一个问题是, 如果我们以后写的页面越来越多, 那么意味着main.js会越来越大, 这样肯定是很不好的

对此, 我们一般的处理方法是把router放到外面去, 例如新建一个js目录然后新建一个router目录再建立一个routes.js, 把所有页面跳转全部放入到这里, 通过es6的方式暴露出去

image-20190523204053314

image-20190523204121326

使用element-ui完成界面

element.u是饿了么团队开源的ui框架

官方文档为:https://element.eleme.cn/#/zh-CN/component/installation

首先进行安装

npm i element-ui -S

然后在main.js里面去引入

image-20190523204821970

然后运行发现报错

image-20190523205229398

是不认识ttf格式的文件

那么就要在webpack中去加入该格式的支持

ttf是一种字体文件, 也缺少woff字体格式

image-20190523205450624

然后拷贝静态资源文件到asserts文件夹下, 用element-ui来进行界面构建

需要在<style>里面去@import一些css样式

在app.vue里面加入如下样式:

  @import './assets/css/replace-elementui.css';

  html {
    height: 100%;
    margin: 0;
    padding: 0;
  }

  body {
    height: 100%;
    margin: 0;
    padding: 0;
    font-family: "PingFang SC", "Helvetica Neue", "Hiragino Sans GB", "Segoe UI", "Microsoft YaHei", "微软雅黑", sans-serif;
  }

  #app {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }

  a {
    text-decoration: none;
  }

login.vue按照element官网表单验证来进行填写, 要写rules在表单处和prop在输入的label处

<template>
  <div id="portal-template">
    <img class="portal-logo" src="../assets/images/logo.png"/>
    <div id="login">
      <el-form :rules="rules"  ref="ruleForm" :label-position="labelPosition"
               label-width="80px" :model="user">
        <el-form-item label="账号" prop="username">
          <el-input v-model="user.username" placeholder="请输入账号" ></el-input>
        </el-form-item>
        <el-form-item label="密码"  prop="password">
          <el-input v-model="user.password" placeholder="请输入密码"></el-input>
        </el-form-item>
        <el-button class="submit-btn" type="primary" @click="login('ruleForm')">登录</el-button>
        <el-button type="primary">注册</el-button>
      </el-form>
    </div>
  </div>

</template>

<script>
  export default {
    name: "Login",
    data() {
      return {
        user: {
          username: '',
          password: ''
        },
        labelPosition: 'top',
        rules: {
          username: [
            {required: true, message: '请输入用户名', trigger: 'blur'}
          ],
          password: [
            {required: true, message: '请输入密码', trigger: 'blur'}
          ]
        }
      }
    },
    methods: {
      login(formName){
        this.$refs[formName].validate((valid) => {
          if (valid) {
            alert('submit!');
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      }
    }
  }
</script>

<style lang="scss">
  @import "../assets/css/portal-template.css";
</style>

效果如下:

image-20190524090604008

使用axios完成异步请求api

有点类似ajax, 完成异步请求的工具

安装axios

需要先安装

运行

npm i axios -d
npm i vue-axios -d
npm i qs -d

axios使用方法看https://github.com/axios/axios

安装后到main.js中去引入vue-axios和axios

image-20190524094732818

要启动前后端同时的话, 端口就不能重复

因此我们去修改前端端口

去webpack的devserver里面全修改

image-20190524095741824

修改端口为8081, 然后在login.vue组件里面去用axios发送异步请求, 去验证,记得还是要加入完整的http地址, 不然就是用原来的域去访问

image-20190524101536497

然后打断点在前端按f8一步步看下去, 显示的具体内容

image-20190524101819344

当我们正经输入的时候可以登录成功

然后为了方便我们不每一次都写入地址和其他重复的参数, 我们需要对其进行封装

写在main.js里面

image-20190524104710217

// 定义默认Axios行为
Axios.defaults.baseURL = 'http://localhost:8080';
Axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
Axios.defaults.withCredentials = true; // 这个是用作跨域请求,如果要进行post就要打开这个开关

之后就是要做post表单提交

这里使用qs来转换我们的请求

要注意this的作用域范围, 指向的都是这一层所用的

image-20190524105941766

// 因为是组件,所以要加上this才能访问到
            this.axios.post('/api/login', qs.stringify({username:this.user.username,password:this.user.password}))
              .then(function (response) {
                // handle success
                var result = response.data;
                if (result.code == 200){
                  alert(result.message);
                }else{
                  alert(result.message);
                }
              })
              .catch(function (error) {
                // handle error
                alert("error"+error);
              })
              .finally(function () {
                // always executed
              });

然后进行测试, 发现可以登录成功, 如下图

image-20190524110205729

但是我们发现提示框太丑了, 可以改用element的错误提示框

image-20190524110632171

> 使用this的时候通常都是表示我们组件本身, 也可以继承他的父类所拥有的属性 > > 但是如果是多层嵌套的this就很容易出错 > > 我们需要对我们的this进行保存 > > 例如使用: var self = this;来保存当前的this来避免作用域发生混乱 > > image-20190524111011601 > > 这样就ok

    methods: {
      login(formName) {
        var self = this;
        self.$refs[formName].validate((valid) =&gt; {
          if (valid) {
            // 因为是组件,所以要加上this才能访问到
            this.axios.post('/api/login', qs.stringify({username:self.user.username,password:self.user.password}))
              .then(function (response) {
                // handle success
                var result = response.data;
                if (result.code == 200){
                  self.$message({
                    message: result.message,
                    type: 'success'
                  });
                }else{
                  self.$message({
                    message: result.message,
                    type: 'error'
                  });
                }
              })
              .catch(function (error) {
                // handle error
                alert("error"+error);
              })
              .finally(function () {
                // always executed
              });
          } else {
            console.log('error submit!!');
            return false;
          }
        });
      }
    }

使用vue-x管理登录状态

参考文旦为vue官网的参考:https://vuex.vuejs.org/zh/guide/

vuex是一个状态管理的模式, 可以给我们一个单向流的管理

image-20190524112007125

例如, 我们在登录的过程中, 有同样一个用户去改变你的登录状态, 那到底是登录了还是没登录?

我们希望状态是惟一的就要使用vuex来管理我们的状态

我们vue开发的都是单页面应用

单页面应用所有的路由管理都是在前端

不会每一次都去访问后端来进行跳转

如果在前端路由跳转中状态发生改变应该以哪个状态为准, 就是靠的vuex

vuex是一个全局状态管理, 有四个属性

image-20190524112646251


使用vuex

首先肯定需要进行引入vuex

npm i vuex -d
import Vuex from 'vuex'
Vue.use(Vuex);

这个是最基本的就不解释了

![image-20190524204633412](/Users/achunorigin/Library/Application Support/typora-user-images/image-20190524204633412.png)

然后需要new一个store对象来存储我们的状态, 也是在main.js中

image-20190524203403284

// vuex
// payload代表传过来的参数
const store = new Vuex.Store({
  state: {
    username: 0
  },
  mutations: {
    changeUsername(state, payload) {
      state.username = payload.username;
    }
  },
  actions: {
    // action需要传入上下文context, 通过action来改变mutations
    // 触发action需要用到store.dispatch()来触发
    changeMyUsername(context, payload){
        context.commit('changeUsername', payload);
    }
  }
});

和前面说的一样, vuex 里面有四个参数, 都是单向调用的, 我们在外部调用action就可以了, 因为只有action是开始的头

image-20190524203706864

用dispatch来访问action, 第二个参数是payload, 用来传递参数用的


那么怎么获取state值呢, 可以通过store的getter来进行调用

image-20190524204036073

然后在foo.vue里面也就是我们跳转的页面里面写上我们的结果

image-20190524204843906

结果如下:

image-20190524220556625

但是我们发现. 由于之后我们可能store写的会越来越大, 就有可能在main.js中很难看, 因为我我们所有东西基本上都是放在main.js上因此, 还是要少点的好

> 看着看着发现.https://zhuanlan.zhihu.com/p/55795638这篇文章讲的不错, 是关于css的

就把store移出去, 写成js, 但是也是还要问题, 如果我们store有很多, 那么是不是要写好多的js文件

对此, vuex给出的解决方案是使用module将store分割成不同的模块

每一个模块拥有自己的state. mutation. action, getter

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -&gt; moduleA 的状态
store.state.b // -&gt; moduleB 的状态

然后用我们的modules来最后统一管理我们的module,这样就很优秀

image-20190524223145061

具体逻辑就是像这样

image-20190524223815934

其他部分具体的逻辑代码就不写了

逻辑就是: main.js里面去引入我们的store管理者modules

引入后再去modules里面去import我们的其他部分store的子module

简单说就是拆分啦


还有一个可以优化的地方, 在我们的login.vue里面验证判断的请求, 其实也是个状态改变, 那我们应该把这些状态改变放入到我们的login-store里面去

image-20190524224642289

放心去后再对代码进行修改

放进去后发现self对象会失效, 是因为self是我们vue对象的, 因此要把vue对象引入进来

image-20190524224924662

引入后修改一些login.vue到login-store的一些传参, 例如传入和接受 改为payload之类的

修改完成对于成功与否的消息返回可以使用es6的promise

</uservo></object></user></user></string></string></t></string,></string,object></t></user></user></user></string,></string,object></t></string,></string,></string,></string,></string,></string,></string,></string,></string,></string,></string,></string,></string,></实体类>

转载于:https://my.oschina.net/sansenlian/blog/3070915

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值