SpringSecurity用户认证的流程

2020-04-02   龙德   SpringSecurity   SpringSecurity  

认证流程如下图所示:

image

根据流程图实现的伪代码:

1.认证拦截器

/**
     * 用于处理来自表单提交的认证拦截器,Spring Security 会默认提供
     * <p> 父类已经实现了 doFilter() 方法,我们只需要实现 attemptAuthentication() 方法,
     * 进行身份认证就可以了
     * 
     */
class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    protected MyUsernamePasswordAuthenticationFilter(String defaultFilterProcessesUrl) {
        super(defaultFilterProcessesUrl);
    }

    // 执行实际的身份认证
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        
        // 客户端提交的用户名
        String username = request.getParameter("username");
        // 客户端提交的密码
        String password = request.getParameter("password");
        
        // 封装成一个未认证的 Authentication 对象
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        // 调用认证管理器进行认证,返回一个已经认证的 Authentication 对象
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
}

2.认证管理器

/**
 * 认证管理器,Spring Security 会默认提供
 */
class MyAuthenticationManager implements AuthenticationManager {

    // 认证提供者(可以有多个)
    private List<AuthenticationProvider> providers = Collections.emptyList();
    
    public MyAuthenticationManager(List<AuthenticationProvider> providers) {
        this.providers = providers;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        Authentication result = null;
        for (AuthenticationProvider provider : providers) {
            if (!provider.supports(toTest)) {
                continue;
            }
            // 调用认证提供者进行认证,如果 result 不为 null ,说明认证通过
            result = provider.authenticate(authentication);
            if (result != null) {
                break;
            }
        }
        if (result == null) {
            throw new ProviderNotFoundException("ProviderManager.providerNotFound");
        }
        return result;
    }

}

3.认证提供者

/**
 * 认证提供者,对用户的信息进行认证,并返回一个完整的 Authentication 对象,Spring Security 会默认提供
 * 
 * <p>认证提供者需要实现 AuthenticationProvider 接口,该接口定义了一个认证的方法 authenticate()。
 * <p>由于 Spring Security 提供了一个抽象类 AbstractUserDetailsAuthenticationProvider,
 * 并且它已经实现了 authenticate() 方法,所以我们不需要自己实现,直接继承这个抽象类即可。
 * 
 */
class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    // 认证的 Service
    private UserDetailsService userDetailsService;
    
    // 密码认证处理器
    private PasswordEncoder passwordEncoder;
    
    public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * 这个方法用于添加额外的检查功能,我们不需要添加,所以空着即可
     */
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        
    }

    /**
     * 这个方法很重要,用于认证用户提供的信息是否正确,
     * 并且返回一个 UserDetails 对象,父类的 authenticate() 方法会用到这个对象
     */
    @Override
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        // 调用认证服务接口,加载 UserDetails 对象
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            throw new UsernameNotFoundException(username);
        }
        // 判断用户名和密码是否正确,如果正确直接返回
        if (userDetails.getUsername().equals(authentication.getPrincipal().toString()) 
                && passwordEncoder.isPasswordValid(userDetails.getPassword(), authentication.getCredentials().toString(), null)) {
            return userDetails;
        }
        throw new BadCredentialsException("username: " + username + ", credentials: " + authentication.getCredentials());
    }

}

4.认证服务

/**
 * 认证服务,一般需要我们提供
 * 
 * <p>认证服务需要实现 UserDetailsService 接口,该接口只定义了一个方法 loadUserByUsername()
 * <p>这里就是我们的认证逻辑了,可以根据 username 去数据库查找用户的信息,角色以及权限,然后包装成 UserDetails 对象,
 * 供后续使用
 */
class MyUserDetailsService implements UserDetailsService {

    /**
     * 这里应该查数据库来组装 UserDetails 对象,
     * 为了演示方便我直接写死了
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 存放用户的角色或者权限
        List<GrantedAuthority> authorities = new ArrayList<>();
        
        // 角色的名字以 "ROLE_" 开头
        SimpleGrantedAuthority authority1 = new SimpleGrantedAuthority("ROLE_ADMIN");
        // 权限的名字没有限制,起啥都行
        SimpleGrantedAuthority authority2 = new SimpleGrantedAuthority("read");
        
        // 添加角色
        authorities.add(authority1);
        // 添加权限
        authorities.add(authority2);
        
        // 构造 User 对象并返回,构造参数的含义注释都有写
        return new org.springframework.security.core.userdetails.User(username, "64c8b1e43d8ba3115ab40bcea57f010b", true, true, true,
                true, authorities);
        
    }
    
}

5.密码认证处理器

/**
 * 密码认证处理器,如果你数据库里的密码是加密的话,还需要提供一个密码认证处理器
 * 
 * <p>构造方法接收一个 String 类型的参数,用来指定加密的类型
 * <p>也就是说你数据库里的密码是用什么加密的,这里就传入什么,后面的认证提供者会用到
 */
class MyMessageDigestPasswordEncoder extends MessageDigestPasswordEncoder {

