悠蓦 | Java工程师

道可道,非常道;
名可名,非常名。

悠蓦 | Java工程师
0%

Spring Security使用浅析

常见类说明

认证过程中常见的类

说明
Authentication 登录用户的角色信息及权限信息等,还包含IPSESSION等信息,不同Authentication实现不尽相同。
AuthenticationProvider 认证的提供者,真正起到真正作用的类。
AuthenticationManager 认证处理器,内置一组AuthenticationProvider用于认证。当有一个Provider判定认证成功时,即判定认证成功。
AuthenticationEventPublisher 用于发布认证成功/失败事件
SecurityFilterChain Spring security自带的过滤器

鉴权过程中常见的类

说明
ConfigAttribute 访问规则,包含需要访问权限的信息
SecurityMetadataSource 存储并返回用户权限信息
FilterSecurityInterceptor 鉴权过滤器,一般位于springsercurity过滤器链中最后一个
AccessDecisionManager 决策管理器,用于保存一组投票器
默认为AffirmativeBased类型 一票赞成
ConsensusBased 少数服从多数
UnanimousBased 一票否决

总体说明

什么是认证

认证就是认为前端传过来的username就是本人。

通过认证后:

  • 如果当前已经有角色信息Authentication),可直接将其角色信息用于授权判断,或在应用业务代码中进行使用。
  • 如果当前没有角色信息Authentication),仅根据username就可以直接去查询角色信息,并可直接将其角色信息用于授权判断,或在应用业务代码中进行使用。

什么是授权

授权是已认证的用户中的权限(GrantedAuthority)能否访问某个资源。

两种授权方式:

  • web授权,通过指定url指定权限
  • 方法授权,基于aop,通过在spring代理类方法上加入如@PreFilter的注解来增加权限

重点流程说明

spring security鉴权初始化流程图

spring secquity 用户认证授权信息存储流程图

spring security授权拦截器

认证初始化阶段三大核心类

  • AuthenticationManagerBuilder用于:

    • 会生产AuthenticationManagerUsernamePasswordAuthenticationFilter就是将用于授权逻辑委派给AuthenticationManager进行处理
    • 创建全局AuthenticationManager,用于子处理器无法处理认证(Authentication)请求时进行处理。
    • 保存认证所需全部的AuthenticationProviderAuthenticationProvider用于AuthenticationManager(认证处理器)进行认证。
    • 初始化defaultUserDetailsService,即默认如何根据用户名查询角色权限信息。
    • 初始化AuthenticationEventPublisher
  • WebSecurity用于:

    • 生产FilterChainProxy,springsecurity封装的一组**SecurityFilterChain**的代理,用来控制FilterChain的执行逻辑
    • ignoredRequests,不进行访问控制(认证、授权)的请求
    • securityFilterChainBuilders
    • FilterSecurityInterceptor授权
    • HttpFirewall拒绝策略,在进入过滤器链前先判断
    • RequestRejectedHandler,请求拒绝处理器,过滤器报异常时调用,无默认
    • DefaultWebSecurityExpressionHandler
  • HttpSecurity用于:

    • 生产SecurityFilterChain
    • requestMatcher请求匹配,用于判断该请求是否走了过滤器
    • OrderedFilter,经过排序的WebSecurity中securityFilterChainBuilders建造的过滤器
    • AuthenticationManager,内置一组AuthenticationProvider用于认证。

FilterChainProxy与SecurityFilterChain与FilterSecurityInterceptor

**FilterChainProxy中组合了DefaultSecurityFilterChain**。

FilterChainProxy相当于一组过滤器的代理。

我们通过@Bean 注入一个SecurityFilterChain对象相当于将自定义的SecurityFilterChain注入到FilterChainProxy持有的一组过滤器的代理中。

FilterSecurityInterceptor也是一个Filter

SecurityFilterChain创建

  1. FilterOrderRegistration构造方法将springSecurity自定义过滤器及顺序注册进来
  2. 通过HttpSecurityConfiguration创建HttpSecurity名为httpSecurityspring bean
  3. 创建HttpSecurity的过程中调用performBuild()http.方法将springSecurity自定义过滤器放入HttpSecurityfilters
  4. HttpSecurity 调用 performBuild 创建DefaultSecurityFilterChain,并将HttpSecurityfilters中所有的springSecurity自定义过滤器放入DefaultSecurityFilterChainfilters

FilterChainProxy。

  1. 初始化

    1. @EnableWebSecurity注解导入WebSecurityConfiguration.classHttpSecurityConfiguration.class
    2. WebSecurityConfiguration中,setter方法setFilterChainProxySecurityConfigurer过程中创建WebSecurity的实例
    3. WebSecurityConfiguration中,创建了名为springSecurityFilterChainFilterChainProxybean,该bean是由WebSecurity调用build方法创建的
    4. build的过程中会创建FilterChainProxy对象,并将全量的securityFilterChains组合到对象中
    5. DelegatingFilterProxy会代理名为springSecurityFilterChainFilterChainProxybean
  2. 认证

    1. 调用DelegatingFilterProxydoFilter会调用FilterChainProxy中的doFilter
    2. 调用FilterChainProxy中的doFilterInternal创建VirtualFilterChain,将FilterChain (系统自带过滤器)和SecurityFilterChainspring Security 自带过滤器链和自定义过滤器链)组合,依次调用
    3. SecurityFilterChain中有一个关键的FilterUsernamePasswordAuthenticationFilter,控制登录行为

