【Java・Spring Boot】別パッケージにて、同じクラス名でDIしたいとき

Spring Bootで新しくControllerクラスを作ろうと思ったら、
サーバが起動しないエラーが起きたので、その解決方法を載せます。

少し長いので、見出しを頼りにしていただける大変嬉しいです。

別パッケージで、同じクラス名を作成し、起動したらエラーが起きた

見出しの通りで、新しくクラスを作るとき、既存のクラス名と同じクラス名を
別パッケージに作ろうとしたらエラーが起きました。

クラス構成、環境しては以下の通りになります。

Spring Bootの環境について

環境は以下

  • Java 1.8
  • STS 3.9.2
  • Spring Bootバージョン 1.5.10

※WindowsでもMacでも同じ解決方法なので、使用OSは省略

既存のクラス



package com.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.example.service.UserInfoService;

@Controller
@RequestMapping("/")
public class UserInfoController {
	
	@Autowired
	private UserInfoService userInfoService;

	@RequestMapping(method=RequestMethod.GET)
	public String index(Model model) {
		
		userInfoService.getUserInfo();
		
		return "main/index";
	}
}

新しく作ったクラス


package com.example.controller.userAdmin;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("/userAdmin")
public class UserInfoController {
	@RequestMapping(method=RequestMethod.GET)
	public String index(Model model) {
		
		return "admin/index";
	}
}

プロジェクトのパッケージ構成

起動しようとしたら以下のエラー

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [com.example.demo.SpringTest2Application]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userInfoController' for bean class [com.example.controller.userAdmin.UserInfoController] conflicts with existing, non-compatible bean definition of same name and class [com.example.controller.UserInfoController]
	at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:181) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:308) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:228) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:272) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:92) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:687) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:525) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:122) ~[spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:693) [spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:360) [spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:303) [spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118) [spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107) [spring-boot-1.5.10.RELEASE.jar:1.5.10.RELEASE]
	at com.example.demo.SpringTest2Application.main(SpringTest2Application.java:15) [classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_111]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_111]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_111]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_111]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-1.5.10.RELEASE.jar:1.5.10.RELEASE]
Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'userInfoController' for bean class [com.example.controller.userAdmin.UserInfoController] conflicts with existing, non-compatible bean definition of same name and class [com.example.controller.UserInfoController]
	at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.checkCandidate(ClassPathBeanDefinitionScanner.java:345) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:283) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:135) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:287) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:245) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:198) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:167) ~[spring-context-4.3.14.RELEASE.jar:4.3.14.RELEASE]
	... 18 common frames omitted

上記のエラーで調べたら以下のサイトがヒットしました。

どうやら、別パッケージでも、同じクラス名を使った場合、
DIするときに、クラス名のみでBean名をつけようとして、
同じBean名だから重複してしまい、結局紐づけられずにエラーが起きるみたいです。

原則同じBean名は使えないのがSpring Bootの仕様です。

そして上記サイトで解決方法が2種類あることを知りました。

1.ControllerのアノテーションにBean名をつける

今回はControllerでしたが、他にもcomponent、Serviceもできる模様
Bean名を固定でつけてあげることにより、Springがその指定した名前でBeanを作ってくれる。

@Controller("userAdminController")
public class UserInfoController

@Controller("userInfoController")
public class UserInfoController

それぞれのクラスにBean名を指定して、起動してみました。

起動してくれました。

ですが、これだと、一つ一つBean名を手作業で書かないといけないので
だいぶ手間です。
なので、そういう時はもう一つの方法を使うとすごく楽です。

2.ComponentScanのNameGenaratorを使い、作るBean名をFQCNにする

何を言っているかわかりやすくするなら・・・

「パッケージも含めたクラス名なら、確実にかぶる事ないから、その文字列をBean名にしようぜ!」

という事です。
つまりはFQCN(完全修飾クラス名)を使おうぜという事です。

NameGenaratorとは

Springが用意してくれている、Bean名を作成してくれる仕組み。というか設定
使うクラスは「AnnotationBeanNameGenerator」を継承したクラスです。
NameGenaratorを設定することにより、Componentがスキャンされるたびに
オーバーライドしたメソッドを呼び出し、処理が走るみたいです。
戻り値は文字列で、それがそのままそのクラスのBean名になります。
引数は、Componentが設定されているクラス自身です。

つまりはこの機構を使って、引数で受け取ったクラスを使い、
FQCNをBean名にするという仕組みができるわけです。

考えた方、本当に天才かと思いました。
FQCNはかぶることもないので、必ず一意のBean名をつけることができます。

作ったクラスは以下です。
FQCNを返してくれるメソッドを持ったクラス


package com.example.demo;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationBeanNameGenerator;

public class FQCNGenerator extends AnnotationBeanNameGenerator {

    @Override
    protected String buildDefaultBeanName(BeanDefinition definition) {
        return definition.getBeanClassName();
    }
}

その後、Applicationクラスに、ComponentScanを設定

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication(scanBasePackages={"com.example"})
@EntityScan("com.example.entity")
@EnableJpaRepositories("com.example.repository")
@ComponentScan(nameGenerator = FQCNGenerator.class)
public class SpringTest2Application {

	public static void main(String[] args) {
		SpringApplication.run(SpringTest2Application.class, args);
	}
}

そして起動

無事起動することができました。

まとめ

やっぱり大体はアノテーションで解決することができるんですね。
別パッケージなのにおかしいなぁ・・・って色々悩んでましたが、
Qiitaのサイトによって助けられました!
本当にありがとうございます!!

そして、FQCNはどういう状況でも役に立つことが理解できました。
個人的には手で名前を決める方が好きだったりしますが、
手間などを考えるとやはりFQCN一択になってしまうのかなと。
「意識しなくても良いところは意識しなくても良いように作る。」
というのも、製造する上で大事な考えだと思いました。

ちなみにセキュリティ面も含めて、何か一つWebアプリケーション作ってみようかなと思いました。

今回は以上!

スポンサーリンク

Java

Posted by タツノコ