    public MyMessageDigestPasswordEncoder(String algorithm) {
        super(algorithm);
    }

    /**
     * @param encPass 数据库密码(通常是加密的)
     * @param rawPass 前端传送过来的密码(通常是明文的)
     * @param salt 盐值
     */
    @Override
    public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
        // 此处应该根据你自己的加密规则来校验密码
        
        // 为了演示方便,我这里只简单的进行比较
        return Objects.equals(encPass, rawPass);
    }
    
}

伪代码执行的流程图如下:

image

调用堆栈如下:

image

SpringSecurity认证失败后,转发到登录页面,造成无限循环

2020-03-17   龙德   SpringSecurity   SpringSecurity  

SpringSecurity 踩坑了,遇到了无限循环认证的问题。具体是认证失败后,我想转发到登录页面,并且返回一些提示信息,结果就踩坑了。

我是 SpringMVC 4.1.7,SpringSecurity 4.0.4

这是 xml 配置

<security:form-login login-page="/admin/login"
    login-processing-url="/admin/login"
    username-parameter="username"
    password-parameter="password" />

我配置了登录页面是 /admin/login,登录表单提交的地址是 /admin/login

当认证失败时,SpringSecurity 默认的失败处理器是 SimpleUrlAuthenticationFailureHandler 类,它默认采用重定向的方式,重定向到 login-page 设置的 URL,也就是重定向到登录页面。

然后我想在认证失败后,根据失败信息返回给客户端一些友好的提示。但是采用重定向的方式,我的提示信息就丢失了,客户端无法收到提示信息。所以我想将重定向的方式改成转发的方式。

这是我改后的 xml 配置

<security:form-login login-page="/admin/login"
    login-processing-url="/admin/login"
    username-parameter="username"
    password-parameter="password" 
    authentication-failure-handler-ref="simpleUrlAuthenticationFailureHandler"/>
    
<bean id="simpleUrlAuthenticationFailureHandler" 
    class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <constructor-arg name="defaultFailureUrl" value="/admin/login"></constructor-arg>
    <property name="useForward" value="true" />
</bean>

我手动配置了 SimpleUrlAuthenticationFailureHandler 类,将 useForward 属性设置为 true,表示启用转发的方式。defaultFailureUrl 属性设置为 /admin/login,表示转发到登录页面。这里说明一下,defaultFailureUrl 属性是认证失败后,重定向或者转发的 URL。如果不设置,默认是 login-page 的值。

重启后发现果然采用了转发的方式,但是这样又遇到了一个问题,转发到 /admin/login 之后,又进入了认证的方法,认证失败后又转发到 /admin/login,然后又进入认证的方法,一直无限循环下去。。。

仔细想了一下,应该是我的配置出了问题

我配置的登录页是 /admin/login,请求方法是 GET,登录表单提交的地址是 /admin/login,请求方式是 POST,也就是说登录页面和登录提交的地址都是同一个,唯一的区别是请求方法不同。

转发之前的地址是 admin/login,请求方法是 POST,由于转发是同一个请求,它的请求方法不会变,所以转发的请求方法也是 POST。又由于我设置的转发 URL 是admin/login,所以转发之后又会进入认证的方法,相当于重新提交了登录表单。所以无论如何都不会进入到登录页面,所以就会造成无限循环。

这里也特别说明一下,为什么默认的重定向不会造成无限循环。因为重定向是客户端的行为,相当于客户端重新发起请求,并且重定向肯定是 GET 请求,所以会正常进入到登录页面,不会造成无线循环。

最后的 xml 配置如下:

<security:form-login login-page="/admin/login.jsp"
    login-processing-url="/admin/login"
    username-parameter="username"
    password-parameter="password" 
    authentication-failure-handler-ref="simpleUrlAuthenticationFailureHandler"/>
    
<bean id="simpleUrlAuthenticationFailureHandler" 
    class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
    <constructor-arg name="defaultFailureUrl" value="/admin/login.jsp"></constructor-arg>
    <property name="useForward" value="true" />
</bean>

结论:login-pagelogin-processing-url 最好不要设置相同的 URL

SpringMVC集成SpringSecurity报错-java.lang.NoSuchMethodError-org.springframework.uti.AntPathMatcher.setCaseSensitive(Z)V

2020-03-17   龙德   SpringSecurity   SpringSecurity  

我的 SpringMVC 版本是 4.1.7,SpringSecurity 版本是 4.1.5

