java3.日志+java8


日志

@RunWith(SpringRunner.class)

Springboot的@RunWith(SpringRunner.class)
注解的意义在于Test测试类要使用注入的类,比如@Autowired注入的类,
有了@RunWith(SpringRunner.class)这些类才能实例化到spring容器中,自动注入才能生效,
不然直接一个NullPointerExecption

FIB WARNNING

sout/syso禁用!!!

声明:本节课之后,除非临时调试代码(用完立刻删除,),否则,所有的代码中不能出现任何的sout,养成dbug调试的好习惯,就不需要临时调试sout代码

日志级别

日志级别:trace < debug < info < warn < error
输出规则:日志输出自动会以指定的日志级别及更高日志级别进行日志输出:比如,当前日志是info,日志输出的级别包括:info,warn,error

  1. trace:

    ​ 实际开发中,几乎不用

  2. debug:

    ​ 代码调试日志级别,也是线上定位问题的重要手段(线上没法断点调试),真实的生产环境值,默认是不开debug级别日志,这个日志记录很详细,只有出了问题再开debug

  3. info:

    ​ 实际开发过程中,使用info记录关键流程日志,springboot默认的日志级别就是info,日志输出小技巧:使用{}进行变量占位,方便进行日志处理

  4. warn:

    ​ 警告日志,他记录的不一定是错误(异常),一般是记录:异常流程(余额不足,用户状态异常)

  5. error:

    ​ 错误日志,是级别最高的日志,记录的是系统产生的所有的异常信息(必然输出)

手动调整日志配置

# 更改默认日志级别
logging.level.com.lcywings.springbootlog03lcywings=debug

# 指定日志输出到文件,不指定具体路径,默认是当前项目的跟路径下
# 约定:所有的日志都不会是覆盖记录,而是自动追加
logging.file=sbt.log
logging.file=E:\sbt.log

# 指定日志输出目录,跟logging.file配置不能同时使用,作用:将日志输出到指定目录下
# 默认的日志输出名称是spring.log
logging.path=E:\log

# 指定文件的日志格式,不需要单独配置,使用springboot约定的即可(没有重新使用logback)
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} *** [%thread] *** %-5level *** %logger{50} *** %msg%n

logback-spring日志配置

有三处需要修改,LOG_HOME、appName、 logger

<?xml version="1.0" encoding="UTF-8"?>
<!--
scan:配置文件发生改变,是否被重新加载,默认值为true。
scanPeriod:设置监测配置文件是否有修改的时间间隔,当scan为true时,此属性生效。默认的时间间隔为1分钟。
debug:是否打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
-->
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志的根目录,可以自动创建log目录,一般公司都是统一的日志目录,不能乱写 -->
    <property name="LOG_HOME" value="E:\log" />
    <!-- 日志文件名称 -->
    <property name="appName" value="lcyWings-springboot-log"/>
    <!-- 控制台输出 -->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!--
        日志输出格式:
         %d{日期时间格式},
         %thread:线程名,
         %-5level:级别从左显示5个字符宽度
         %logger{60} 表示logger名字最长60个字符,否则按照句点分割。 
         %msg:记录的日志消息,
         %n是换行符
        -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{60} - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 滚动文件记录,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
    <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 指定服务器记录日志文件的名称 -->
        <file>${LOG_HOME}/${appName}.log</file>
        <!--
        当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
        TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
        -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--
            滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 
            %i:当文件大小超过maxFileSize时,按照i进行文件滚动
            -->
            <fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
            <!-- 
            可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
            且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
            那些为了归档而创建的目录也会被删除。
            -->
            <MaxHistory>30</MaxHistory>
            <!-- 
            当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
            -->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>30MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <!-- 日志输出格式: -->     
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{60} : %line ] - %msg%n</pattern>
        </layout>
    </appender>

    <!-- 
      logger主要用于存放日志对象,也可以定义日志类型、级别
      name:表示匹配的logger类型前缀,也就是包的前半部分
      level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
      additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
        false:表示只用当前logger的appender-ref
          true:表示当前logger的appender-ref和rootLogger的appender-ref都有效
    -->
    <logger name="com.lcywings.springbootlog03lcywings" level="info" />
    <logger name="org.springframework" level="debug" additivity="false"/>

    <!-- 
    root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
    要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 
    -->
    <root level="debug">
        <appender-ref ref="stdout" />
        <appender-ref ref="appLogAppender" />
    </root>
