Spring Boot之Kaptcha验证码使用

1.引入Kaptcha依赖

<!-- 验证码 -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

2.KaptchaBean.java 配置文件

package com.wjy329.shiro.filter;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.servlet.KaptchaServlet;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class KaptchaBean {
    @Bean
    public org.springframework.boot.web.servlet.ServletRegistrationBean kaptchaServlet() {

        org.springframework.boot.web.servlet.ServletRegistrationBean registrationBean = new org.springframework.boot.web.servlet.ServletRegistrationBean(new KaptchaServlet(), "/kaptcha.jpg");

        registrationBean.addInitParameter(Constants.KAPTCHA_SESSION_CONFIG_KEY,
                Constants.KAPTCHA_SESSION_KEY);
        registrationBean.addInitParameter(Constants.KAPTCHA_IMAGE_HEIGHT, "38");//高度
        registrationBean.addInitParameter(Constants.KAPTCHA_IMAGE_WIDTH, "80");//宽度
        registrationBean.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "26");//字体大小
        registrationBean.addInitParameter(Constants.KAPTCHA_BORDER_THICKNESS, "1"); //边框
        registrationBean.addInitParameter(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "red"); //文字颜色

        //可以设置很多属性,具体看com.google.code.kaptcha.Constants
//      kaptcha.border  是否有边框  默认为true  我们可以自己设置yes,no
//      kaptcha.border.color   边框颜色   默认为Color.BLACK
//      kaptcha.border.thickness  边框粗细度  默认为1
//      kaptcha.producer.impl   验证码生成器  默认为DefaultKaptcha
//      kaptcha.textproducer.impl   验证码文本生成器  默认为DefaultTextCreator
//      kaptcha.textproducer.char.string   验证码文本字符内容范围  默认为abcde2345678gfynmnpwx
//      kaptcha.textproducer.char.length   验证码文本字符长度  默认为5
//      kaptcha.textproducer.font.names    验证码文本字体样式  默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
//      kaptcha.textproducer.font.size   验证码文本字符大小  默认为40
//      kaptcha.textproducer.font.color  验证码文本字符颜色  默认为Color.BLACK
//      kaptcha.textproducer.char.space  验证码文本字符间距  默认为2
//      kaptcha.noise.impl    验证码噪点生成对象  默认为DefaultNoise
//      kaptcha.noise.color   验证码噪点颜色   默认为Color.BLACK
//      kaptcha.obscurificator.impl   验证码样式引擎  默认为WaterRipple
//      kaptcha.word.impl   验证码文本字符渲染   默认为DefaultWordRenderer
//      kaptcha.background.impl   验证码背景生成器   默认为DefaultBackground
//      kaptcha.background.clear.from   验证码背景颜色渐进   默认为Color.LIGHT_GRAY
//      kaptcha.background.clear.to   验证码背景颜色渐进   默认为Color.WHITE
//      kaptcha.image.width   验证码图片宽度  默认为200
//      kaptcha.image.height  验证码图片高度  默认为50
        return registrationBean;

    }
}

3.前端页面

<div class="layui-form-item">
    <div class="layui-inline">
    <label class="layui-form-label">验证码</label>
    <div class="layui-input-inline">
        <input type="text" name="captcha" required lay-verify="required" placeholder="请输入验证码" autocomplete="off" class="layui-input" style="width: 110px;float: left">
        <img src="kaptcha.jpg" name="captcha"  id="captcha" style="float: right" onclick="refreshCode(this)"/>
    </div>
</div>

这里的 onclick="refreshCode(this)"  是点击刷新验证码;

配置完上面3步可以访问你的页面看看验证码是否显示,如果显示则配置成功,就可以进行后续判断验证码阶段。

image.png

4.判断验证码

我这里由于使用了shiro(其他也一样),我简化了判断的方法,直接在你登陆的控制器内判断即可,Kaptcha会把前端的验证码信息存放到session中,所有我们在登陆的控制器用session中的验证码和前端表单传过来的验证码比较即可;

public String loginUser(@RequestParam("username")String username,
                        @RequestParam("password")String password,
                        @RequestParam("captcha") String captcha,
                        HttpSession httpSession){
    String sessionCaptcha = httpSession.getAttribute("KAPTCHA_SESSION_KEY").toString();
    if(!captcha.equals(sessionCaptcha)){
        return WebUtils.getInstance().writeMsg(ResultCode.ERROR, "验证码输入错误");
    }
    ......
 }

上面是我的验证代码,可以参考一下,注意获取session中的验证码的key为   “KAPTCHA_SESSION_KEY”  

