Celem tego projektu jest pokazanie, w jaki sposób działa dependency injection framework np. Spring.
Repozytorium zademonstruje krok po kroku, w jaki sposób zbudować własny framework. Oczywiście jest to tylko uproszczona forma. Spring jest rozwijany od prawie 20 lat przez setki programistów, więc ledwo zbliżymy się do niego. Jednak repozytorium pokaże koncept takiego frameworka i udowodni, że nie kryje się tam żadna magia.
W repozytorium znajdziecie pakiety z poszczególnymi krokami, a poniżej jest ich omówienie:
ApplicationContextCompanyDao z implementacją CompanyDaoImplCompanyService z implementacją CompanyServiceImplModelu Company
Manualne zarządzanie transakcją
@Override
public void createCompany(Company company) {
try {
beginTransaction();
logger.info("SERVICE: START - create company");
companyDao.createCompany(company);
logger.info("SERVICE: END - create company");
commitTransaction();
} catch (Exception e) {
rollbackTransaction();
}
}
Proxy jest pośrednikiem, który w łatwy sposób pozwala nam na wykonanie czegoś przed i po konkretnej metodzie:
class MyClass {
void method() {
// ...
}
}
doSmthBefore();
myClass.method();
doSmthAfter();
Proxy- klasa, która odpowiada za tworzenie Proxy znajduje się w JDK.MyServiceImpl, musi ona implementować interfejs MyService.$Proxy.MyClass$$EnhancerBySpringCGLIB.
Żródło: https://www.baeldung.com/spring-aop-vs-aspectj
Domyślnym typem w Springu jest Dynamic Proxy.
Żródło: https://docs.spring.io/spring-framework/docs/5.3.x/reference/html/core.html#aop-introduction-proxies
Jednak gdy stworzymy nową aplikację w Spring Initializr to możemy się zdziwić. Pomimo tego, że dodamy interfejs to w debugu zobaczymy klasę stworzoną przez CGLib X$$EnhancerBySpringCGLIB
Czy dokumentacja kłamie? Nie, domyślna konfiguracja Springa używa Dynamic Proxy, ale już domyśla konfiguracja Spring Boot (2.x) używa CGLib. Powodem tego jest: prevent nasty proxy issues. A czym jest ten "paskudny problem"? Gdy programista spróbuje wstrzyknąć implementację zamiast interfejsu otrzymałby bład:
@Autowired
CompanyServiceImpl companyService;
// vs
@Autowired
CompanyService companyService;
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'companyServiceImpl' could not be injected as a 'com.patres.framework.service.CompanyServiceImpl' because it is a JDK dynamic proxy that implements:
com.patres.framework.service.CompanyService
Action:
Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.
Więcej na ten temat: https://www.programmersought.com/article/87046285018/
W tym projekcie będziemy używać Dynamic Proxy.
InvocationHandlerNiezbędna do tego jest implementacja InvocationHandler. Interfejs posiada 1 metodę:
public Object invoke(Object proxy, Method method, Object[] args)
Przyjmuje on 3 parametry:
* proxy — Instancja proxy, w której została wywołana metoda (nie będziemy jej używać, będziemy działali bezpośrednio na obiekcie. W przeciwnym wypadku wywołalibyśmy nieskonczoną rekurencję).
* method — Instancja Method, która jest wywoływana przez proxy.
* args — Argmenty przekazywane do metody (method).
Przykład implementacji proxy, która jeszcze nic nie robi:
public class ProxyHandler implements InvocationHandler {
private final Object objectToHandle;
public ProxyHandler(Object objectToHandle) {
this.objectToHandle = objectToHandle;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(objectToHandle, args);
}
}
CompanyServiceImpl).method.invoke(objectToHandle, args) — Robi dokładnie to samo co robiłaby nasza instancja CompanyServiceImpl, gdybyśmy ją wywoływali bezpośrednio. Metoda method jest wywoływana z instancji objectToHandle z argumentami args.Aby stworzyć proxy z handlerem, należy użyć klasę Proxy z JDK:
final CompanyDao companyDao = new CompanyDaoImpl();
final CompanyService companyServiceProxy = (CompanyService) Proxy.newProxyInstance(
Step2aApp.class.getClassLoader(),
new Class[]{CompanyService.class},
new ProxyHandler(new CompanyServiceImpl(companyDao))
);
Metoda newProxyInstance przyjmuje 3 parametry:
* loader — Class loader, który zdefiniuje proxy
* interfaces — Listę interfejsów
* h — Invocation handler: klasa, która ma obsłużyć to proxy (w naszym przypadku ProxyHandler)
Załóżmy, że chcemy stworzyć proxy, aby pomagał nam w obsłudze transakcji. Bez tego musielibyśmy za każdym razem otwierać i zatwierdzać transakcję. Aby uniknąć duplikacji kodu, możemy do tego wykorzystać proxy.
public class ProxyHandler implements InvocationHandler {
private static final Logger logger = LoggerFactory.getLogger(ProxyHandler.class);
private final Object objectToHandle;
public ProxyHandler(Object objectToHandle) {
this.objectToHandle = objectToHandle;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
beginTransaction();
final Object invoke = method.invoke(objectToHandle, args);
commitTransaction();
return invoke;
} catch (Exception e) {
rollbackTransaction();
throw e;
}
}
private void beginTransaction() {
logger.debug("BEGIN TRANSACTION");
}
private void commitTransaction() {
logger.debug("COMMIT TRANSACTION");
}
private void rollbackTransaction() {
logger.error("ROLLBACK TRANSACTION");
}
}
W metodzie invoke:
* na początku otwieramy transakcję
* wywołujemy metodę, która nas interesuje
* zatwierdzamy zmiany
* lub je wycofujemy w przypadku błędu
Zwróć uwagę
Na potrzeby tego projektu, nie będziemy implementować prawdziwej transakcji przy użyciu EntityManager — uprościmy to do zwykłego loggera.
Gotowe — proxy, które obsługuje transakcje już działa!
2021-06-13 16:45:39,642 [main] DEBUG ProxyHandler:32 - BEGIN TRANSACTION
2021-06-13 16:45:39,643 [main] INFO CompanyServiceImpl:20 - SERVICE: START - create company
2021-06-13 16:45:39,644 [main] INFO CompanyDaoImpl:13 - DAO: START - create company
2021-06-13 16:45:39,645 [main] INFO CompanyDaoImpl:15 - DAO: END - create company
2021-06-13 16:45:39,645 [main] INFO CompanyServiceImpl:22 - SERVICE: END - create company
2021-06-13 16:45:39,645 [main] DEBUG ProxyHandler:36 - COMMIT TRANSACTION
Jak już wspomniałem, w tym projekcie będziemy używać Dynamic Proxy. Jednak przedstawię tworzenie proxy przy pomocy CGLib w ramach ciekawostki.
MethodInterceptorPodobnie jak w przypadku Dynamic Proxy musimy stworzyć klasę, która będzie zarządzała transakcją. W tym celu stworzymy klasę, którą implementuje MethodInterceptor. Posiada ona jedną metodę:
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
z 4 argumentami:
* obj - this z rozszerzonej klasy. Inaczej niż w przypadku Dynamic Proxy: tutaj obiekt, którego chcemy obsłużyć dostajemy w parametrze, nie musimy go dodawać do konstruktora. Jest to oczywiste, bo w przypadku Dynamic Proxy działamy na interfejsie, więc nie mamy dostępu do instancji.
* method – przechwycona metoda.
* args – Argumenty przekazywane do metody (method)
* proxy – Służy do wywołania nadrzędnej metody (super)
Przykładowa implementacja:
public class ProxyMethodInterceptor implements MethodInterceptor {
private static final Logger logger = LoggerFactory.getLogger(ProxyMethodInterceptor.class);
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
try {
beginTransaction();
final Object invoke = proxy.invokeSuper(obj, args);
commitTransaction();
return invoke;
} catch (Exception e) {
rollbackTransaction();
throw e;
}
}
private void beginTransaction() {
logger.debug("BEGIN TRANSACTION");
}
private void commitTransaction() {
logger.debug("COMMIT TRANSACTION");
}
private void rollbackTransaction() {
logger.error("ROLLBACK TRANSACTION");
}
}
Zwróć uwagę
Metodę wywołujemy za pomocą klasy nadrzędnej invokeSuper, w przeciwnym wypadku wpadlibyśmy w nieskończoną rekurencję.
Tworzenie proxy przy pomocy CGLib odbywa się w kilku krokach:
public class Step2bApp {
private static final Logger logger = LoggerFactory.getLogger(Step2bApp.class);
public static void main(String[] args) {
/* 1 */ final Enhancer enhancer = new Enhancer();
/* 2 */ enhancer.setSuperclass(CompanyService.class);
/* 3 */ enhancer.setCallback(new ProxyMethodInterceptor());
/* 4 */ CompanyService companyService = (CompanyService) enhancer.create(new Class[]{CompanyDao.class}, new Object[]{new CompanyDao()});
companyService.createCompany(new Company());
}
}
Enhencer.Zwróć uwagę
Aby uruchomić CGLib w Java 16, musimy dodać JVM option - --illegal-access=permit - https://github.com/cglib/cglib/issues/191
Wiemy już jak działa proxy, więc czas na wstrzykiwanie zależności. Co chcemy osiągnąć? Application Context, który pozwoli nam na pobieranie beanów przy pomocy interfejsów.
public class Step3App {
public static void main(String[] args) {
final ApplicationContext applicationContext = new ApplicationContext(Step3App.class);
final CompanyService companyServiceProxy = applicationContext.getBean(CompanyService.class);
companyServiceProxy.createCompany(new Company());
}
}
Potrzebujemy do tego 2 adnotacji: jedną do definiowania beanów oraz drugą do ich wstrzykiwania:
* Autowired - W Springu istnieje kilka sposobów na wstrzyknięcie zależności. Jednak zalecane jest, aby to robić przez konstruktor, dlatego taki też ustawimy target.
java
@Target(value = ElementType.CONSTRUCTOR)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}
Component - Aby dać znać naszemu frameworkowi, które klasy mają być przez niego zarządzane stworzymy adnotacje @Component. W przypadku Springa jest kilka ta$ claude mcp add Java-Own-Framework---step-by-step \
-- python -m otcore.mcp_server <graph>