在 SpringMVC 里集成 SpringSecurity 后,运行出错:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityFilterChainRegistration' defined in class path resource [org/springframework/boot/autoconfigure/security/SpringBootWebSecurityConfiguration.class]: Unsatisfied dependency expressed through constructor argument with index 0 of type [javax.servlet.Filter]: : Error creating bean with name 'org.springframework.security.filterChainProxy': Cannot resolve reference to bean 'org.springframework.security.filterChains' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChains': Cannot resolve reference to bean 'org.springframework.security.web.DefaultSecurityFilterChain#0' while setting bean property 'sourceList' with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.DefaultSecurityFilterChain#0': Cannot create inner bean '(inner bean)#3da73b75' of type [org.springframework.security.web.util.matcher.AntPathRequestMatcher] while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChainProxy': Cannot resolve reference to bean 'org.springframework.security.filterChains' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChains': Cannot resolve reference to bean 'org.springframework.security.web.DefaultSecurityFilterChain#0' while setting bean property 'sourceList' with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.DefaultSecurityFilterChain#0': Cannot create inner bean '(inner bean)#3da73b75' of type [org.springframework.security.web.util.matcher.AntPathRequestMatcher] while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:749)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:464)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1119)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1014)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.boot.context.embedded.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:176)
    at org.springframework.boot.context.embedded.ServletContextInitializerBeans.addServletContextInitializerBeans(ServletContextInitializerBeans.java:80)
    at org.springframework.boot.context.embedded.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:68)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.getServletContextInitializerBeans(EmbeddedWebApplicationContext.java:216)
    at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext$1.onStartup(EmbeddedWebApplicationContext.java:202)
    at org.springframework.boot.context.embedded.tomcat.ServletContextInitializerLifecycleListener.lifecycleEvent(ServletContextInitializerLifecycleListener.java:64)
    at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:117)
    at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:90)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5095)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1409)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1399)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChainProxy': Cannot resolve reference to bean 'org.springframework.security.filterChains' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChains': Cannot resolve reference to bean 'org.springframework.security.web.DefaultSecurityFilterChain#0' while setting bean property 'sourceList' with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.DefaultSecurityFilterChain#0': Cannot create inner bean '(inner bean)#3da73b75' of type [org.springframework.security.web.util.matcher.AntPathRequestMatcher] while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:634)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:140)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1139)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1042)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1120)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1044)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:942)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:813)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
    ... 25 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.filterChains': Cannot resolve reference to bean 'org.springframework.security.web.DefaultSecurityFilterChain#0' while setting bean property 'sourceList' with key [0]; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.DefaultSecurityFilterChain#0': Cannot create inner bean '(inner bean)#3da73b75' of type [org.springframework.security.web.util.matcher.AntPathRequestMatcher] while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:108)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveManagedList(BeanDefinitionValueResolver.java:382)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:157)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyPropertyValues(AbstractAutowireCapableBeanFactory.java:1477)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1222)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 41 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.security.web.DefaultSecurityFilterChain#0': Cannot create inner bean '(inner bean)#3da73b75' of type [org.springframework.security.web.util.matcher.AntPathRequestMatcher] while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:313)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveValueIfNecessary(BeanDefinitionValueResolver.java:129)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveConstructorArguments(ConstructorResolver.java:634)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:140)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1139)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1042)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
    ... 53 more
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#3da73b75': Bean instantiation via constructor failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:275)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1139)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1042)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:504)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476)
    at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveInnerBean(BeanDefinitionValueResolver.java:299)
    ... 65 more
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.util.matcher.AntPathRequestMatcher]: Constructor threw exception; nested exception is java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:163)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:122)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:267)
    ... 70 more
Caused by: java.lang.NoSuchMethodError: org.springframework.util.AntPathMatcher.setCaseSensitive(Z)V
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher$SpringAntMatcher.createMatcher(AntPathRequestMatcher.java:291)
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher$SpringAntMatcher.<init>(AntPathRequestMatcher.java:275)
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher$SpringAntMatcher.<init>(AntPathRequestMatcher.java:268)
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher.<init>(AntPathRequestMatcher.java:134)
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher.<init>(AntPathRequestMatcher.java:101)
    at org.springframework.security.web.util.matcher.AntPathRequestMatcher.<init>(AntPathRequestMatcher.java:87)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:147)
    ... 72 more

原因是 spring-core-4.1.x 不存在 AntPathMatchersetCaseSensitive() 方法,请参阅 4.1.X.RELEASE 上的 AntPathMatcher4.2.X.RELEASE 上的 AntPathMatcher

解决方法:使用 spring-core-4.2.x 版本,或者降低 SpringSecurity 的版本。