5.点击刷新验证码

前面也说了点击刷新,就是一个方法,需要加入时间更新,代码如下:

function refreshCode(){
    var captcha = document.getElementById("captcha");
    captcha.src = "/kaptcha.jpg?t=" + new Date().getTime();
}

以上就是在项目中添加验证码的方法,希望有所帮助。一些配置项可以查看第2步中的配置类或者百度(-_-)。

Spring Boot之分页

分页是我们 常用的功能,MySQL实现分页的原理 就是用limit,比如 select * from table limit 0,5;  意思就是从0行开始查询5条数据,那么要实现分页我们需要两个参数,起始位置和每页的数据量,起始位置需要计算,为了前端传值的方便,前端只需传入当前页码每页数据量即可,下面我们来具体实现:

数据准备:

-- ----------------------------
-- Table structure for person
-- ----------------------------
DROP TABLE IF EXISTS `person`;
CREATE TABLE `person` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of person
-- ----------------------------
INSERT INTO `person` VALUES ('1', 'wjy', '24');
INSERT INTO `person` VALUES ('2', 'wjy1', '25');
INSERT INTO `person` VALUES ('3', '25', '25');
INSERT INTO `person` VALUES ('4', 'wjy2', '25');
INSERT INTO `person` VALUES ('5', 'wjy3', '25');
INSERT INTO `person` VALUES ('6', 'wjy4', '26');
INSERT INTO `person` VALUES ('7', 'wjy5', '27');
INSERT INTO `person` VALUES ('8', 'wjy0', '24');

image.png

1.分页实体 

PageBean.java:

package com.wjy329.pagedemo.utils;

import java.util.List;

public class PageBean<T> {
    //起始
    private int offset;
    // 当前页
    private int currentPage;
    // 每页显示的数据量
    private int pageCount;
    // 总记录数
    private int totalCount;
    // 总页数 = 总记录数 / 每页显示的行数  (+ 1)
    private int totalPage;
    // 分页查询到的数据
    private List<T> pageData;

    public int getOffset() {
        return offset;
    }

    public void setOffset(int offset) {
        this.offset = offset;
    }

    // 返回总页数
    public int getTotalPage() {
        if (totalCount % pageCount == 0) {
            totalPage = totalCount / pageCount;
        } else {
            totalPage = totalCount / pageCount + 1;
        }
        return totalPage;
    }
    public void setTotalPage(int totalPage) {
        this.totalPage = totalPage;
    }

    public int getCurrentPage() {
        return currentPage;
    }
    public void setCurrentPage(int currentPage){
        this.currentPage = currentPage;
    }
    public int getPageCount() {
        return pageCount;
    }
    public void setPageCount(int pageCount) {
        this.pageCount = pageCount;
    }
    public int getTotalCount() {
        return totalCount;
    }
    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }

    public List<T> getPageData() {
        return pageData;
    }
    public void setPageData(List<T> pageData) {
        this.pageData = pageData;
    }
}

2.分页工具类

PageUtils.java:

package com.wjy329.pagedemo.utils;

import java.util.List;

public class PageUtils {
    public static PageBean getPage(Integer currentPage, Integer pageCount, Integer total){
        PageBean pageBean = new PageBean();
        //如果传入的当前页面为空,那么默认为第一页
        if(currentPage == null){
            currentPage = 1;
        }
        //如果传入的数据量为空,默认显示5条
        if(pageCount == null){
            pageCount = 5;
        }
        //起始位置的计算
        int offset  = (currentPage-1)*pageCount;
        pageBean.setCurrentPage(currentPage);
        pageBean.setPageCount(pageCount);
        pageBean.setTotalCount(total);
        pageBean.setOffset(offset);

        return pageBean;
    }
}

3.实体类

person.java:

package com.wjy329.pagedemo.entity;


import java.io.Serializable;

public class Person implements Serializable {
    private Integer id;

    private String name;

    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

4.PersonMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.wjy329.pagedemo.dao.PersonDao">
    <resultMap id="BaseResultMap" type="com.wjy329.pagedemo.entity.Person">
        <id column="id" jdbcType="INTEGER" property="id" />
        <result column="name" jdbcType="VARCHAR" property="name" />
        <result column="age" jdbcType="INTEGER" property="age" />
    </resultMap>

    <select id="findAllByPage" parameterType="com.wjy329.pagedemo.utils.PageBean" resultMap="BaseResultMap">
        SELECT id,name,age
        FROM person
        LIMIT #{page.offset},#{page.pageCount}
    </select>