</configuration> 

@Slf4j

@Slf4j是用作日志输出的,在类的开头加入该注解,简化代码,可以直接使用log对象输出日志。


lambda表达式

简单的Lambda表达式使用前后对比

// 使用前
// 普通写法
Runnable runnable1 = new Runnable() {
    @Override
    public void run() {
        log.info("------ aaaaaa ------");
    }
};

// 启动线程
new Thread(runnable1).start();
// 使用后
// lambda写法
Runnable runnable2 = () -> log.info("------ bbbbbb ------");

// 启动线程
new Thread(runnable2).start();

Lambda简化流程(四级递减)

带一个参数,没有返回值:

​ 1.不使用Lambda表达式

// java8有一个默认提供的函数式接口:消费性接口
log.info("------ 普通写法 ------");
Consumer<String> consumer1 = new Consumer<String>() {
    @Override
    public void accept(String s) {
        log.info("****** 消费数据:{} ******", s);
    }
};
consumer1.accept("consumer接口,没有使用lambda表达式");

​ 2.最复杂模式

// 写法1:标准写法
Consumer<String> consumer2 = (String s) -> {
    log.info("****** 消费数据:{} ******", s);
};
consumer2.accept("consumer接口,使用lambda表达式");

​ 3.省略参数

// 写法2:省略参数类型,java类型自动推断
List<String> list = new ArrayList<>();
Consumer<String> consumer3 = (s) -> {
    log.info("****** 消费数据:{} ******", s);
};
consumer3.accept("consumer接口,使用lambda表达式,省略参数类型");

​ 4.省略小括号

// 写法3:省略参数类型的基础上,如果只有一个参数,可以省略小括号
Consumer<String> consume4 = s -> {
    log.info("****** 消费数据:{} ******", s);
};
consume4.accept("consumer接口,使用lambda表达式,省略参数类型,一个参数可以省略小括号");

​ 5.省略大括号(单参无返自动类型,究极省略状态)

// 写法4:省略参数类型的基础上,如果只有一个参数,可以省略大括号(跟返回值类型没有关系)
Consumer<String> consume5 = s -> log.info("****** 消费数据:{} ******", s);
consume5.accept("consumer接口,使用lambda表达式,省略参数类型,可以省略大括号");

Lambda表达式写法:多个参数,有返回值

二级递减+不可省略

​ 1.不使用Lambda表达式

Comparator<Integer> comparator1 = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
};
log.info("****** 11和15的比较结果:{} ******", comparator1.compare(11, 15));

​ 2.省略参数类型

// 写法1:可以省略参数类型,但是多个参数不能省略小括号
Comparator<Integer> comparator2 = (o1, o2) -> o1.compareTo(o2);
log.info("****** 13和13的比较结果:{} ******", comparator2.compare(13, 13));

​ 3.使用方法引用 究极省略,至尊无敌

// 写法2:使用方法引用
Comparator<Integer> comparator3 = Integer::compareTo;
log.info("****** 19和17的比较结果:{} ******", comparator3.compare(19, 17));

​ 4.不可以省略大括号(多语句行为)

// 写法3:方法实现如果是多条语句,不可以省略大括号
Comparator<Integer> comparator4 = (o1, o2) -> {
    log.info("第一个数:{}", o1);
    log.info("第二个数:{}", o2);
    return o1.compareTo(o2);
};
log.info("****** 19和17的比较结果:{} ******", comparator4.compare(19, 17));

四大基本接口

消费性接口

/**
 * @author : Lcywings
 * @date : 2021/7/19 11:20
 * @acl : true
 * @description : 消费型接口 - 方法有一个参数,没有返回值
 */
static void testLambdaConsumer() {
    // 有钱 1000000,以前如果实现不同的业务逻辑,必须增加不同的方法实现
    consumData(1000000, money -> log.info("------ 有{}钱,买个小房子 ------", money));

    consumData(1000000, money -> {
        log.info("------ 有{}钱,买个小房子 ------", money);
        log.info("------ 剩下的钱,吃喝玩乐 ------", money);
    });

    consumData(1000000, new Consumer<Double>() {
        @Override
        public void accept(Double aDouble) {
            log.info("cnm*{}", 1000000);
        }
    });


}