我采用的是第二种方法,将 4.1.5 改成 4.0.4,这是我修改后的 pom.xml

<dependency> 
    <groupId>org.springframework.security</groupId> 
    <artifactId>spring-security-web</artifactId> 
    <version>4.0.4.RELEASE</version> 
</dependency> 
<dependency> 
    <groupId>org.springframework.security</groupId> 
    <artifactId>spring-security-config</artifactId> 
    <version>4.0.4.RELEASE</version > 
</dependency>

MyBatis删除数据,不报错,SQL能执行,但是数据并没有删除

2020-03-15   龙德   MyBatis   MyBatis  

遇到一个诡异的问题,MyBatis 根据主键 ID 删除数据,不报错,SQL 也正常打印出来,但是数据并没有删除。拎 SQL 到数据库中执行,却能正常执行,能把数据删除。

MyBatis 打印的 SQL,好像没啥问题

image

但是数据还在,并没有删除

image

拎 SQL 到数据库中执行,却能正常执行,能把数据删除。

image

这真是太奇怪了

一开始想的会不会是 MyBatis 没有 commit,但是仔细想,MyBatis 不是自动 commit 的吗,insert 和 update 都能正常执行,怎么 delete 反而没效果呢?

debug 看看

image

差点吐血。。。原来是 ID 前面多了一个空格。。。

改了之后果然能删除了

image

image

这种错误真的是低级而又不容易发现

SpringBoot解决form表单提交中文乱码

2020-02-28   龙德   SpringBoot   乱码  

form 表单提交数据映射到后台对象,中文出现乱码

image

解决方法如下:

1.application.properties 文件配置字符编码

spring.banner.charset=UTF-8
spring.messages.encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.force=true
spring.http.encoding.enabled=true
server.tomcat.uri-encoding=UTF-8

2.配置 CharacterEncodingFilter

之前 SpringMVC 是在 web.xml 里配置的

<filter>
	<description>字符集过滤器</description>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
    	<description>字符集编码</description>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
		<param-name>forceEncoding</param-name>
		<param-value>true</param-value>
	</init-param>
</filter>
<filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

SpringBoot 的话直接注入 bean 即可

@Configuration
public class CharacterEncodingFilterConfig {

	@Bean
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setForceEncoding(true);
        characterEncodingFilter.setEncoding("UTF-8");
        registrationBean.setFilter(characterEncodingFilter);
        return registrationBean;
    }

}

重启之后,重新提交表单,中文就可以正常显示了

image

SpringBoot 处理全局异常,包括 404 错误,返回 JSON 或者页面

2020-01-21   龙德   SpringBoot   异常处理 HandlerExceptionResolver ControllerAdvice ExceptionHandler ErrorController  

第一种方式:实现 HandlerExceptionResolver 接口

BaseException 是我自定义的业务异常类,包含错误码和错误信息。

public class BaseExceptionHandler implements HandlerExceptionResolver {

	@Override
	public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
			Exception e) {
		String contentType = request.getContentType();
		// 返回 json
		if (Objects.equals("application/json", contentType)) {
		    // 使用这个类可以返回 json 数据
			MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
			jsonView.setExtractValueFromSingleKeyModel(true);
			ModelAndView mv = new ModelAndView(jsonView);
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject(new Result<>(be.getErrorCode(), be.getMessage()));
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject(new Result<>(BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode(),
						BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()));
				return mv;
			}
		} else {
		    // 返回页面
			ModelAndView mv = new ModelAndView();
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject("exception", be.getMessage());
				mv.addObject("errorCode", be.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
				mv.addObject("errorCode", BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			}
		}
	}

}

这种方式只能处理 Controller 层抛出的异常,对于 DispatcherServlet 抛出的异常无能为力。

第二种方式:使用 @ControllerAdvice 和 @ExceptionHandler 注解

@ControllerAdvice
public class GlobalExceptionHandler {
	
	@ExceptionHandler(value = Exception.class)
	public ModelAndView defaultErrorHandler(Exception e, HttpServletRequest request, HttpServletResponse response) throws Exception {
		String contentType = request.getContentType();
		if (Objects.equals("application/json", contentType)) {
			MappingJackson2JsonView jsonView = new MappingJackson2JsonView();
			jsonView.setExtractValueFromSingleKeyModel(true);
			ModelAndView mv = new ModelAndView(jsonView);
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject(new Result<>(be.getErrorCode(), be.getMessage()));
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject(new Result<>(BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode(),
						BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()));
				return mv;
			}
		} else {
			ModelAndView mv = new ModelAndView();
			if (e instanceof BaseException) {
				BaseException be = (BaseException) e;
				response.setStatus(be.getHttpCode());
				mv.addObject("exception", be.getMessage());
				mv.addObject("errorCode", be.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			} else {
				response.setStatus(BaseErrorCodeEnum.INTERNAL_ERROR.getHttpCode());
				mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
				mv.addObject("errorCode", BaseErrorCodeEnum.INTERNAL_ERROR.getErrorCode());
				mv.setViewName("/default/front/error/error");
				return mv;
			}
		}
	  }
	
}