    <select id="getAllCnt"  resultType="java.lang.Integer">
    select count(id) from person
  </select>

</mapper>

5.PersonDao.java

package com.wjy329.pagedemo.dao;

import com.wjy329.pagedemo.entity.Person;
import com.wjy329.pagedemo.utils.PageBean;
import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface PersonDao {
    List<Person> findAllByPage(@Param(value="page")PageBean page);

    Integer getAllCnt();
}

6.PersonService.java

package com.wjy329.pagedemo.service;

import com.wjy329.pagedemo.entity.Person;
import com.wjy329.pagedemo.utils.PageBean;

import java.util.List;

public interface PersonService {
     PageBean queryByPage(Integer currentPage, Integer pageCount);
}

7.PersonServiceImpl.java

package com.wjy329.pagedemo.service.impl;

import com.wjy329.pagedemo.dao.PersonDao;
import com.wjy329.pagedemo.entity.Person;
import com.wjy329.pagedemo.service.PersonService;
import com.wjy329.pagedemo.utils.PageBean;
import com.wjy329.pagedemo.utils.PageUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class PersonServiceImpl implements PersonService {

    @Autowired
    private PersonDao personDao;

    @Override
    public PageBean queryByPage(Integer currentPage,Integer pageCount) {
        Integer total = 0;
        List<Person> persons  = null;
        total = this.personDao.getAllCnt();
        PageBean pageBean = PageUtils.getPage(currentPage,pageCount,total);
        //查询出来的分页数据
        persons = this.personDao.findAllByPage(pageBean);
        pageBean.setPageData(persons);
        return pageBean;
    }
}

8.PersonController.java

package com.wjy329.pagedemo.controller;

import com.wjy329.pagedemo.entity.Person;
import com.wjy329.pagedemo.service.PersonService;
import com.wjy329.pagedemo.utils.PageBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@Controller
@RequestMapping("/person")
public class PersonController {
    @Autowired
    private PersonService personService;

    @RequestMapping("/getAll")
    @ResponseBody
    public PageBean getAll(Integer currentPage, Integer pageCount){
        PageBean list = personService.queryByPage(currentPage, pageCount);
        return list;
    }
}

上面就是全部的代码,然后我们开始访问: localhost:80/person/getAll   由于没有传入参数,所以按工具类中的默认 值查询,也就是第一页,5条数据:

image.png

访问: localhost:80/person/getAll?currentPage=2&pageCount=5   返回第2页的5条数据

image.png

上面就是分页的简单实现,然后据需要配合前端 的页面传值和获取 数据等。

demo地址: https://github.com/wjy329/page-demo

JS密码强度判断

 //判断密码规则,长度至少为8位,必须包含大小写字母、数字和特殊符号
 var reg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}/;
 var result = $("#newPassword").val().match(reg);
 if(result == null){
     Msg.error("密码长度至少为8位,必须包含大小写字母、数字和特殊符号");
     return false;
 }

上面代码是验证密码长度至少为8位,而且必须包含大小写字母、数字和特殊符号。

JAVA学习之HASHMAP底层(二)

这篇文章让我们了解下高并发下的HashMap。

HashMap随着插入的数据的增多,使得key值的冲突会越来越多,这时候HashMap就会进行扩容。

先看一下扩容的条件:

HashMap.size()>=Capacity*LoadFactor

其中HashMap.size()是HashMap的长度,Capacity是HashMap的容量,LoadFactor是负载因子(默认值为0.75f)

当满足扩容条件时,HashMap会先新建一个容量为之前2倍的Entry,然后遍历之前的Entry,重新Hash到新的数组

因为index = HashCode(key) & (length -1 ) ,新的数组长度变了,所以都得重新计算。

HashMap在多线程下会出现死循环,死循环发生在扩容的第二步,也就是重新Hash那一步,先把源码附上,以后能看懂了再分析。。。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

MySQL必知必会(一)

和题目一样,通过《MySQL必知必会》这本书记录一下MySQL应该掌握的知识。

前提当然是你已经安装了MySQL,至于如何安装MySQL,网上也有很多,这里不做记录

1.使用MySQL
打开终端,输入:                                                               

mysql -u username -p

然后回车,输入相应的密码即可;

#显示当前所有的数据库
show databases;
#选择数据库,dataname就是你要选择的数据库名字
use dataname;
#显示当前数据库下的所有表
show tables;
#显示tablename这个表的列
show columns from tablename;
#显示tablename列的第二种方法(更加简洁)
describe tablename;