FilterSecurityInterceptor授权

  • FilterSecurityInterceptor
    • 内置成员变量AccessDecisionManager,有以下实现
      • AffirmativeBased
      • ConsensusBased
      • UnanimousBased
    • FilterSecurityInterceptor调用AccessDecisionManager.decide(authenticated, object, attributes)
    • 获取全部参与投票的AccessDecisionVoter,进行判断
  • MethodSecurityInterceptor

核心流程代码

实现自定义功能的UsernamePasswordAuthenticationFilter

  1. 写一个子类MyFilter继承UsernamePasswordAuthenticationFilter,重写attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,增加想要的功能,调用父类attemptAuthentication方法

    public class MyFilter extends UsernamePasswordAuthenticationFilter {
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
           if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }
            if (!MediaType.APPLICATION_JSON_VALUE.equalsIgnoreCase(request.getContentType())) {
                return super.attemptAuthentication(request, response);
            }
            Map map = null;
            try {
                map = new ObjectMapper().readValue(request.getInputStream(), Map.class);
            } catch (IOException e) {
                throw new BusinessException(ResponseCode.FAIL);
            }
            String username = (String) map.get(getUsernameParameter());
            String password = (String) map.get(getPasswordParameter());
            UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
  2. 注册将MyFilter

    @Bean
    public LoginFilter loginFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/login");
        loginFilter.setUsernameParameter("username");
        loginFilter.setPasswordParameter("password");
        loginFilter.setAuthenticationManager(authenticationManager);
        loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(authentication.getPrincipal())));
        });
        loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
            response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.LOGIN)));
        });
        return loginFilter;
    }
  3. 替换SecurityFilterChainUsernamePasswordAuthenticationFilter

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, MyFilter myFilter) throws Exception {
        http.addFilterAt(myFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

用户登录信息存储方式

一旦认证成功后将Authentication ``authResult保存在了SecurityContextHolder中。

Authenticationauthenticated属性设置为true ,安全拦截器都会对他放行

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
		Authentication authResult) throws IOException, ServletException {
	SecurityContext context = SecurityContextHolder.createEmptyContext();
	context.setAuthentication(authResult);
	SecurityContextHolder.setContext(context);
}

认证的核心代码

UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
      password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

this.getAuthenticationManager().authenticate(authRequest)中调用UserDetailsServiceloadUserByUsername(String username)查询用户的密码及权限信息,并封装成UserDetails对象,在调用additionalAuthenticationChecks(UserDetails userDetails)方法中, 调用DaoAuthenticationProviderUsernamePasswordAuthenticationToken authentication)进行密码校验,并将AbstractAuthenticationTokenauthenticated(已登录状态)设置为true

密码自动升级配置方式及说明

  • 通过注入UserDetailsPasswordService实现类

  • 通过前缀名识别密码,通过前缀名修改加密方式,再进行加密保存

    @Bean
    public UserDetailsPasswordService userDetailsPasswordService() {
        return new UserDetailsPasswordService() {
            @Override
            public UserDetails updatePassword(UserDetails user, String newPassword) {
                userService.updateUserPassword(user.getUsername(), newPassword);
                return user;
            }
        };
    }
  • 由于检测到加密方式改变会自动改变密码,当springsecurity升级时,修改了默认加密方式也不用修改代码