这种方式可以处理 Controller 层和 DispatcherServlet 抛出的异常,所以用这种方式就好了。

处理 404 错误

DispatcherServlet 中有一个 throwExceptionIfNoHandlerFound 属性,如下图所示:

image

它的作用是:如果找不到处理该请求的处理程序,是否抛出 NoHandlerFoundException?

它默认是 false,也就是说找不到处理器时不抛出 NoHandlerFoundException 异常,DispatcherServlet 会自己处理。

所以上面那两个全局处理类都无法捕获到 NoHandlerFoundException 异常,也就无法处理 404 错误。

你可以在 application.properties 加上这段参数,将 throwExceptionIfNoHandlerFound 属性设置为 true。

spring.mvc.throw-exception-if-no-handler-found=true

但是这样还是无法起作用,关键是这段代码:

image

这段代码表明只有找不到处理器,也就是 mappedHandler 为 null 的情况下,才会进入 noHandlerFound 方法。

然而 SpringBoot 会默认配置一个静态资源映射处理器 ResourceHttpRequestHandler,所以不会进入 noHandlerFound 方法。

image

幸好 SpringBoot 还提供了配置接口,我们只要在 application.properties 里加入以下配置,即可关闭默认的静态资源映射处理器。

spring.resources.add-mappings=false

这样,当 DispatcherServlet 找不到处理器时,又没有默认的静态资源映射处理器,同时我们把 throwExceptionIfNoHandlerFound 属性设置了 true,那么 DispatcherServlet 就会抛出 NoHandlerFoundException 异常。就会被我们上面定义的 GlobalExceptionHandler 类捕获到,返回我们自定义的处理结果。

但是这种方式有一个弊端,就是关闭静态资源映射处理器后,无法访问到静态资源,只适合纯后端接口,没有前端的项目。

如果在不影响静态资源访问的情况下,又能处理 404 错误呢?请看下面这种方式:

SpringBoot 提供了一个接口,名字叫做 ErrorController。这个接口有一个实现类 BasicErrorController,它主要定义了两个方法:

image

DispatcherServlet 处理一个不存在的请求时,该请求会转发到 /${context-path}/error,并且不会被 ControllerAdvice 拦截。

它默认的页面是这样的:

image

默认的 JSON 是这样的:

image

所以,我们可以自己定义一个 BasicErrorController,返回自定义的页面和 JSON 数据。

比如我是这样写的:

@RequestMapping(value = "/error", produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) {
	ModelAndView mv = new ModelAndView();
	mv.addObject("exception", BaseErrorCodeEnum.INTERNAL_ERROR.getMessage());
	mv.addObject("errorCode", getStatus(request).value());
	mv.setViewName("/default/front/error/error");
	return mv;
}

@RequestMapping(value = "/error")
@ResponseBody
public ResponseEntity<Result<String>> error(HttpServletRequest request) {
	HttpStatus status = getStatus(request);
	return new ResponseEntity<Result<String>>(
			new Result<>(String.valueOf(status.value()), BaseErrorCodeEnum.INTERNAL_ERROR.getMessage()), status);
}

这时候 ErrorController 接口应该有两个实现类,一个是 SpringBoot 的,还有一个是自己定义的。

image

但是这样也不会冲突,因为 SpringBoot 会判断,如果已经存在 ErrorController 的实现类时,它就不会加载 BasicErrorController。

源码链接:

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/handler/BaseExceptionHandler.java

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/handler/GlobalExceptionHandler.java

https://github.com/miansen/Roothub/blob/master/src/main/java/wang/miansen/roothub/common/controller/BaseErrorController.java

成员变量和静态方法不具有多态性

2020-01-18   龙德   Java   多态  

成员变量不具有多态性,请看下面的例子:

public class Super {

	public int field = 0;

	public int getField() {
		return field;
	}

}
public class Sub extends Super {

	public int field = 1;

	public int getField() {
		return field;
	}
	
	public int getSuperField() {
		return super.field;
	}
	
}
public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println("sup.field = " + sup.field + ", sup.getField() = " + sup.getField());
		Sub sub = new Sub();
		System.out.println("sub.field = " + sub.field + ", sub.getField() = " + sub.getField()
				+ ", sub.getSuperField() = " + sub.getSuperField());
	}