关于show语句还有很多,我们可以输入help show来显示允许使用的show的语句,同时,我们也可以使用 help  xxx 来显示所支持的xxx的相关语句。

上面几行命令简单的介绍了MySQL的基本操作;

2.检索数据

#检索单个列,表示从studnet表中查出名为id的列
select id from student;
#检索多个列,表示从student中查出名为id和name的列,多个字段用,隔开
select id,name from student;
#检索所有列,使用 * 表示返回表中所有的列
select * from student;
#去重,使用distinct可以只显示不同的id
select distinct id from student;
#限制结果,返回不多于5行的数据
select id from student limit 5;
#返回从行3开始的5行数据,第一个数字为开始位置,第二个数字为检索的行数
select id from student limit 3,5;

3.排序检索数据

#根据id排序查询出来的id
select id from student order by id;
#多个列排序,先按照id排序,如果id相同则按照name排序
select id,name from studnet order by id,name;
#指定排序顺序,asc 升序(默认)    desc 降序
select id from student order by id desc;
#使用order by 和 limit 组合可以找出最高或最低的数据
#查询年龄最大的学生的年龄
select age from student order by age desc limit 1;

4.过滤数据

where语句的使用;

#查询id为1的学生的年龄
select age from student where id = 1;

where语句常用的操作符

操作符 说明
= 等于
<> 不等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于
between 在指定的两个值之间

组合语句过滤:

#查询id为1和年龄小于20的所有学生的名字和年龄
select name,age from student where id = 1 and age < 20;
#查询id为1或者2的学生的姓名
select name from student where id =1 or id = 2;

where可以包含任意数目的and和or操作符,但是需要注意优先级,and的优先级高于or,但是一般推荐组合时使用()

#查询id为1和5的学生的年龄
select age from student where id in (1,5);
#查询除id为1、5的学生的年龄
select age from student where id not in (1,5);

用通配符进行过滤-like的用法

#查询名字以wjy开头的所有学生的信息
select * from student where name like 'wjy%';
#查询名字包含wjy的学生的信息
select * from student where name like '%wjy%';
#查询名字以w开头,y结尾的学生的信息
select * from student where name like 'w%y';
#查询名字为w_y,_代表一个字符
select * from student where name like 'w_y';

“_”“%”的区别就是:% 可以代表任意字符,_只可以匹配单个字符;

通配符的处理所花时间较长,不要过度使用。

5.函数

函数一般用在数据上的执行,给数据的转换和处理提供了方便。

常用的文本处理的函数:

函数 说明
Left() 返回串左边的字符
Length() 返回串的长度
Locate() 找出串的一个子串
Lower() 将串转为小写
LTrim() 去掉串左边的空格
Right() 返回串右边的字符
RTrim() 去掉串右边的空格
Soundex() 返回串的SOUNDEX值
SubString() 返回子串的字符
Upper()

将串转换为小写

当然还有时间处理的函数、数值处理的函数等等,这里不对这些介绍,接下来看看数据汇总的一些函数。

AVG()函数:求平均值

#求出student表中所有的年龄的平均值 as 用来表示结果的别名;
select avg(age) as avg_age from student;

COUNT() 函数:进行计数

#返回学生表中学生的总数
select count(*) as num_studnet from student;
#返回address的总数,表示表中登记了地址的学生数
select count(address) as num_student from student;

MAX()函数:返回指定列中的最大值

#返回学生中年龄最大的年龄值
select max(age) from student;

MIN()函数:返回最小值,不作示例

SUM()函数:返回指定列的和

#计算所有学生的年龄和
select sum(age) from student;

6.分组

#根据班级class_id分别统计出每个班级的学生数
select class_id,count(*) as num from student group by class_id;

MySQL也允许过滤分组,之前的where是对行进行过滤,对分组进行过滤需要使用having

#过滤出学生数大于20的班级
select class_id,count(*) as num from student group by class_id having count(*) > 20;

7.子查询

子查询就是嵌套在其他查询中的查询;

比如说有三张表客户表customers、订单表orders、订单明细表orderitems;

需要列出订购物品book121的所有客户,我们可以分为3步:

(1)在订单明细表中查出包含book121的所有订单编号;

(2)根据订单编号在订单表中查出客户id;

(3)根据客户id在客户表中查询详细信息;

#执行上面的第一步
select order_num from ordersitems where prod_id = 'book121';
#假设返回的结果是 10001,10005 两个订单号
#执行第二步,根据10001,10005查询客户id
select cust_id from orders where order_num in (10001,10005);
#假设返回的结果是 20001,20005另个客户id
#执行第三步,根据客户id查询客户信息
select cust_name,cust_contact from customers where cust_id in (20001,20005);