public static PasswordEncoder createDelegatingPasswordEncoder() {
   String encodingId = "bcrypt";
   Map<String, PasswordEncoder> encoders = new HashMap<>();
   encoders.put(encodingId, new BCryptPasswordEncoder());
   encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
   encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
   encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
   encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
   encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
   encoders.put("scrypt", new SCryptPasswordEncoder());
   encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
   encoders.put("SHA-256",
         new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
   encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
   encoders.put("argon2", new Argon2PasswordEncoder());
   return new DelegatingPasswordEncoder(encodingId, encoders);
}
  • PasswordEncoderFactories中,会将BCryptPasswordEncoder作为默认的Encoder

    因此没有必要手动注入PasswordEncoder

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

RememberMe配置方式及说明

服务端session30分钟就过期了,此时客户端携带cookie请求服务端受保护资源需重新登录

rememberMe开启会将认证信息放到cookie中,如果session过期,服务端会解密cookie,而不用重新登录。

  1. LoginFilter中增加

    /**
     * @author sakura
     */
    public class LoginFilter extends UsernamePasswordAuthenticationFilter {
        @Autowired
        private RememberMeServices rememberMeServices;
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws AuthenticationException {
            //其他代码略
            if (rememberMeServices instanceof PersistentTokenBasedRememberMeServices) {
                String rememberMeParameterName = ((PersistentTokenBasedRememberMeServices) rememberMeServices).getParameter();
                String rememberMe = (String) map.get(rememberMeParameterName);
                    request.setAttribute(rememberMeParameterName, rememberMe);
            }
            //其他代码略
        }
    }
  2. SecurityConfig中增加

    /**
         * 持久化series
         * @param dataSource
         * @return
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
            return new JdbcTokenRepositoryImpl() {
                {
                    setDataSource(dataSource);
    //                第一次创建表才需要
    //                setCreateTableOnStartup(true);
                }
            };
        }
    
      /**
         * 记住我,重写了判断【前端传递记住我参数】是否开启。
         * @param userDetailsService
         * @param persistentTokenRepository
         * @return
         */
        @Bean
        public RememberMeServices rememberMeServices(UserDetailsService userDetailsService, PersistentTokenRepository persistentTokenRepository) {
            return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), userDetailsService, persistentTokenRepository) {
                protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
                    Object paramValue = request.getAttribute(parameter);
                    if (paramValue != null && paramValue instanceof String) {
                        String attribute = (String) paramValue;
                        if (attribute.equalsIgnoreCase("true") || attribute.equalsIgnoreCase("on")
                                || attribute.equalsIgnoreCase("yes") || attribute.equals("1")) {
                            return true;
                        }
                    }
                    return false;
                }
            };
        }
        @Bean
        public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
            return authenticationConfiguration.getAuthenticationManager();
        }
    
        @Bean
        public LoginFilter loginFilter(AuthenticationManager authenticationManager, RememberMeServices rememberMeServices) {
            LoginFilter loginFilter = new LoginFilter();
            loginFilter.setFilterProcessesUrl("/login");
            loginFilter.setUsernameParameter("username");
            loginFilter.setPasswordParameter("password");
            //第一次登录,勾选记住我则触发PersistentTokenRepository持久化
            loginFilter.setAuthenticationManager(authenticationManager);
            loginFilter.setRememberMeServices(rememberMeServices);
            loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpServletResponse.SC_OK);
                response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(authentication.getPrincipal())));
            });
            loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
                response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.LOGIN)));
            });
            return loginFilter;
        }
    
     @Bean
        public SecurityFilterChain filterChain(HttpSecurity http, LoginFilter loginFilter, RememberMeServices rememberMeServices) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and().formLogin()
                    .and().exceptionHandling().authenticationEntryPoint((request, response, exception) -> {
                        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                        response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>(ResponseCode.UN_LOGIN)));
                    })
                    .and().logout().logoutUrl("/logout").logoutSuccessHandler((request, response, authentication) -> {
                        response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
                        response.setStatus(HttpServletResponse.SC_OK);
                        response.getWriter().println(new ObjectMapper().writeValueAsString(new RestResult<>()));
                    })
                    .and().rememberMe().rememberMeServices(rememberMeServices)
                    .and().csrf().disable();
            http.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class);
            return http.build();
        }
        @Bean
        public PersistentTokenRepository persistentTokenRepository(DataSource dataSource) {
            return new JdbcTokenRepositoryImpl() {
                {
                    setDataSource(dataSource);
    //                第一次创建表才需要
    //                setCreateTableOnStartup(true);
                }
            };
        }
  3. Remember-me token中存在用:分隔的两段,第一段为series,第二段为tokenspring secrity会根据seriespersistent_logins表中查询token,如果表中tokenRemember-me token中第二段token一致校验通过。

  4. Remember-me token的过期时间为两周,根据去persistent_logins表中所存储的日期判断token是否过期

  5. 根据去persistent_logins表中所存储的用户名,通过loadUserByUsername(String username)查询出用户信息及权限

Csrf配置方式及说明

当携带header中携带XSRF-TOKEN参数时,springsecurity不会返回一个新的csrf token

CsrfToken csrfToken = this.tokenRepository.loadToken(request);
		boolean missingToken = (csrfToken == null);
		if (missingToken) {
			csrfToken = this.tokenRepository.generateToken(request);
			this.tokenRepository.saveToken(csrfToken, request, response);
		}

Oauth2 授权码模式说明

  • 应用客户端
  • 资源服务器
  • 授权服务器
  1. 用户在应用客户端(浏览器、移动设备或者服务器)上,请求资源服务器受保护资源,由于未携带资源token令牌无法登陆,故点击使用第三方登录
  2. 应用客户端通过用户代理(浏览器)向授权服务器发送获取授权码的请求,携带客户端client id,授权方式,重定向url。
  3. 授权服务器通过用户代理(浏览器)展示出是否授权应用客户端某些权限。如:读、写、可读可写。
  4. 用户代理(浏览器)点击提交后,向授权服务器提交权限
    • 如果拒绝,则拒绝授权
    • 如果允许,通过(1)中重定向url,url中携带授权码(此授权码是与登录用户关联的)返回给客户端
  5. 客户端拿到授权码后,携带客户端client id、客户端密钥、重定向url(与请求授权码时设置的重定向URL一样)、授权码授权服务器发送请求
  6. 授权服务器返回客户端查询资源token令牌
  7. 客户端携带资源token令牌访问资源服务器
  8. 授权服务器使用资源token令牌在资源服务器中进行操作