输出结果:

sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

本例中,Super 和 Sub 各有一个成员变量 field,当 Sub 继承 Super 后,Sub 实际包含两个名为 field 的成员变量:一个是它自己的,还有一个是从 Super 那里继承得来的。尽管这两个成员变量的名字相同,但是它们不是同一个变量,因为它们在内存中分配了不同的存储空间。当 Sub 对象向上转型为 Super 引用时,任何成员变量的访问操作都将由编译器解析。所以使用 Super 类型的引用访问 field 变量时,指向的是 Super 类中的 field。同理,使用 Sub 类型的引用访问 field 变量时,指向的是 Sub 类中的 field。你如果想访问从 Super 那里继承来的 field 时,必须使用 super 关键字,显示地调用 suepr.field。

虽然这看起来有些混淆,但是实际中,这些问题是不会发生的。首先,你通常会将所有的成员变量的访问权限都设置成 private,因此你无法直接访问它们,而是通过提供的 get 方法访问。另外,你通常不会对子类和父类的变量设置相同的名字,因为这种做法本身就容易令人混淆。

静态方法也不具有多态性,请看下面的例子:

public class Super {

	public static String staticGet() {
		return "Super staticGet()";
	}
	
	public String dynamicGet() {
		return "Super dynamicGet()";
	}

}
public class Sub extends Super {
	
	public static String staticGet() {
		return "Sub staticGet()";
	}
	
	public String dynamicGet() {
		return "Sub dynamicGet()";
	}
	
}
public static void main(String[] args) {
		Super sup = new Sub();
		System.out.println(sup.staticGet());
		System.out.println(sup.dynamicGet());
	}

输出结果:

Super staticGet()
Sub dynamicGet()

静态方法是与类,而非某个对象相关联的,所以静态方法不具有多态性。

Git报错:Your local changes to the following files would be overwritten by merge:xx

2020-01-11   龙德   杂七杂八   Git  

用git pull来更新代码的时候,遇到了下面的问题:

$ git pull
Updating 725dd43..c34cb92
error: Your local changes to the following files would be overwritten by merge:
        src/main/java/cn/roothub/bbs/common/dao/jdbc/builder/DataSourceBuilder.java
Please commit your changes or stash them before you merge.
Aborting

出现这个问题的原因是其他人修改了 xxx.java 并提交到版本库中去了,而你本地也修改了xxx.java,这时候你进行 git pull 操作就好出现冲突了。

有两种解决方法:

1、保留本地的修改

git stash
git pull
git stash pop

通过 git stash 将工作区恢复到上次提交的内容,同时备份本地所做的修改,之后就可以正常 git pull 了,git pull 完成后,执行 git stash pop 将之前本地做的修改应用到当前工作区。

git stash: 备份当前的工作区的内容,从最近的一次提交中读取相关内容,让工作区保证和上次提交的内容一致。同时,将当前的工作区内容保存到Git栈中。

git stash pop: 从Git栈中读取最近一次保存的内容,恢复工作区的相关内容。由于可能存在多个 Stash 的 内容,所以用栈来管理,pop 会从最近的一个 stash 中读取内容并恢复。

git stash list: 显示 Git 栈内的所有备份,可以利用这个列表来决定从那个地方恢复。

git stash clear: 清空 Git 栈。此时使用 gitg 等图形化工具会发现,原来 stash 的哪些节点都消失了。

2、放弃本地修改

这种方法会丢弃本地修改的代码,而且不可找回。

git reset --hard
git pull

通过上述的方法 pull 之后,可能还会报这个错误:

error: The following untracked working tree files would be overwritten by merge:
        src/main/java/wang/miansen/roothub/common/dto/BaseDTO.java
Please move or remove them before you merge.
Aborting

这是由于一些 untracked working tree files(未跟踪的文件)引起的问题。

可以执行这个命令删除这些未跟踪的文件:

# 首先确认要删除的文件
git clean -fd -n
# 如果以上命令给出的文件列表是你想删除的, 那么接下来执行
git clean -d -fx

对于这个命令 git clean -d -fx,-f表示文件 -d表示目录, 如果还要删除.gitignore中的文件那么再加上-x。

命名参考

git删除未跟踪文件

  • 删除 untracked files

git clean -f

  • 连 untracked 的目录也一起删掉

git clean -fd

  • 连 gitignore 的untrack 文件/目录也一起删掉 (慎用,一般这个是用来删掉编译出来的 .o之类的文件用的)

git clean -fdx

  • 在用上述 git clean 前,墙裂建议加上 -n 参数来先看看会删掉哪些文件,防止重要文件被误删
git clean -nf
git clean -nfd
git clean -nfdx

Java 的异常