// 将多个实现的方法整合为通用方法
static void consumData(double mondy, Consumer<Double> consumer) {
    consumer.accept(mondy);
}

供给型接口

/**
 * @author : Lcywings
 * @date : 2021/7/19 11:20
 * @acl : true
 * @description : 供给型接口 - 没有参数,可以返回任意类型的结果
 */
static void testLambdaSupplier() {
    Supplier<String> supplier = () -> "数据";
    log.info("------ 供给型接口返回:{} ------", supplier.get());
}

断言型接口

/**
 * @author : Lcywings
 * @date : 2021/7/19 11:20
 * @acl : true
 * @description : 断言型接口 - 有一个参数,返回一个boolean类型的结果
 */
static void testLambdaPredicate() {
    // 有字符串集合,根据不同的业务实现不同的数据返回
    List<String> nameList = Arrays.asList("Mark", "Marry", "Jack", "LiLei", "HanMeiMei", "LiSuJie");

    // 返回一个集合,将所有姓名中,包含L的姓名返回
    List<String> nameContainL = collectNameListByCondition(nameList, name -> name.contains("L"));
    log.info("------ 所有姓名中,包含L的姓名:{} ------", nameContainL);

    // 返回一个集合,将所有姓名中,包含r或者n的姓名返回
    List<String> nameContainrn = collectNameListByCondition(nameList, name -> name.contains("r") || name.contains("n"));
    log.info("------ 所有姓名中,包含r或者n的姓名:{} ------", nameContainrn);
}

static List<String> collectNameListByCondition(List<String> names, Predicate<String> predicate) {
    //定义返回集合
    List<String> newNameList = new ArrayList<>();

    //借助断言型接口,指定条件规则(注意;规则是在方法调用出指定的)
    names.forEach(name -> {
        // 判断姓名是否符合规则,使用断言型接口的test方法
        if (predicate.test(name)) {
            newNameList.add(name);
        }
    });
    return newNameList;
}

函数型接口

函数型接口
/**
 * @author : Lcywings
 * @date : 2021/7/19 12:08
 * @acl : true
 * @description : 函数型接口 -
 */
static void testLambdaFunction() {
    Function<Double, Long> function = Math::round;
    Long result = function.apply(123.55);
    log.info("------ 函数型接口处理数据,返回结果:{} ------", result);
}
自定义函数型接口
/**
 * @author : Lcywings
 * @date : 2021/7/19 12:08
 * @acl : true
 * @description : 自定义函数型接口 -
 */
static void testMyLambdaFunction(MyFunctionInterface<String> myFunctionInterface, String msg) {
    myFunctionInterface.hello(msg);
    myFunctionInterface.hi();

}
MyFunctionInterface.class

​ 接口默认方法,可以有实现,Java1.8新更新内容

/**
 * Created on 2021/7/19.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 自定义函数式接口
 * <p>
 * 定义:如果一个接口中,只用一个抽象方法,那么这个接口就是函数式接口
 * 注意:@FunctionalInterface注解标注的接口必须是函数式接口否则接口会报错,但是:1.8支持在接口中声明默认实现方法
 */
@FunctionalInterface
public interface MyFunctionInterface<T> {
    void hello(T t);

    default void hi() {
        System.out.println("这个是默认方法可以直接从接口调用");
    }
}

调用方法

// 普通lowB调用
// testMyLambdaFunction(str -> System.out.println(str), "自定义函数式接口");
// 高↑级↓调用
testMyLambdaFunction(System.out::println, "自定义函数式接口");

Stream

生成流

生成流1-无中生有

1 of

// 1-5每个数字的平方
Stream.of(1, 2, 3, 4, 5).forEach(n -> System.out.print(n * n + ","));

2 iterate

// 获取从1开始前十个偶数,创建无限流
Stream.iterate(0, m -> m + 2).limit(10).forEach(y -> System.out.print(y + ","));

3 generate

// 获取10个随机数
Stream.generate(Math::random).limit(5).forEach(y -> System.out.print(y + ","));

生成流2-数组/集合创建流

1 操作数组

// 已知数列,获取最大值
int[] nums = {13, 23, 11, 45, 67, 34, 78, 56, 52};