没搞懂ObjectPostProcessor是干嘛的

http.apply(new UrlAuthorizationConfigurer(http.getSharedObject(ApplicationContext.class)).withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    @Override
    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
        object.setSecurityMetadataSource(mySecurityMetadataSource);
        AccessDecisionManager accessDecisionManager = object.getAccessDecisionManager();
        if (accessDecisionManager instanceof AbstractAccessDecisionManager) {
            ((AbstractAccessDecisionManager) accessDecisionManager).getDecisionVoters().add(new MyDecisionVoter());
        }
        object.setRejectPublicInvocations(true);
        return object;
    }
}));

java8函数式

函数式编程特点

  • 有一个@FunctionalInterface接口(或该接口中只有一个抽象方法)
  • 有一个返回值与@FunctionalInterface接口返回值相同类型的方法(该方法所在类可以不实现接口,也可以与接口方法名不同,因为只有一个抽象方法,所以明确知道调用的方法是什么)
  • 接口实现函数为静态方法,必须需要有相同参数列表,参考multiply
  • 接口实现函数为非静态方法
    • 以非静态方式掉用时,即new LambdaTest()::compute,必须需要有相同参数列表,参考minus
    • 以静态方式掉用时,即LambdaTest::compute,必须比接口少一个参数。相当于将非静态方法转为静态方法,并作为第一个参数加入进来,参考divided
public class LambdaTest {
    protected int num;

    @FunctionalInterface
    public interface Plus {
        int plus(int num1, int num2);
    }

    public static int computePlus(int num1, int num2, Plus plus) {
        return plus.plus(num1, num2);
    }

    @FunctionalInterface
    public interface Minus {
        int minus(int num1);
    }

    public static int computeMinus(int num1, Minus minus) {
        return minus.minus(num1);
    }

    public int compute(int num1) {
        return num - num1;
    }

    @FunctionalInterface
    public interface Multiply {
        int multiply(LambdaTest a, LambdaTest b);
    }

    public static int computeMultiply(LambdaTest a, LambdaTest b, Multiply multiply) {
        return multiply.multiply(a, b);
    }

    public static int compute(LambdaTest a, LambdaTest b) {
        return a.num * b.num;
    }

    @FunctionalInterface
    public interface Divided {
        int divided(LambdaTest a, OtherClass b);
    }

    public static int computeDivided(LambdaTest a, OtherClass b, Divided divided) {
        return divided.divided(a, b);
    }

    public int compute(OtherClass arg) {
        return arg.num / num;
    }

    public static void main(String[] args) {
        //函数调用
        int plus = computePlus(1, 2, ((num1, num2) -> num1 + num2 + 1 - 1));
        System.out.println("plus: " + plus);

        //静态方法调用
        int multiply = computeMultiply(new LambdaTest() {{
            num = 5;
        }}, new LambdaTest() {{
            num = 6;
        }}, LambdaTest::compute);
        System.out.println("multiply: " + multiply);

        //非静态方法调用,以非静态方式调用,即new LambdaTest()::compute
        int minus = computeMinus(3, new LambdaTest() {{
            num = 2;
        }}::compute);
        System.out.println("minus: " + minus);

        //非静态方法调用,以静态方式调用,即LambdaTest::compute
        int divided = computeDivided(new LambdaTest() {{
            num = 5;
        }}, new OtherClass() {{
            num = 6;
        }}, LambdaTest::compute);
        System.out.println("divided: " + divided);
    }

}
public class OtherClass {
    protected int num=2;
}

java8时间类

优势

  1. 线程安全
    • 新的日期/时间API中,所有的类都是不可变的。
  2. 年月日使用贴近日常习惯
    • Data类型查看年份要加1900,月是从0开始,日期是从1开始
    • Calendar类型年份不必转换,月份仍然要加1,查看星期几时返回的1代表周日
    • 转换日期时对夏令时有优化,只需要修改ZonedDateTime的时区即可

时间单位说明

中国标准时间(China Standard Time) CST UTC+8 GMT+8

  • UT Universal Time 世界时。根据原子钟计算出来的时间。
  • GMT Greenwich Mean Time 格林尼治标准时间。这是以英国格林尼治天文台观测结果得出的时间,这个地方的当地时间过去被当成世界标准的时间。
  • UTC Coordinated Universal Time 协调世界时。每隔几年都会给世界时+1秒,让(UT)和(GMT)相差不至于太大。并将得到的时间称为UTC
  • 协调世界时不与任何地区位置相关,也不代表此刻某地的时间,所以在说明某地时间时要加上时区

常用类介绍

java8常用时间类

Class类 说明
ZoneId 时区
Instant 时刻(直接打印会显示UTC+0GMT+0时区)
LocalDate 年月日,不含时区信息
LocalTime 时分秒,不含时区信息
LocalDateTime 年月日时分秒,不含时区信息
ZonedDateTime 年月日时分秒及时区
Duration 时差,一般用于换算时分秒(注意,换算成秒会将所有时间全部转化成该类型)
Period 时差,一般用于换算年月日(注意,换算成秒不会将所有时间全部转化成该类型)

