悠蓦 | 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