// 数组没法直接进行流操作,先转换为流再操作
int maxNum = Arrays.stream(nums).max().getAsInt();
log.info("------ 数组最大值:{} ------", maxNum);

2 操作用户对象集合

// 操作用户对象集合,获取账户余额大于6000的用户信息
Stream<User> userStream = UserUtil.users.stream();
UserUtil.users.stream().forEach(user -> {
    if (user.getLeftMoney() > 6000)
        log.info("****** 余额大于6000的用户信息:{} ******", user);
});

流的中间操作

1-简单基本操作

1 筛选

// 筛选:filter,账户余额大于5000,且性别是男(1)的用户信息
UserUtil.users.stream().filter(user -> user.getLeftMoney() >= 5000).filter(user -> user.getGender() == 1).forEach(System.out::println);

2 截断

// 截断:limit,指定截断(最大)数量,返回对应数量的元素
UserUtil.users.stream().limit(5).forEach(System.out::println);

3 过滤

// 过滤:ship,过滤掉前面指定数量的元素,从后面元素开始
UserUtil.users.stream().skip(2).forEach(System.out::println);

4 去重

// 去重:distinct
Stream.of(11, 22, 33, 44, 22, 33, 11, 55).distinct().forEach(System.out::println);

简单流式操作练习(实现分页)

// 使用截断和过滤实现分页(每页展示三条数据,获取第二页数据)
UserUtil.users.stream().skip(3).limit(3).forEach(System.out::println);

复杂流式操作练习

2-映射操作

1 映射:map,将集合元素转换格式或者提取元素信息,会自动将映射规则作用到集合中的每个元素中,形成新的元素集合

List<String> strs = Arrays.asList("mark", "tom", "jack", "chouli");
strs.stream().map(String::toUpperCase).forEach(System.out::println);

2 将用户集合中,提取用户名并判断用户名长度大于5的用户的用户名信息

UserUtil.users.stream().map(User::getUserName).filter(uname -> uname.length() > 5).forEach(System.out::println);

3 mapToInt.规则作用到每个元素上,产生一个IntStream流,获取所有用户的用户名长度信息

UserUtil.users.stream().mapToInt(user -> user.getUserName().length()).forEach(System.out::println);

4 练习:获取strs中所有字符串的所有字符

strs.stream().map(str -> {
    List<Character> characters = new ArrayList<>();
    for (Character c : str.toCharArray()) {
        characters.add(c);
    }
    return characters.stream();
}).forEach(System.out::println);
// 简化
strs.stream().map(a -> Arrays.stream(a.split(""))).forEach(System.out::println);

基本操作3-排序操作

1 排序: sorted,默认是使用元素的自然排序规则

List<String> strs = Arrays.asList("Mark", "Tom", "Jack", "Chouli");
strs.stream().sorted().forEach(System.out::println);

2 定制化排序,指定排序规则:先按照性别排序,再按照余额排序

UserUtil.users.stream().sorted((u1, u2) -> {
    int result = Integer.compare(u1.getGender(), u2.getGender());
    if (result == 0) {
        // 如果性别相同,按照余额排序
        return Double.compare(u1.getLeftMoney(), u2.getLeftMoney());
    }
    return result;
}).forEach(System.out::println);

基本操作4-匹配和查找操作

1 anyMatch():至少匹配一个,返回true,否则返回false(查找用户集合中,有没有人余额小于4000)

if (UserUtil.users.stream().anyMatch(user -> user.getLeftMoney() < 4000)) {
    log.info("------ 用户中有人的余额小于4000 ------");
} else {
    log.info("------ 所有用户的余额大于于4000 ------");
}

2 allMatch():全匹配,所有的都满足条件,返回true,否则返回false(查找用户集合中,是不是所有人姓名长度都大于4)

if (UserUtil.users.stream().allMatch(user -> user.getUserName().length() > 4)) {
    log.info("------ 用户中所有人的姓名长度大于4 ------");
} else {
    log.info("------ 用户中有人的姓名长度小于4 ------");
}

3 noneMatch():没有一个满足条件,返回true,否则返回false(查找用户集合中,是否有人的余额大于一万)

if (UserUtil.users.stream().noneMatch(user -> user.getLeftMoney() > 10000)) {
    log.info("------ 用户中所有人的余额小于一万 ------");
} else {
    log.info("------ 用户中有人的余额大于一万 ------");
}