使用DEMO

public static void main(String[] args) {
    //ZoneId 是时区相关类
    //北京时区
    ZoneId beijing = ZoneId.of("UTC+8");
    //美国夏威夷州时区
    ZoneId hawaii = ZoneId.of("UTC-10");

    System.out.println("//--------------------------------------------------------------------------------");
    //不含有时区的时间相关类
    System.out.println("不含有时区的时间相关类");
    //Instant  时刻   用来代替时间戳     有年月日时分秒信息     也可以理解成有时区(打印时有时区标识),但只能是UTC+0 GMT+0时区
    Instant instant = Instant.now();
    System.out.println("Instant.now()" + instant);
    System.out.println("Instant.of() " + Instant.ofEpochSecond(1656599684));
    System.out.println("Instant.of() " + Instant.ofEpochMilli(1656599721234L));

    //LocalDateTime 有年月日时分秒信息     不含有时区   体现在打印没有表示时区的后缀
    //LocalDate,LocalTime 与LocalDateTime同理
    LocalDateTime localDateTime = LocalDateTime.now();
    LocalDate localDate = LocalDate.now();
    LocalTime localTime = LocalTime.now();
    System.out.println("LocalDateTime.now()" + localDateTime);
    System.out.println("LocalDateTime.of()" + LocalDateTime.ofInstant(instant, beijing));
    System.out.println("LocalDateTime.of()有时区问题" + LocalDateTime.ofEpochSecond(1656599684, 0, ZoneOffset.UTC));
    System.out.println("LocalDateTime.of()" + LocalDateTime.of(2022, 6, 29, 0, 0, 0));
    System.out.println("LocalDate,LocalTime 与LocalDateTime同理" + localDate + localTime);

    System.out.println("//--------------------------------------------------------------------------------");

    System.out.println("含有时区的时间相关类");

    //ZonedDateTime 含有时区的时间相关类,由LocalDateTime和ZoneId组成
    //ZonedDateTime 通过Instant或LocalDateTime增加时区而来
    ZonedDateTime zonedDateTime = instant.atZone(beijing);
    System.out.println("ZonedDateTime通过Instant转换而来(1):" + zonedDateTime);
    zonedDateTime = instant.atZone(hawaii);
    System.out.println("ZonedDateTime通过Instant转换而来(2):" + zonedDateTime);

    //ZonedDateTime带有时区的当前时间  LocalDateTime拼接上时区后缀
    zonedDateTime = localDateTime.atZone(hawaii);
    System.out.println("ZonedDateTime通过LocalDateTime拼接上时区后缀而来:" + zonedDateTime);

    System.out.println("//--------------------------------------------------------------------------------");

    //Instant常用时间操作
    System.out.println("Instant常用时间操作");
    System.out.println("获取时间戳(秒): " + instant.getEpochSecond());
    System.out.println("获取时间戳(毫秒): " + instant.toEpochMilli());
    System.out.println("取整: " + instant.truncatedTo(ChronoUnit.HOURS));

    //LocalDateTime,ZonedDateTime常用时间操作,二者共同的操作
    System.out.println("LocalDateTime,ZonedDateTime常用时间操作,二者共同的操作");

    System.out.println("获取时间戳(秒): " + localDateTime.toInstant(ZoneOffset.UTC));

    System.out.println("取日期" + localDateTime.getDayOfMonth());
    System.out.println("取星期" + localDateTime.getDayOfWeek());
    System.out.println("取日期加" + localDateTime.plusDays(1));
    System.out.println("取日期减" + localDateTime.minusDays(1));

    System.out.println("调整日期(时分秒不变)" + zonedDateTime.withDayOfMonth(1));
    System.out.println("调整日期(时分秒不变)" + zonedDateTime.withDayOfYear(1));

    System.out.println("日期比大小" + localDateTime.isAfter(localDateTime.plusDays(1)));
    System.out.println("计算时差");
    Duration duration = Duration.between(localDateTime, localDateTime
            .minusWeeks(10).plus(10, ChronoUnit.HOURS).plus(20, ChronoUnit.MINUTES));
    System.out.println("计算时差,一共相差天(差值全部转化)" + duration.toDays());
    System.out.println("计算时差,一共相小时(差值全部转化)" + duration.toHours());
    System.out.println("计算时差,一共相分钟(差值全部转化)" + duration.toMinutes());
    System.out.println("计算时差格式化" + format(duration));
    System.out.println("当天3点" + localDate.atTime(LocalTime.of(3, 0, 0)));

    Period period = Period.between(LocalDate.now(), LocalDate.of(2000, 12, 1));
    String s = String.format("计算时差相差%d年%02d月%02d日", period.getYears(), period.getMonths(), period.getDays());
    System.out.println(s);

    //LocalDate常用时间操作
    System.out.println("LocalDate常用时间操作");
    // 本月第一天
    System.out.println("本月第1天:" + localDate.with(TemporalAdjusters.firstDayOfMonth()));
    System.out.println("本月最后1天:" + localDate.with(TemporalAdjusters.lastDayOfMonth()));
    System.out.println("下月第1天:" + localDate.with(TemporalAdjusters.firstDayOfNextMonth()));
    System.out.println("本月第1个周一:" + localDate.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY)));
    System.out.println("本月第2个周二:" + localDate.with(TemporalAdjusters.dayOfWeekInMonth(2, DayOfWeek.TUESDAY)));
    System.out.println("本周三:" + localDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY)));
    System.out.println("下一个周一(不算今天否则加上OrSame):" + localDate.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));
    System.out.println("上一个周一(不算今天否则加上OrSame):" + localDate.with(TemporalAdjusters.previous(DayOfWeek.MONDAY)));
    System.out.println("当天0点:" + localDate.atStartOfDay());
    System.out.println("是否是闰年:" + localDate.isLeapYear());
    System.out.println("//--------------------------------------------------------------------------------");

    //DateTimeFormatter,用于【格式化时间为字符串】或【转字符串为时间】
    System.out.println("格式化操作");
    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    //时间格式转字符串格式 dateTimeFormatter.format或者DateTime.format都行
    System.out.println("时间转字符串(Formatter.format):" + dateTimeFormatter.format(localDateTime));
    //会去掉时区(除非指定显示时区)
    System.out.println("时间转字符串,会去掉时区(DateTime.format):" + zonedDateTime.format(dateTimeFormatter) + "  " + zonedDateTime);

    //字符串转时间
    LocalDateTime parseLocalDateTime = LocalDateTime.parse("2022-06-29T23:41:03");
    System.out.println("字符串转时间:" + parseLocalDateTime);

    //字符串转时间,使用自定义格式
    parseLocalDateTime = LocalDateTime.parse("2022-06-29 03:41:03", dateTimeFormatter);
    System.out.println("字符串转时间,使用自定义格式:" + parseLocalDateTime);

    System.out.println("夏威夷时区" + zonedDateTime);
    zonedDateTime = zonedDateTime.withZoneSameInstant(beijing);
    System.out.println("夏威夷转北京时区" + zonedDateTime);
}