2019-12-25   龙德   Java   Exception  

异常分类

Java 的异常分为两类,受检异常和非受检异常。也可以叫运行时异常和非运行时异常。都是一样的意思。

先看看受检异常

public void foo() throws Exception {
		System.out.println("hello world!");
	}

throws 关键字作用于方法名后面,作用是抛出异常。

这里抛出了 Exception 异常,这是个受检异常。也就是说调用方必须对这个异常进行处理,否则编译不会通过。

这是 Eclipse 给出的提示,要么继续抛出异常,交给上层调用者处理,要么就在方法里处理。

然后是非受检异常

public void foo() throws RuntimeException {
		System.out.println("hello world!");
	}

RuntimeException 就是一个非受检异常,从名字就可以知道,这是一个在程序运行时才有可能会发生的异常。

由于是运行时才会发生的异常,所以非受检异常不强制要求做处理。

异常堆栈

public void controller() {
	service();
}

public void service() {
	dao();
}

public void dao() {
	System.out.println(1 / 0);
}

public static void main(String[] args) {
	ExceptionTest exceptionTest = new ExceptionTest();
	exceptionTest.controller();
}

控制台输出的异常信息:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.test.ExceptionTest.dao(ExceptionTest.java:31)
	at com.test.ExceptionTest.service(ExceptionTest.java:25)
	at com.test.ExceptionTest.controller(ExceptionTest.java:19)
	at com.test.ExceptionTest.main(ExceptionTest.java:37)

异常堆栈的输出跟方法调用一样,也是先进后出的结构。main 方法最先调用,异常最后输出。dao 方法最后调用,异常最先输出。

异常链

捕捉住异常后,将老异常当做新异常的构造函数参数创建新异常并继续抛出,这样就形成了异常链。

public void controller() throws Exception {
	try {
		service();
	} catch (Exception e) {
		throw new Exception("controller 层抛出的异常", e);
	}
}

public void service() throws Exception {
	try {
		dao();
	} catch (Exception e) {
		throw new Exception("service 层抛出的异常", e);
	}
}

public void dao() throws Exception {
	try {
		System.out.println(1/0);
	} catch (Exception e) {
		throw new Exception("dao 层抛出的异常", e);
	}
}
	
public static void main(String[] args) {
	ExceptionTest exceptionTest = new ExceptionTest();
	try {
		exceptionTest.controller();
	} catch (Exception e) {
		e.printStackTrace();
	}
}

控制台输出的异常信息:

java.lang.Exception: controller 层抛出的异常
	at com.test.ExceptionTest.controller(ExceptionTest.java:21)
	at com.test.ExceptionTest.main(ExceptionTest.java:44)
Caused by: java.lang.Exception: service 层抛出的异常
	at com.test.ExceptionTest.service(ExceptionTest.java:29)
	at com.test.ExceptionTest.controller(ExceptionTest.java:19)
	... 1 more
Caused by: java.lang.Exception: dao 层抛出的异常
	at com.test.ExceptionTest.dao(ExceptionTest.java:37)
	at com.test.ExceptionTest.service(ExceptionTest.java:27)
	... 2 more
Caused by: java.lang.ArithmeticException: / by zero
	at com.test.ExceptionTest.dao(ExceptionTest.java:35)
	... 3 more

异常链也是先进后出的结构,新异常最先输出,老异常最后输出。也就是说第一个异常信息就是异常抛出的地方,最后一个异常信息就是产生异常的源头。

ApplicationContext 发布事件报错-Caused by-java.lang.IllegalStateException-ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context-Root WebApplicationContext-startup date [Sat Dec 14 15:02:30 CST 2019];root of context hierarchy

2019-12-16   龙德   SpringMVC   ApplicationContext 事件  

报错信息如下:

Caused by: java.lang.IllegalStateException: ApplicationEventMulticaster not initialized - call 'refresh' before multicasting events via the context: Root WebApplicationContext: startup date [Sat Dec 14 15:02:30 CST 2019]; root of context hierarchy
	at org.springframework.context.support.AbstractApplicationContext.getApplicationEventMulticaster(AbstractApplicationContext.java:344)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:331)
	at wang.miansen.roothub.common.dao.jdbc.spring.DataSourceConfiguration.afterPropertiesSet(DataSourceConfiguration.java:163)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1633)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1570)
	... 132 more

抛异常的地方:

可以看到 applicationEventMulticaster 为 null 就抛出了异常。

代码如下

package wang.miansen.roothub.common.dao.jdbc.spring;

public class DataSourceConfiguration implements FactoryBean<DataSource>, ApplicationContextAware, InitializingBean {

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

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 上下文容器,主要的作用是防止重复初始化数据源。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 创建数据源
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	protected <T> T createDataSource(Class<? extends DataSource> type) {
		return (T) dataSourceProperties.initializeDataSourceBuilder().type(type).build();

	}