上面的sql语句是每一步的执行过程,这时候我们将它们嵌套一下:

select cust_name,cust_contact from customers
from customers
where cust_id in (select cust_id
                    from orders
                    where order_num in (select order_num
                                        from orderitems
                                        where prod_id = 'book121'));

JPress使用

JPress是一个用Java语言编写的开源的快速建站平台,一般用于博客的快速搭建,类似于大名鼎鼎的WordPress。

JPress官方网站:http://www.jpress.io

首先  git clone https://gitee.com/fuhai/jpress.git  下载项目

然后用shell进入项目的目录,执行mvn package 命令

image.png

执行完毕后,可以看到在start-tomcat/target下生成stater-tomcat-1.0.war 

image.png

然后我们在MySQL中新建一个数据库,运行项目目录下的db.sql,创建表;

将刚才生成的war包拷贝到tomcat的webapps目录下,然后解压。

image.png

找到WEB-INF下的classes文件夹下的jboot.properties,修改为自己的数据库配置

image.png

然后启动tomcat 就可以访问了。。。

image.png

访问 http://127.0.0.1:8080/starter-tomcat-1.0/

跳转到初始化界面

image.png

填好自己的信息后,就可以管理自己的博客了。

默认的后台界面:

image.png

默认的主页:

image.png

剩下的东西自己探索,或者参考官方的文档,写的很是详细。

Java学习之HashMap底层(一)

HashMap作为常用和常考的Java映射,有必要在详细的学习一下,结合多篇博客和资料的理解,简单记录下个人的想法。

HashMap的每一个键值对都是一个Entry,一个个的键值对(Entry)分散存储在数组中,构成了HashMap的主干,每个Entry实际上是作为一个链表的头结点,当后面的key的index冲突的时候,就在Entry之后添加一个新的节点。

面试中,我们常常回答:HashMap 的底层是数组加链表的结合体,JDK1.8中添加了红黑树。这个以后再研究。

下面我们通过一个图来直观的感受一下:

image.png

HashMap中用来处理数组索引的哈希函数是: index  = hashCode(key) & (length – 1)  其中length是HashMap 的长度。

HashMap的默认初始长度是16,扩容的阈值是0.75,每次扩容为原表数量的2倍(必须是2的幂)

当计算出来的key的index冲突后,新的key就会充当头结点,因为HashMap的发明人认为后插入的数据可能被查找的几率更大。

再谈一下为什么初始长度是16,简单的来讲是为了让index均匀分布:

根据上面的哈希函数,length-1 = 16 -1 =15,二进制为:1111 

假设有三个key的hashCode是1001、1011、1111,它们分别与1111进行& 运算

image.png

将它们再转为10进制会得到三个不同的index

当长度为10时,10-1=9,9的二进制为 1001,将上面三个hashCode再分别与1001进行&运算

image.png

这时我们发现,得到的二进制数一样,这就意味着index的重复几率也太大了,不符合均匀分布的原则,所以长度设置为16或其它2的幂。

Get方法原理:

先将key通过index = Hash(key) 得到相应的index的值,然后根据index的值查找相对应的entry,由于可能有hash冲突,所以会在链表中匹配key。

Mac IDEA快捷键记录

Mac键盘符号和修饰键说明

⌘ Command

⇧ Shift

⌥ Option

⌃ Control

↩︎ Return/Enter

⌫ Delete

⌦ 向前删除键(Fn+Delete)

↑ 上箭头

↓ 下箭头

← 左箭头

→ 右箭头

⇞ Page Up(Fn+↑)

⇟ Page Down(Fn+↓)

Home Fn + ←

End Fn + →

⇥ 右制表符(Tab键)

⇤ 左制表符(Shift+Tab)

⎋ Escape (Esc)

一、Editing(编辑)

⌃Space 基本的代码补全(补全任何类、方法、变量)

⌃⇧Space 智能代码补全(过滤器方法列表和变量的预期类型)

⌘⇧↩ 自动结束代码,行末自动添加分号

⌘P 显示方法的参数信息

⌃J, Mid. button click 快速查看文档

⇧F1 查看外部文档(在某些代码上会触发打开浏览器显示相关文档)

⌘+鼠标放在代码上 显示代码简要信息

⌘F1 在错误或警告处显示具体描述信息