使用Mybatis需要注意的点

  • 使用LocalDateTime,会使用数据库设置的时区,而不是Java应用的时区。

    • 在设置JDBC链接添加&serverTimezone=Asia/Shanghai
  • Java应用与数据库对应关系

    数据库类型 对应Java类型
    DATETIME LocalDateTime
    TIMESTAMP LocalDateTime
    DATE LocalDate
    TIME LocalTime

心得

  1. 为什么时间戳转LocalDataTime需要时区信息呢?LocalDataTime毕竟不带时区信息。

    假设不需要带有时区信息,那么时间戳转LocalDateTime应该就是默认的UTC时区对应的LocalDateTime,但这往往不是人们关心的。

    当人们使用LocalDataTime更关心自己当前时区的时间,故时间戳在转换成LocalDateTime时需要加上时区信息的操作才更为合理。

    但这不代表LocalDataTime需要保存时区信息,他只是一个时间值。

  2. 为什么时间戳转Instant不需要时区信息呢?

    Instant也可以理解成有时区(打印时有时区标识),但只能是UTC+0 GMT+0时区,故在存储时不需要加上时区信息也不会造成歧义。

    当人们使用Instant时往往会将其转换成其他可以使用本时区信息的类,如LocalDataTimeZonedDateTime,在转换的过程中加入时区信息即可。

maven多环境配置与打包插件

maven简略说明

Maven 本质上是一个项目管理和理解工具

Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成。

  • maven中基本的的内置变量如下

    变量名 路径
    ${project.xxx} 当前pom文件的任意节点的内容,如${project.packaging}pom中指定打包类型标签packaging
    ${basedir} 项目根目录
    ${project.parent.basedir} 按照上两条的规律,可理解成${project.xxx}+${basedir},即:父项目的根目录
  • apachepom-4.0.0.xml下定义的内置变量如下

    变量名 路径
    ${project.build.directory} ${project.basedir}/target
    ${project.build.outputDirectory} ${project.build.directory}/classes
    ${project.build.finalName} ${project.artifactId}-${project.version}
    ${project.build.testOutputDirectory} ${project.build.directory}/test-classes
    ${project.build.sourceDirectory} ${project.basedir}/src/main/java
    ${project.build.scriptSourceDirectory} ${project.basedir}/src/main/scripts
    ${project.build.testSourceDirectory} ${project.basedir}/src/test/java
    ${project.build.resources.resource.directory} ${project.basedir}/src/main/resources
    ${project.build.testResources.testResource.directory} ${project.basedir}/src/test/resources

maven多环境配置profiles

Maven多环境配置依赖于pom.xmlprofilesprofile指定的变量与激活条件

  • 多环境配置

    <!-- 环境信息 -->
    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <env>dev</env>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
        <profile>
            <id>test</id>
            <properties>
                <env>test</env>
            </properties>
        </profile>
        <profile>
            <id>prod</id>
            <properties>
                <env>prod</env>
            </properties>
        </profile>
    </profiles>

    其中,在mvncompilepackage等情况下,加上参数-P dev即激活profiles.profile.id对应的dev环境

  • springboot配置文件中,指定激活配置文件方式如下

    spring:
      profiles:
        active: @env@

    @env@指代pom.xmlprofiles.profile.properties指定的参数

    这样就会激活对应的配置文件

    application-dev.yml
    application-prod.yml
    application-test.yml