    /**
     * 最终注入的数据源
     */
	@Override
	public DataSource getObject() throws Exception {
		return null;
	}

	@Override
	public Class<DataSource> getObjectType() {
		return DataSource.class;
	}

	@Override
	public boolean isSingleton() {
		return true;
	}

	
	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		// 只存在父容器时才初始化数据源,防止重复初始化。
		if (this.applicationContext.getParent() == null) {
			DataSourceInitializer initializer = getDataSourceInitializer();
			this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
		}
	}

}

DataSourceConfiguration 类实现了 ApplicationContextAware 和 InitializingBean 接口,本来想在 afterPropertiesSet 方法里发布初始化数据源的事件,结果就报了上面那个错误。

分析一下原因:

在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常

跟一下 ApplicationContext 初始化过程看看为什么 applicationEventMulticaster 会是 null。

Spring 中 AbstractApplicationContext 抽象类的 refresh() 方法是用来刷新 Spring 的应用上下文的,所以在 refresh() 方法处打个断点。

afterPropertiesSet() 方法也打上一个断点,然后启动项目。

进入了 refresh() 方法

image

其它的方法先不看,重点是 registerBeanPostProcessors(beanFactory) 方法。

这个方法是用来注册 BeanPostProcessor 的,需要在所有的 application bean 初始化之前调用,拦截 Bean 的创建。

进去看看

调用了一个静态方法,继续往下走。

这里是找出 BeanPostProcessor 接口的所有实现类。就不仔细看了,直接看这里:

可以看到这里调用了 beanFactory.getBean() 方法,触发了 bean 实例的创建。从而触发 setApplicationContext() 和 afterPropertiesSet() 方法。

但是初始化事件广播器的方法 initApplicationEventMulticaster() 是在 registerBeanPostProcessors() 方法之后。

所以导致在 afterPropertiesSet() 方法中发布事件时,ApplicationContext 的 applicationEventMulticaster 属性为 null,导致抛出异常。

解决方法:

既然执行 afterPropertiesSet() 方法时,ApplicationContext 的 applicationEventMulticaster 属性还没准备好,那么可以等 initApplicationEventMulticaster() 方法执行完之后才能发布事件了。

继续往下看,注册监听器的方法是 registerListeners();

那么可以换一种思路,当监听器注册完毕之后在发布事件,因为注册监听器的方法是在初始化事件广播器之后,可以保证在发布事件的时候事件广播器是可用的。

可以这么做,新建一个类,实现 BeanPostProcessor 和 ApplicationContextAware 接口,在 postProcessAfterInitialization() 方法里判断 bean 的类型是否为 DataSourceInitializerListener,如果是就发布事件,具体代码如下:

public class DataSourceInitializedPublisher implements BeanPostProcessor, ApplicationContextAware {

	/**
	 * 上下文容器,可以使用该对象来发布事件。
	 */
	private ApplicationContext applicationContext;

	/**
	 * 数据源
	 */
	private DataSource dataSource;

	/**
	 * 数据库基本配置
	 */
	private DataSourceProperties dataSourceProperties;

	/**
	 * 数据源初始化器
	 */
	private DataSourceInitializer dataSourceInitializer;
	
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
		// 当事件监听器初始化好之后发布初始化数据源的事件。
		if (bean instanceof DataSourceInitializerListener) {
			// 只存在父容器时才发布,防止重复初始化。
			if (this.applicationContext.getParent() == null) {
				DataSourceInitializer initializer = getDataSourceInitializer();
				this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer));
			}
		}
		return bean;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.applicationContext = applicationContext;
	}

	/**
	 * 实例化 DataSourceInitializer
	 * @return DataSourceInitializer
	 */
	public DataSourceInitializer getDataSourceInitializer() {
		if (this.dataSourceInitializer == null) {
			this.dataSourceInitializer = new DataSourceInitializer(this.dataSource, this.dataSourceProperties);
		}
		return this.dataSourceInitializer;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setDataSourceProperties(DataSourceProperties dataSourceProperties) {
		this.dataSourceProperties = dataSourceProperties;
	}

}

xml 配置:

<!-- 数据源初始化事件监听器 -->
<bean id="dataSourceInitializerListener" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializerListener" />
	
<!-- 发布初始化数据源事件 -->
<bean id="dataSourceInitializedPublisher" class="wang.miansen.roothub.common.dao.jdbc.spring.DataSourceInitializedPublisher" >
		<property name="dataSourceProperties" ref="dataSourceProperties" />
		<property name="dataSource" ref="dataSource" />
</bean>