⌘N, ⌃↩, ⌃N 生成代码(getter、setter、构造函数、hashCode/equals,toString)

⌃O 覆盖方法(重写父类方法)

⌃I 实现方法(实现接口中的方法)

⌘⌥T 包围代码(使用if..else, try..catch, for, synchronized等包围选中的代码)

⌘/ 注释/取消注释与行注释

⌘⌥/ 注释/取消注释与块注释

⌥↑ 连续选中代码块

⌥↓ 减少当前选中的代码块

⌃⇧Q 显示上下文信息

⌥↩ 显示意向动作和快速修复代码

⌘⌥L 格式化代码

⌃⌥O 优化import

⌃⌥I 自动缩进线

⇥ / ⇧⇥ 缩进代码 / 反缩进代码

⌘X 剪切当前行或选定的块到剪贴板

⌘C 复制当前行或选定的块到剪贴板

⌘V 从剪贴板粘贴

⌘⇧V 从最近的缓冲区粘贴

⌘D 复制当前行或选定的块

⌘⌫ 删除当前行或选定的块的行

⌃⇧J 智能的将代码拼接成一行

⌘↩ 智能的拆分拼接的行

⇧↩ 开始新的一行

⌘⇧U 大小写切换

⌘⇧] / ⌘⇧[ 选择直到代码块结束/开始

⌥⌦ 删除到单词的末尾(⌦键为Fn+Delete)

⌥⌫ 删除到单词的开头

⌘+ / ⌘- 展开 / 折叠代码块

⌘⇧+ 展开所以代码块

⌘⇧- 折叠所有代码块

⌘W 关闭活动的编辑器选项卡

二、Search/Replace(查询/替换)

Double ⇧ 查询任何东西

⌘F 文件内查找

⌘G 查找模式下,向下查找

⌘⇧G 查找模式下,向上查找

⌘R 文件内替换

⌘⇧F 全局查找(根据路径)

⌘⇧R 全局替换(根据路径)

⌘⇧S 查询结构(Ultimate Edition 版专用,需要在Keymap中设置)

⌘⇧M 替换结构(Ultimate Edition 版专用,需要在Keymap中设置)

三、Usage Search(使用查询)

⌥F7 / ⌘F7 在文件中查找用法 / 在类中查找用法

⌘⇧F7 在文件中突出显示的用法

⌘⌥F7 显示用法

四、Compile and Run(编译和运行)

⌘F9 编译Project

⌘⇧F9 编译选择的文件、包或模块

⌃⌥R 弹出 Run 的可选择菜单

⌃⌥D 弹出 Debug 的可选择菜单

⌃R 运行

⌃D 调试

⌃⇧R, ⌃⇧D 从编辑器运行上下文环境配置

五、Debugging(调试)

F8 进入下一步,如果当前行断点是一个方法,则不进入当前方法体内

F7 进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中

⇧F7 智能步入,断点所在行上有多个方法调用,会弹出进入哪个方法

⇧F8 跳出

⌥F9 运行到光标处,如果光标前有其他断点会进入到该断点

⌥F8 计算表达式(可以更改变量值使其生效)

⌘⌥R 恢复程序运行,如果该断点下面代码还有断点则停在下一个断点上

⌘F8 切换断点(若光标当前行有断点则取消断点,没有则加上断点)

⌘⇧F8 查看断点信息

六、Navigation(导航)

⌘O 查找类文件

⌘⇧O 查找所有类型文件、打开文件、打开目录,打开目录需要在输入的内容前面或后面加一个反斜杠/

⌘⌥O 前往指定的变量 / 方法

⌃← / ⌃→ 左右切换打开的编辑tab页

F12 返回到前一个工具窗口

⎋ 从工具窗口进入代码文件窗口

⇧⎋ 隐藏当前或最后一个活动的窗口,且光标进入代码文件窗口

⌘⇧F4 关闭活动run/messages/find/… tab

⌘L 在当前文件跳转到某一行的指定处

⌘E 显示最近打开的文件记录列表

⌘⌥← / ⌘⌥→ 退回 / 前进到上一个操作的地方

⌘⇧⌫ 跳转到最后一个编辑的地方

⌥F1 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择(如在代码编辑窗口可以选择显示该文件的Finder)

⌘B / ⌘ 鼠标点击 进入光标所在的方法/变量的接口或是定义处

⌘⌥B 跳转到实现处,在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口

⌥ Space, ⌘Y 快速打开光标所在方法、类的定义

⌃⇧B 跳转到类型声明处

⌘U 前往当前光标所在方法的父类的方法 / 接口定义

⌃↓ / ⌃↑ 当前光标跳转到当前文件的前一个/后一个方法名位置

⌘] / ⌘[ 移动光标到当前所在代码的花括号开始/结束位置

⌘F12 弹出当前文件结构层,可以在弹出的层上直接输入进行筛选(可用于搜索类中的方法)

⌃H 显示当前类的层次结构

⌘⇧H 显示方法层次结构

⌃⌥H 显示调用层次结构

F2 / ⇧F2 跳转到下一个/上一个突出错误或警告的位置

F4 / ⌘↓ 编辑/查看代码源

⌥ Home 显示到当前文件的导航条

F3选中文件/文件夹/代码行,添加/取消书签

⌥F3 选中文件/文件夹/代码行,使用助记符添加/取消书签

⌃0…⌃9 定位到对应数值的书签位置

⌘F3 显示所有书签

七、Refactoring(重构)

F5 复制文件到指定目录

F6 移动文件到指定目录

⌘⌫ 在文件上为安全删除文件,弹出确认框

⇧F6 重命名文件

⌘F6 更改签名

⌘⌥N 一致性

⌘⌥M 将选中的代码提取为方法

⌘⌥V 提取变量

⌘⌥F 提取字段

⌘⌥C 提取常量

⌘⌥P 提取参数

八、VCS/Local History(版本控制/本地历史记录)

⌘K 提交代码到版本控制器

⌘T 从版本控制器更新代码

⌥⇧C 查看最近的变更记录

⌃C 快速弹出版本控制器操作面板

九、Live Templates(动态代码模板)

⌘⌥J 弹出模板选择窗口,将选定的代码使用动态模板包住

⌘J 插入自定义动态代码模板

十、General(通用)

⌘1…⌘9 打开相应编号的工具窗口

⌘S 保存所有

⌘⌥Y 同步、刷新

⌃⌘F 切换全屏模式

⌘⇧F12 切换最大化编辑器

⌥⇧F 添加到收藏夹

⌥⇧I 检查当前文件与当前的配置文件

§⌃, ⌃` 快速切换当前的scheme(切换主题、代码样式等)

⌘, 打开IDEA系统设置

⌘; 打开项目结构对话框

⇧⌘A 查找动作(可设置相关选项)

⌃⇥ 编辑窗口标签和工具窗口之间切换(如果在切换的过程加按上delete,则是关闭对应选中的窗口)

十一、Other(一些官方文档上没有体现的快捷键)

⌘⇧8 竖编辑模式

 

导航

⌘O 查找类文件 Ctrl + N

⌘⌥O 前往指定的变量 / 方法 Ctrl + Shift + Alt + N

⌃← / ⌃→ 左右切换打开的编辑tab页 Alt← / Alt→

⎋ 从工具窗口进入代码文件窗口 ESC

⌘L 在当前文件跳转到某一行的指定处 Ctrl + G

⌘E 显示最近打开的文件记录列表 Ctrl + E

⌘⌥← / ⌘⌥→ 退回 / 前进到上一个操作的地方 Ctrl + Alt + ← Ctrl + Alt + →

⌘⇧⌫ 跳转到最后一个编辑的地方

⌃H 显示当前类的层次结构 Ctrl + H

⌘⇧H 显示方法层次结构

⌃⌥H 显示调用层次结构

F4 / ⌘↓ 编辑/查看代码源

⌘⌥U 显示类UML图

⌃J 查看注释

编辑

⌥⌦ 删除到单词的末尾(⌦键为Fn+Delete)

⌥⌫ 删除到单词的开头

⌘+ / ⌘- 展开 / 折叠代码块

⌘F1 在错误或警告处显示具体描述信息

⌘⌥L 格式化代码

⌃⌥O 优化import

⇧↩ 开始新的一行

⌘⇧↩ 自动结束代码,行末自动添加分号

⌃I 实现方法(实现接口中的方法)

⇧F6 重命名文件或者变量

⌘N, ⌃↩, ⌃N 生成代码(getter、setter、构造函数、hashCode/equals,toString)

⌘P 显示方法的参数信息

查找

Double⇧ 查找任何东西

⌘⇧F 全局查找(根据路径)

⌘F 文件内查找

⌘G 查找模式下,向下查找

⌘⇧G 查找模式下,向上查找

导航

⌘⌥B 跳转到接口的实现

⌘U 查看接口定义

⌘⌥← / ⌘⌥→ 退回 / 前进到上一个操作的地方

⌘B / ⌘ 鼠标点击 进入光标所在的方法/变量的接口或是定义处

⌃⇧B 跳转到类型声明处

⌥ Space, ⌘Y 快速打开光标所在方法、类的定义

⌘O 查找类文件

⌘⇧O 查找所有类型文件、打开文件、打开目录,打开目录需要在输入的内容前面或后面加一个反斜杠/

F12 返回到前一个工具窗口

⎋ 从工具窗口进入代码文件窗口

⇧⎋ 隐藏当前或最后一个活动的窗口,且光标进入代码文件窗口

F3选中文件/文件夹/代码行,添加/取消书签

⌥F3 选中文件/文件夹/代码行,使用助记符添加/取消书签

⌃0…⌃9 定位到对应数值的书签位置

⌘F3 显示所有书签

⌥F1 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择(如在代码编辑窗口可以选择显示该文件的Finder)

⌘F12 弹出当前文件结构层,可以在弹出的层上直接输入进行筛选(可用于搜索类中的方法)

通用

⌃⌘F 切换全屏模式

Java学习之异常

今天复习一下Java的异常,先看下图:

image.png

我们从图中可以看出Java中异常的顶级类是Throwable,所有的异常都继承于这个类;

Error和Exception是异常类的两个大的分类;

Error是非程序异常,是程序不能捕捉的异常,一般是编译或者是系统性的错误;

Exception是程序异常类,由程序内部产生;Exception又分为运行时异常(非检查性异常)和非运行时异常(检查性异常)。

Java中一般分为3种类型的异常:

  • 非运行时性异常:最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。例:IOException…

  • 运行时异常: 运行时异常是可能被程序员避免的异常。与检查性异常相反,运行时异常可以在编译时被忽略。例:NullPointerException…

  • 错误: 错误不是异常,而是脱离程序员控制的问题。错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

日志框架-SLF4J+Logback

SLF4J全称:Simple Logging Facade for Java,即简单日志门面它是把不同的日志系统的实现进行了具体的抽象化,只提供了统一的日志使用接口,使用时只需要按照其提供的接口方法进行调用即可,由于它只是一个接口,并不是一个具体的可以直接单独使用的日志框架,所以最终日志的格式、记录级别、输出方式等都要通过接口绑定的具体的日志系统来实现,这些具体的日志系统就有log4j,logback,java.util.logging等,它们才实现了具体的日志系统的功能。


简单的说,SLF4J提供了日志的接口,日志的实现需要其它具体的框架来实现;

今天我就记录下SLF4J+Logback的组合来提供日志的框架。

新建一个Springboot的工程,Springboot中已经提供了SLF4J,所以我们不需要添加它的相关依赖;

工程目录结构:

image.png


在测试类中编写测试代码:

package com.wjy329.logdemo;/**
 * @Author wjy329
 * @Time 2018/11/68:53 PM
 * @description
 */

import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

    private final Logger logger = LoggerFactory.getLogger(logTest.class);

    @Test
    public void testLog(){
        logger.info("这是info日志");
        logger.error("这是错误日志");
        logger.debug("这是debug日志");
    }
}

控制台输出的结果是:

image.png

我们可以看到info和error级别的日志输出了,原因是默认的级别是info级别,所以debug级别的日志就没有打印。

日志的级别:ERROR>WARN>INFO>DEBUG>TRACE   级别越高优先级越高,当级别为info时,优先级比info低的debug和trace就不打印了,当级别为error时,只会打印error级别的日志。

上面仅仅是日志的简单使用,下面我们介绍一般在项目中是如何配置日志的。

在resource目录下新建一个日志的配置文件,我这里叫logback.xml  

image.png

具体的代码:

<?xml version="1.0" encoding="utf-8" ?>

<configuration>
    <!-- 输出到控制台的日志 -->
    <appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>
                %d - %msg%n
            </pattern>
        </encoder>

    </appender>

    <!-- 写出到日志文件 -->
    <appender name="testLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 滚动策略 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 输出路径 -->
            <fileNamePattern>/Users/wjy329/desktop/testLog/wjy329-%d{yyyy-MM-dd}.log</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>
                %d - %msg%n
            </pattern>
        </encoder>
    </appender>
    <!-- 日志级别 -->
    <root level="info">
        <appender-ref ref="consoleLog"/>
        <appender-ref ref="testLog"/>
    </root>

</configuration>

然后我们运行测试代码:

控制台输出如下:

image.png

输出到日志文件如下:

image.png

上面就是简单的演示日志系统的使用,后面如果遇到更详细的东西也会继续更新。