4 返回集合中第一个元素

Optional<User> optionalUser1 = UserUtil.users.stream().findFirst();
log.info("------ 第一个用户信息:{} ------", optionalUser1);

5 返回集合中任意一个元素(数据量小,串行情况下,一般直接返回第一个,但是并行就是返回任意一个)

Optional<User> optionalUser2 = UserUtil.users.stream().findAny();
log.info("------ 任意一个用户信息:{} ------", optionalUser2);

6 练习1:获取用户中,最高余额

Optional<Double> maxLeftMoney = UserUtil.users.stream().map(User::getLeftMoney).max(Double::compare);
log.info("------ 获取用户中,最高余额:{} ------", maxLeftMoney);

7 练习2:获取用户中,最低余额的那个用户信息

Optional<User> minUser = UserUtil.users.stream().min(Comparator.comparingDouble(User::getLeftMoney));
log.info("------ 获取用户中,最低余额的那个用户信息:{} ------", minUser);

8 练习3:给所有的用户的用户余额增加500

UserUtil.users.stream().peek(user -> user.setLeftMoney(user.getLeftMoney() + 500)).forEach(System.out::println);

基本操作5-规约

1 reduce:规约,用来统计求和

List<Integer> nums = Arrays.asList(2, 4, 6, 8, 10);
Optional<Integer> sum1 = nums.stream().reduce((x, y) -> x + y);
log.info("------ 集合中数字总和:{} ------", sum1);
Optional<Integer> sum2 = nums.stream().reduce(Integer::sum);
log.info("------ 集合中数字总和:{} ------", sum2);

2 执行原理

Optional<Integer> sum3 = nums.stream().reduce((m, n) -> {
    System.out.println("m = " + m);
    m += n;
    System.out.println("n = " + n);
    System.out.println("m_new = " + m);
    return m;
});
log.info("------ 集合中数字总和:{} ------", sum3);

3 练习1:计算数值集合中所有数字的平方和

Optional<Integer> numSqSum = nums.stream().map(x -> x * x).reduce(Integer::sum);
log.info("------ 所有数字的平方和:{} ------", numSqSum);

4 练习2:计算所有用户的账户余额总数

Optional<Double> userSum = UserUtil.users.stream().map(User::getLeftMoney).reduce(Double::sum);
log.info("------ 所有用户的账户余额总数:{} ------", userSum);

基本操作6-收集

1 collect:收集可以将流中的结果收集为新的集合,将用户中性别为女(0)的用户存入新的集合中

List<User> users = UserUtil.users.stream().filter(user -> user.getGender() == 0).collect(Collectors.toList());
        users.forEach(System.out::println);

2 将用户中编号,姓名新的集合中

Map<Integer, String> userMap = UserUtil.users.stream().collect(Collectors.toMap(User::getUserId, User::getUserName));

3 数值集合,求最大,最小,平均,总和

List<Integer> nums = Arrays.asList(24, 56, 38, 76, 18, 47);
int sum = nums.stream().collect(Collectors.summingInt(Integer::intValue));
Double avg = nums.stream().collect(Collectors.averagingInt(Integer::intValue));
Integer max = nums.stream().collect(Collectors.maxBy(Integer::compare)).get();
Integer min = nums.stream().collect(Collectors.minBy(Integer::compare)).get();
log.info("------ sum={},avg={},max={},min={} ------", sum, avg, max, min);

4 一次性操作

IntSummaryStatistics summaryStatistics = nums.stream().collect(Collectors.summarizingInt(Integer::intValue));
log.info("------ summaryStatistics={} ------", summaryStatistics);

5 根据性别分组

Map<Integer, List<User>> group = UserUtil.users.stream().collect(Collectors.groupingBy(User::getGender));
log.info("------ 根据性别分组={} ------", group);

所有方法总结

  • .of()
  • .forEach()
  • .iterate()
  • .generate()

  • .max()
  • .getAsInt()
  • .filter()
  • .limit()
  • .skip()
  • .distinct()
  • .map()
  • .mapToInt()
  • .sorted()
  • .anyMatch()
  • .allMatch()
  • .noneMatch()
  • .findFirst()
  • .findAny()
  • .min()
  • .peek()
  • .reduce()
  • .collect()

扩展用法

Q.E.D.