maven打包插件assembly

该插件可以将你想指定的东西塞到一起

  1. pom中引入插件

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>2.4</version>
        <executions>
            <execution>
                <id>assembly</id>
                <phase>package</phase>
                <goals>
                    <goal>single</goal>
                </goals>
                <configuration>
                    <descriptors>
                        <descriptor>${basedir}/src/main/assembly/assembly.xml</descriptor>
                    </descriptors>
                </configuration>
            </execution>
        </executions>
    </plugin>
  2. ${basedir}/src/main/assembly/assembly.xml中的配置说明

    <assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
        <id>auto-assembly</id>
        <formats>
            <format>tar.gz</format>
        </formats>
        <includeBaseDirectory>true</includeBaseDirectory>
    
        <fileSets>
            <!-- config files -->
            <fileSet>
                <directory>${project.build.directory}/classes/</directory>
                <excludes></excludes>
                <includes>
                    <include>application*.yml</include>
                </includes>
                <fileMode>0644</fileMode>
                <outputDirectory>/</outputDirectory>
            </fileSet>
            <!-- executable jar -->
            <fileSet>
                <directory>${project.build.directory}</directory>
                <includes>
                    <include>${project.artifactId}-${project.version}.jar</include>
                </includes>
                <fileMode>0755</fileMode>
                <outputDirectory>/</outputDirectory>
            </fileSet>
        </fileSets>
    </assembly>

    该插件新建了一个包,名字叫:

    ${project.artifactId}-${project.version}-${assembly.id}-${assembly.formats.format}

    fileSets中指定了两个fileSet

    application*.yml${project.artifactId}-${project.version}.jar放到了新包下。

踩坑:maven项目引入外部jar

项目中遇到一种场景,在编译源码时需要引入外部jar,此时有三种方式,即上传私服,上传本地仓库及在工程中直接引入。但是这三种方式均有坑!

容易踩坑的地方

  1. 通过将外部jar上传到私服与本地仓库,必须同时上传pom文件,否则引入不了外部jar依赖的jar包

    同时执行以下命令是正确用法

    要在本地存储库中安装 JAR,请使用以下命令:

    mvn install:install-file -Dfile=<path-to-file> -DgroupId=<group-id> -DartifactId=<artifact-id> -Dversion=<version> -Dpackaging=<packaging>

    如果还有 pom 文件,您可以使用以下命令安装它:

    mvn install:install-file -Dfile=<path-to-file> -DpomFile=<path-to-pomfile>
  2. 通过<scope>system</scope>引入的外部jar无法引入外部jar依赖jar包

搭建工程并解析

工程介绍

项目依赖关系

项目B

  • pom文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>B</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.example</groupId>
                <artifactId>C</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </project>
  • JAVA代码

    public class Main {
        public static void main(String[] args) {
            Hello.sayHello();
        }
    }

项目C

  • pom文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>C</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
    
    </project>
  • JAVA代码

    /**
     * @author sakura
     * @date: 2022/6/17 22:04
     * @description:
     */
    public class Hello {
        public static void sayHello() {
            System.out.println("hello");
        }
    }
    

验证:

项目B依赖项目C,观察一下三种A依赖B的方式,能否引用到C的静态方法

  1. 外部jar上传到私服与本地仓库,但不上传pom文件
  2. 通过<scope>system</scope>方式在工程中直接引入

外部jar上传到私服与本地仓库,但不上传pom文件

  • 项目Apom文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>A</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.example</groupId>
                <artifactId>B</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </project>
  • 执行上传指令,这里使用本地仓库举例

    • 文件相对路径根位置,此时已经且换到B工程目录下。
    • -DgeneratePom=false:不自动生成pom文件。但即使自动生成的pom也带任何依赖,只有除依赖外其他信息。这里为了说明清晰就***不生成pom***了。
    mvn install:install-file -Dfile="target\B-1.0-SNAPSHOT.jar" -DgroupId="org.example" -DartifactId="B" -Dversion="1.0-SNAPSHOT" -Dpackaging="jar" -DgeneratePom=false
  • 此时切换到A工程下查看依赖树 mvn dependency:tree

    [INFO] ---------------------------< org.example:A >----------------------------
    [INFO] Building A 1.0-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [WARNING] The POM for org.example:B:jar:1.0-SNAPSHOT is missing, no dependency information available
    [INFO] 
    [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ A ---
    [INFO] org.example:A:jar:1.0-SNAPSHOT
    [INFO] \- org.example:B:jar:1.0-SNAPSHOT:compile
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.776 s
    [INFO] Finished at: 2022-06-17T22:56:55+08:00
    [INFO] ------------------------------------------------------------------------
    

    得出结论C的依赖没有被打包进来,自然无法调用。

通过<scope>system</scope>方式在工程中直接引入

  • 项目Apom文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>org.example</groupId>
        <artifactId>A</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>8</maven.compiler.source>
            <maven.compiler.target>8</maven.compiler.target>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.example</groupId>
                <artifactId>B</artifactId>
                <version>1.0-SNAPSHOT</version>
                <scope>system</scope>
                <systemPath>${project.basedir}/lib/B-1.0-SNAPSHOT.jar</systemPath>
            </dependency>
        </dependencies>
    </project>
  • 项目根目录下创建lib文件夹并将打包好的B工程放进去

  • 此时切换到A工程下查看依赖树 mvn dependency:tree

    [INFO] ---------------------------< org.example:A >----------------------------
    [INFO] Building A 1.0-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [INFO] 
    [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ A ---
    [INFO] org.example:A:jar:1.0-SNAPSHOT
    [INFO] \- org.example:B:jar:1.0-SNAPSHOT:system
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.797 s
    [INFO] Finished at: 2022-06-17T23:07:41+08:00
    [INFO] ------------------------------------------------------------------------

    得出结论C的依赖没有被打包进来,自然无法调用。

正确用法

既要上传jar同时要上传POM文件

  • 切换到B工程目录下,执行

    mvn install:install-file -Dfile="target\B-1.0-SNAPSHOT.jar" -DgroupId="org.example" -DartifactId="B" -Dversion="1.0-SNAPSHOT" -Dpackaging="jar" -DgeneratePom=false
    mvn install:install-file -Dfile="pom.xml" -DgroupId="org.example" -DartifactId="B" -Dversion="1.0-SNAPSHOT" -Dpackaging="pom" -DgeneratePom=false
  • 此时切换到A工程下查看依赖树 mvn dependency:tree

    [INFO] ---------------------------< org.example:A >----------------------------
    [INFO] Building A 1.0-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [INFO] 
    [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ A ---
    [INFO] org.example:A:jar:1.0-SNAPSHOT
    [INFO] \- org.example:B:jar:1.0-SNAPSHOT:compile
    [INFO]    \- org.example:C:jar:1.0-SNAPSHOT:compile
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.824 s
    [INFO] Finished at: 2022-06-17T23:23:14+08:00
    [INFO] ------------------------------------------------------------------------
  • 得出结论C的依赖被打包进来,可以调用

lsf4j日志门面整合多种日志框架

什么是日志门面

什么是门面,为什么要使用门面

门面指的是一种设计模式–JAVA设计模式之门面模式(外观模式),文中总结了门面模式的优点:

松散耦合: 门面模式松散了客户端与子系统的耦合关系,让子系统内部的模块能更容易扩展和维护。

简单易用: 门面模式让子系统更加易用,客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟门面类交互就可以了。

更好的划分访问层次: 通过合理使用Facade,可以帮助我们更好地划分访问的层次。有些方法是对系统外的,有些方法是系统内部使用的。把需要暴露给外部的功能集中到门面中,这样既方便客户端使用,也很好地隐藏了内部的细节。

而标题中所提到的lsf4j正是常用的日志门面之一。当我们引入日志门面框架并使用其接口时,不论系统中使用的是哪种种框架,只要符合日志门面框架的接口就能随时切换;当我们系统中引用了jar包时,也可以排除掉jar包使用的日志框架,使得日志格式保持一致,以便后续对日志进行分析挖掘。

常用的日志门面有哪些

  • slf4j
  • JCL

lsf4j是目前市面上最流行的日志门面,使用 lsf4j可以很灵活的使用占位符进行参数占位,简化代码,拥有更好的可读性。

常用的日志框架及其引入方式

  • Log4j

    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${version}</version>
    </dependency>
  • Log4j2

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>${version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${version}</version>
    </dependency>
  • LogBack

    <!-- slf4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${version}</version>
    </dependency>
      
    <!-- logback -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>${version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${version}</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-access</artifactId>
        <version>${version}</version>
    </dependency>

常用日志框架如何使用lsf4j门面

框架名称 如何使用lsf4j标准
Log4j 添加slf4j-log4j12依赖
Log4j2 添加log4j-slf4j-impl依赖
LogBack 无须操作,自身已实现slf4j标准(从LogBack配置文件也可看出)

lsf4j整合Log4j,LogBack,Log4j2

当我们系统中引用了jar包时,很容易就造成系统中存在多种日志框架共存的局面。

  • 多数场景下,引入的jar都是实现了lsf4j门面标准。我们可以在pom文件中,找到引入其他日志框架的jar,通过exclusion标签将该日志框架排除。

    框架名称 删除依赖
    Log4j log4j,slf4j-log4j12
    Log4j2 log4j-api,log4j-core
  • 凡事都有例外,在一些场景中,引入的jar没有实现lsf4j门面标准,也没有办法修改源码,添加lsf4j门面标准(即无法通过添加依赖的方式使用lsf4j门面),那该怎么办呢?答案还是修改pom文件,排除掉未实现lsf4j门面标准的jar,并引入新的依赖,新的依赖使用适配器模式,使其实现lsf4j门面标准。

    框架名称 删除依赖 补充依赖
    Log4j log4j log4j-over-slf4j
    Log4j2 log4j-api,log4j-core log4j-to-slf4j