socialgekon.com
  • Tärkein
  • Suunnitteluprosessi
  • Muokkaus
  • Prosessi Ja Työkalut
  • Sijoittajat Ja Rahoitus
Web-Käyttöliittymä

Advanced Java Class Tutorial: Opas luokan uudelleenlataamiseen

Sisään Java-kehitysprojektit , tyypilliseen työnkulkuun kuuluu palvelimen uudelleenkäynnistys jokaisen luokan muutoksen yhteydessä, eikä kukaan valittaa siitä. Se on tosiasia Java-kehityksestä. Olemme työskennelleet niin jo ensimmäisestä päivästäni Java-palvelussa. Mutta onko Java-luokan uudelleenlataus niin vaikeaa? Ja voisiko tuo ongelma olla sekä haastava että jännittävä ratkaista taitava Java-kehittäjä ? Tässä Java-luokan opetusohjelmassa yritän ratkaista ongelman, auttaa sinua saamaan kaikki lennon luokan uudelleenlatauksen edut ja parantamaan tuottavuuttasi valtavasti.

Java-luokan uudelleenlataamisesta ei keskustella usein, ja tätä prosessia on tutkittu hyvin vähän. Olen täällä muuttamaan sitä. Tämä Java-luokkien opetusohjelma antaa vaiheittaisen selityksen tästä prosessista ja auttaa sinua hallitsemaan tämän uskomattoman tekniikan. Muista, että Java-luokan uudelleenlatauksen toteuttaminen vaatii paljon huolellisuutta, mutta sen oppiminen vie sinut suuriin liigoihin sekä Java-kehittäjänä että ohjelmistoarkkitehtina. Se ei myöskään satuta ymmärtämään kuinka välttää 10 yleisintä Java-virhettä .

Työtilan asetukset

Kaikki tämän opetusohjelman lähdekoodi on ladattu GitHubiin tässä .



Tarvitset koodin suorittamiseksi, kun noudatat tätä opetusohjelmaa Maven , Mennä ja joko Pimennys tai IntelliJ IDEA .

Jos käytät Eclipse-ohjelmaa:

  • Suorita komento mvn eclipse:eclipse luoda Eclipse-projektitiedostot.
  • Lataa luotu projekti.
  • Aseta lähtöpoluksi target/classes.

Jos käytät IntelliJ: tä:

  • Tuo projektin pom tiedosto.
  • IntelliJ ei käännä automaattisesti, kun käytät jotain esimerkkiä, joten sinun on joko:
  • Suorita esimerkit IntelliJ: n sisällä, ja aina kun haluat koota, sinun on painettava Alt+B E
  • Suorita esimerkit IntelliJ: n ulkopuolella run_example*.bat. Aseta IntelliJ: n kääntäjän automaattinen kääntäminen tosi-arvoksi. Sitten, aina kun vaihdat mitä tahansa Java-tiedostoa, IntelliJ kääntää sen automaattisesti.

Esimerkki 1: Luokan lataaminen uudelleen Java Class Loader -sovelluksella

Ensimmäinen esimerkki antaa sinulle yleisen käsityksen Java-luokan kuormaajasta. Tässä on lähdekoodi.

Seuraavat User luokan määritelmä:

public static class User { public static int age = 10; }

Voimme tehdä seuraavaa:

public static void main(String[] args) { Class userClass1 = User.class; Class userClass2 = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example1.StaticInt$User'); ...

Tässä opetusohjelmassa on kaksi User luokat ladattu muistiin. userClass1 ladataan JVM: n oletusluokan latauslaitteella ja userClass2 käyttämällä DynamicClassLoader, mukautettua luokan kuormaajaa, jonka lähdekoodi on myös GitHub-projektissa, ja jonka kuvaan yksityiskohtaisesti alla.

Tässä on loput main menetelmä:

out.println('Seems to be the same class:'); out.println(userClass1.getName()); out.println(userClass2.getName()); out.println(); out.println('But why there are 2 different class loaders:'); out.println(userClass1.getClassLoader()); out.println(userClass2.getClassLoader()); out.println(); User.age = 11; out.println('And different age values:'); out.println((int) ReflectUtil.getStaticFieldValue('age', userClass1)); out.println((int) ReflectUtil.getStaticFieldValue('age', userClass2)); }

Ja tulos:

Seems to be the same class: qj.blog.classreloading.example1.StaticInt$User qj.blog.classreloading.example1.StaticInt$User But why there are 2 different class loaders: [email protected] [email protected] And different age values: 11 10

Kuten näette täällä, vaikka User luokilla on sama nimi, ne ovat itse asiassa kahta erilaista luokkaa, ja niitä voidaan hallita ja manipuloida itsenäisesti. Ikäarvo, vaikka se on ilmoitettu staattisena, on olemassa kahdessa versiossa, joka liitetään erikseen kullekin luokalle, ja sitä voidaan muuttaa myös itsenäisesti.

Normaalissa Java-ohjelmassa ClassLoader on portaali, joka tuo luokkia JVM: ään. Kun yksi luokka vaatii toisen luokan lataamista, lataaminen on ClassLoader: n tehtävä.

Tässä Java-luokan esimerkissä mukautettu ClassLoader nimetty DynamicClassLoader käytetään lataamaan User toisen version luokassa. Jos DynamicClassLoader: n sijaan käytimme taas oletusluokan kuormaajaa (komennolla StaticInt.class.getClassLoader()), niin sama User class käytetään, koska kaikki ladatut luokat tallennetaan välimuistiin.

Java ClassLoaderin oletustoiminnan tutkiminen DynamicClassLoaderiin nähden on avain hyötyä tästä Java-luokkien opetusohjelmasta.

DynamicClassLoader

Normaalissa Java-ohjelmassa voi olla useita luokkakuormaajia. Se, joka lataa pääluokan, ClassLoader, on oletusarvo, ja koodistasi voit luoda ja käyttää niin monta luokkakuormaajaa kuin haluat. Tämä on siis avain luokan uudelleenlataukseen Java-sovelluksessa. DynamicClassLoader on mahdollisesti koko tämän opetusohjelman tärkein osa, joten meidän on ymmärrettävä, kuinka dynaaminen luokan lataus toimii, ennen kuin voimme saavuttaa tavoitteemme.

Toisin kuin ClassLoader: n oletuskäyttäytyminen, DynamicClassLoader perii aggressiivisemman strategian. Normaali luokan lataaja antaisi vanhemmalleen ClassLoader prioriteetti ja vain kuormaluokat, joita sen vanhempi ei voi ladata. Se sopii normaaleihin olosuhteisiin, mutta ei meidän tapauksessamme. Sen sijaan DynamicClassLoader yrittää käydä läpi kaikki luokkansa polut ja ratkaista kohdeluokan, ennen kuin se luopuu oikeudesta vanhempaansa.

Yllä olevassa esimerkissämme DynamicClassLoader luodaan vain yhdellä luokan polulla: 'target/classes' (nykyisessä hakemistossamme), joten se pystyy lataamaan kaikki kyseisessä paikassa asuvat luokat. Kaikissa luokissa, joita ei ole, sen on viitattava vanhempaan luokkaohjelmaan. Esimerkiksi meidän on ladattava String luokka StaticInt luokka, ja luokan kuormaajalla ei ole pääsyä rt.jar JRE-kansiossamme, joten String käytetään vanhemman luokan kuormaajaluokkaa.

Seuraava koodi on peräisin AggressiveClassLoader , DynamicClassLoader emoluokka ja näyttää missä tämä käyttäytyminen on määritelty.

byte[] newClassData = loadNewClass(name); if (newClassData != null) { loadedClasses.add(name); return loadClass(newClassData, name); } else { unavaiClasses.add(name); return parent.loadClass(name); }

Ota huomioon seuraavat DynamicClassLoader -ominaisuudet:

  • Ladatuilla luokilla on sama suorituskyky ja muut määritteet kuin muilla luokkien oletuslataimen lataamilla luokilla.
  • DynamicClassLoader voidaan kerätä roskiin yhdessä kaikkien ladattujen luokkien ja objektien kanssa.

Mahdollisuuden ladata ja käyttää kahta saman luokan versiota ajattelemme nyt vanhan version poistamista käytöstä ja uuden korvaamista sen korvaamiseksi. Seuraavassa esimerkissä teemme juuri niin ... jatkuvasti.

Esimerkki 2: Luokan lataaminen jatkuvasti

Tämä seuraava Java-esimerkki osoittaa, että JRE voi ladata ja ladata luokkia ikuisesti, vanhojen luokkien ollessa kaatopaikalla, roskat kerätään ja upouusi luokka ladataan kiintolevyltä ja otetaan käyttöön. Tässä on lähdekoodi.

Tässä on pääsilmukka:

public static void main(String[] args) { for (;;) { Class userClass = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example2.ReloadingContinuously$User'); ReflectUtil.invokeStatic('hobby', userClass); ThreadUtil.sleep(2000); } }

Kahden sekunnin välein vanha User luokka poistetaan, uusi ladataan ja sen menetelmä hobby vedottu.

Tässä on User luokan määritelmä:

@SuppressWarnings('UnusedDeclaration') public static class User { public static void hobby() { playFootball(); // will comment during runtime // playBasketball(); // will uncomment during runtime } // will comment during runtime public static void playFootball() { System.out.println('Play Football'); } // will uncomment during runtime // public static void playBasketball() { // System.out.println('Play Basketball'); // } }

Suorita tämä sovellus, yritä kommentoida User -koodissa ilmoitettua koodia ja poistaa kommentti luokassa. Huomaat, että aina käytetään uusinta määritelmää.

Tässä on esimerkki tuotoksesta:

... Play Football Play Football Play Football Play Basketball Play Basketball Play Basketball

Joka kerta, kun DynamicClassLoader on luotu, se lataa User luokka target/classes kansio, jossa olemme asettaneet Eclipse tai IntelliJ tuottamaan uusimman luokkatiedoston. Kaikki vanhat DynamicClassLoader s ja vanhat User luokat irrotetaan ja ne joutuvat roskakoriin.

On erittäin tärkeää, että edistyneet Java-kehittäjät ymmärtävät dynaamisen luokan uudelleenlatauksen, riippumatta siitä, onko se aktiivinen vai linkittymätön.

Jos olet perehtynyt JVM HotSpotiin, tässä on huomionarvoista, että luokan rakennetta voidaan myös muuttaa ja ladata uudelleen: playFootball menetelmä on poistettava ja playBasketball menetelmä lisätty. Tämä eroaa HotSpotista, joka sallii vain menetelmän sisällön muuttamisen, tai luokkaa ei voida ladata uudelleen.

Nyt kun voimme ladata luokan uudelleen, on aika yrittää ladata useita luokkia kerralla. Kokeillaan seuraavassa esimerkissä.

Esimerkki 3: Useiden luokkien lataaminen uudelleen

Tämän esimerkin tulos on sama kuin esimerkissä 2, mutta se näyttää kuinka tämä käyttäytyminen voidaan toteuttaa sovelluksen tyyppisemmässä rakenteessa, jossa on konteksti-, palvelu- ja malliobjekteja. Tämän esimerkin lähdekoodi on melko suuri, joten olen osoittanut vain osan siitä täällä. Koko lähdekoodi on tässä .

Tässä on main menetelmä:

public static void main(String[] args) { for (;;) { Object context = createContext(); invokeHobbyService(context); ThreadUtil.sleep(2000); } }

Ja menetelmä createContext:

private static Object createContext() { Class contextClass = new DynamicClassLoader('target/classes') .load('qj.blog.classreloading.example3.ContextReloading$Context'); Object context = newInstance(contextClass); invoke('init', context); return context; }

Menetelmä invokeHobbyService:

private static void invokeHobbyService(Object context) { Object hobbyService = getFieldValue('hobbyService', context); invoke('hobby', hobbyService); }

Ja tässä on Context luokka:

public static class Context { public HobbyService hobbyService = new HobbyService(); public void init() { // Init your services here hobbyService.user = new User(); } }

Ja HobbyService luokka:

public static class HobbyService { public User user; public void hobby() { user.hobby(); } }

Context luokka tässä esimerkissä on paljon monimutkaisempi kuin User luokka edellisissä esimerkeissä: sillä on linkkejä muihin luokkiin ja sillä on init menetelmä kutsutaan jokaiseksi se on instantisoitu. Pohjimmiltaan se on hyvin samanlainen kuin reaalimaailman sovelluksen kontekstiluokat (joka seuraa sovelluksen moduuleja ja tekee riippuvuusinjektioita). Joten pystymme lataamaan tämän Context luokka yhdessä kaikkien siihen liittyvien luokkien kanssa on hieno askel kohti tämän tekniikan soveltamista tosielämässä.

Java-luokan uudelleenlataus on vaikeaa edes edistyneille Java-insinööreille.

Kun luokkien ja objektien määrä kasvaa, myös vaihe 'vanhojen versioiden pudottaminen' vaikeutuu. Tämä on myös suurin syy siihen, miksi luokan uudelleenlataus on niin vaikeaa. Jos haluat pudottaa vanhat versiot, meidän on varmistettava, että kun uusi konteksti on luotu, kaikki viittaukset vanhoihin luokkiin ja esineisiin hylätään. Kuinka voimme käsitellä tätä tyylikkäästi?

main menetelmä tässä pitää sisällön kontekstiobjektin ja se on ainoa linkki kaikkiin pudotettaviin asioihin. Jos katkaiset linkin, kontekstiobjekti ja kontekstiluokka sekä palveluobjekti… altistuvat kaikki roskakorille.

Pieni selitys siitä, miksi luokat yleensä ovat niin sitkeitä eivätkä kerää roskia:

  • Normaalisti lataamme kaikki luokkamme Java-oletuslataimeen.
  • Class-classloader-suhde on kaksisuuntainen, ja luokan kuormaaja tallentaa välimuistiin myös kaikki ladatut luokat.
  • Joten niin kauan kuin classloader on edelleen kytketty mihinkään live-säikeeseen, kaikki (kaikki ladatut luokat) ovat immuuneja roskien kerääjälle.
  • Tästä huolimatta, ellemme pysty erottamaan koodia, jonka haluamme ladata, oletuslatauslaitteen jo lataamasta koodista, uusia koodimuutoksia ei koskaan käytetä ajon aikana.

Tässä esimerkissä näemme, että kaikkien sovellusten luokkien uudelleenlataus on itse asiassa melko helppoa. Tavoitteena on vain pitää ohut, pudotettava yhteys aktiivisesta säikeestä käytössä olevaan dynaamiseen luokan kuormaajaan. Mutta entä jos haluamme joidenkin esineiden (ja niiden luokkien) olevan ei voidaan ladata uudelleen ja käyttää uudelleen uudelleenlataussyklien välillä? Katsotaanpa seuraavaa esimerkkiä.

Esimerkki 4: Pysyvien ja uudelleen ladattujen luokkatilojen erottaminen

Tässä on lähdekoodi. .

main menetelmä:

public static void main(String[] args) { ConnectionPool pool = new ConnectionPool(); for (;;) { Object context = createContext(pool); invokeService(context); ThreadUtil.sleep(2000); } }

Joten voit nähdä, että temppu tässä ladataan ConnectionPool luokka ja sen ilmentäminen uudelleenlataussyklin ulkopuolella pitämällä se pysyvässä tilassa ja välittää viittaus Context esineitä

createContext menetelmä on myös hieman erilainen:

private static Object createContext(ConnectionPool pool) { ExceptingClassLoader classLoader = new ExceptingClassLoader( (className) -> className.contains('.crossing.'), 'target/classes'); Class contextClass = classLoader.load('qj.blog.classreloading.example4.reloadable.Context'); Object context = newInstance(contextClass); setFieldValue(pool, 'pool', context); invoke('init', context); return context; }

Tästä lähtien kutsumme objektien ja luokkien, jotka ladataan jokaisen jakson aikana, 'uudelleen ladattavaksi tilaksi' ja muita - esineitä ja luokkia, joita ei kierrätetä eikä uusita uudelleenlataussyklien aikana - 'pysyväksi tilaksi'. Meidän on oltava hyvin selkeitä siitä, mitkä objektit tai luokat pysyvät missä tilassa, vetämällä siten erotusviiva näiden kahden tilan välille.

Ellei sitä käsitellä oikein, Java-luokan lataamisen erottaminen voi johtaa epäonnistumiseen.

Kuten kuvasta näkyy, Context eivät ole vain esine ja UserService objekti viittaa ConnectionPool esine, mutta Context ja UserService luokat viittaavat myös ConnectionPool luokassa. Tämä on erittäin vaarallinen tilanne, joka johtaa usein sekaannukseen ja epäonnistumiseen. ConnectionPool luokkaa ei saa ladata DynamicClassLoader, vain yksi ConnectionPool luokka muistiin, joka on oletusarvoisesti ladattu ClassLoader Tämä on yksi esimerkki siitä, miksi on niin tärkeää olla varovainen suunniteltaessa luokan uudelleenlatausarkkitehtuuria Javalassa.

Entä jos DynamicClassLoader lataa vahingossa ConnectionPool luokka? Sitten ConnectionPool objektia pysyvästä avaruudesta ei voida siirtää Context esine, koska Context object odottaa toisen luokan objektia, joka on myös nimetty ConnectionPool, mutta on itse asiassa erilainen luokka!

Joten miten estämme DynamicClassLoader lataamasta ConnectionPool luokka? Tässä esimerkissä käytetään DynamicClassLoader: n käyttämisen sijaan sen alaluokkaa nimeltä: ExceptingClassLoader, joka siirtää latauksen superlatauslaitteelle ehtofunktion perusteella:

(className) -> className.contains('$Connection')

Jos emme käytä ExceptingClassLoader täällä, sitten DynamicClassLoader lataa ConnectionPool luokka, koska kyseinen luokka sijaitsee 'target/classes' kansio. Toinen tapa estää ConnectionPool luokka noutaa DynamicClassLoader on koota ConnectionPool luokka toiseen kansioon, ehkä eri moduuliin, ja se kootaan erikseen.

Tilan valitsemisen säännöt

Nyt Java-luokan lataustyö muuttuu todella hämmentäväksi. Kuinka määritämme, minkä luokkien tulisi olla pysyvässä tilassa ja mitkä luokissa uudelleen ladattavassa tilassa? Tässä ovat säännöt:

  1. Uudelleen ladattavan tilan luokka voi viitata luokkaan pysyvässä tilassa, mutta luokka pysyvässä tilassa ei voi koskaan viittaa luokkaan ladattavassa tilassa. Edellisessä esimerkissä ladattava Context luokan viittaukset jatkuvat ConnectionPool luokka, mutta ConnectionPool ei viittaa Context
  2. Luokka voi olla olemassa kummassakin tilassa, jos se ei viittaa mihinkään luokkaan toisessa tilassa. Esimerkiksi hyötyluokka, jossa on kaikki staattiset menetelmät, kuten StringUtils voidaan ladata kerran pysyvään tilaan ja ladata erikseen uudelleen ladattavaan tilaan.

Joten voit nähdä, että säännöt eivät ole kovin rajoittavia. Lukuun ottamatta ylitysluokkia, joiden kohteisiin viitataan kahden tilan yli, kaikkia muita luokkia voidaan vapaasti käyttää joko pysyvässä tilassa tai uudelleen ladattavassa tilassa tai molemmissa. Tietenkin vain ladattavan tilan luokat nauttivat lataamisesta uudelleenlataussyklien avulla.

Joten käsitellään haastavinta luokan uudelleenlatauksen ongelmaa. Seuraavassa esimerkissä yritämme soveltaa tätä tekniikkaa yksinkertaiseen verkkosovellukseen ja nauttia Java-luokkien lataamisesta aivan kuten mikä tahansa komentosarjakieli.

Esimerkki 5: Pieni puhelinluettelo

Tässä on lähdekoodi. .

Tämä esimerkki on hyvin samanlainen kuin miltä normaalin verkkosovelluksen pitäisi näyttää. Se on yhden sivun sovellus, jossa on AngularJS, SQLite, Maven ja Laiturin sulautettu verkkopalvelin .

Tässä on ladattava tila verkkopalvelimen rakenteessa:

Verkkopalvelimen rakenteen uudelleen ladattavan tilan perusteellinen tuntemus auttaa hallitsemaan Java-luokan lataamisen.

Verkkopalvelimessa ei ole viitteitä todellisiin palvelinsovelluksiin, joiden on pysyttävä ladattavassa tilassa, jotta ne voidaan ladata uudelleen. Se pitää sisällään tynkäpalvelimia, jotka jokaisen palvelumenetelmän kutsun yhteydessä ratkaisevat todellisen servletin todellisessa yhteydessä suoritettavaksi.

Tämä esimerkki tuo myös uuden objektin ReloadingWebContext, joka tarjoaa verkkopalvelimelle kaikki arvot kuten tavallinen konteksti, mutta pitää sisäisesti viitteitä todelliseen kontekstiobjektiin, jonka DynamicClassLoader voi ladata uudelleen. Juuri tämä ReloadingWebContext jotka tarjoavat tynkäpalvelimia web-palvelimelle.

ReloadingWebContext käsittelee tynkäpalvelimia web-palvelimelle Java-luokan uudelleenlatausprosessissa.

ReloadingWebContext on todellisen kontekstin kääre, ja:

  • Lataa todellisen kontekstin uudelleen, kun kutsutaan HTTP GET -kohtaan '/'.
  • Tarjoaa tynkäpalvelimia web-palvelimelle.
  • Asettaa arvot ja käyttää menetelmiä joka kerta, kun todellinen konteksti alustetaan tai tuhotaan.
  • Voidaan konfiguroida lataamaan konteksti uudelleen vai ei, ja mitä luokkakuormaajaa käytetään uudelleenlataamiseen. Tämä auttaa sovellusta suoritettaessa tuotannossa.

Koska on erittäin tärkeää ymmärtää, kuinka eristämme pysyvän tilan ja ladattavan tilan, tässä on kaksi luokkaa, jotka ylittävät kahden tilan:

Luokka qj.util.funct.F0 kohteelle public F0 connF sisään Context

  • Toimintoobjekti palauttaa yhteyden aina, kun toiminto käynnistetään. Tämä luokka sijaitsee qj.util-paketissa, joka ei kuulu DynamicClassLoader -ryhmään.

Luokka java.sql.Connection kohteelle public F0 connF sisään Context

  • Normaali SQL-yhteysobjekti. Tämä luokka ei asu DynamicClassLoader -luokan polulla, joten sitä ei oteta.

Yhteenveto

Tässä Java-luokkien opetusohjelmassa olemme nähneet, kuinka voit ladata yhden luokan uudelleen, ladata yhden luokan jatkuvasti, ladata koko luokan useita luokkia ja ladata useita luokkia erillään luokista, joita on jatkettava. Näillä työkaluilla avainasemassa luotettavan luokan uudelleenlatauksen saavuttamisessa on erittäin puhdas muotoilu. Sitten voit vapaasti manipuloida luokkiasi ja koko JVM: ää.

Java-luokan uudelleenlatauksen toteuttaminen ei ole helpoin asia maailmassa. Mutta jos annat sille laukauksen ja jostain hetkestä löydät luokkasi ladattavan lennossa, niin olet melkein siellä jo. Tehtävää on jäljellä hyvin vähän, ennen kuin saavutat täysin upean puhtaan suunnittelun järjestelmällesi.

Onnea ystäväni ja nauti uudesta supervoimastasi!

Estetiikka ja havainnointi - Kuinka lähestyä käyttökokemuksia

Ux-Suunnittelu

Estetiikka ja havainnointi - Kuinka lähestyä käyttökokemuksia
Tietorakenteen periaatteet mobiililaitteille (infografiikan kanssa)

Tietorakenteen periaatteet mobiililaitteille (infografiikan kanssa)

Ux-Suunnittelu

Suosittu Viestiä
Init.js: Opas Full-Stack-JavaScriptin miksi ja miten
Init.js: Opas Full-Stack-JavaScriptin miksi ja miten
Taso ylöspäin - opas pelin käyttöliittymään (infografiikan kanssa)
Taso ylöspäin - opas pelin käyttöliittymään (infografiikan kanssa)
Tarvitset sankarin: projektipäällikkö
Tarvitset sankarin: projektipäällikkö
Viimeinen opas päivämäärän ja ajan manipulointiin
Viimeinen opas päivämäärän ja ajan manipulointiin
Kuinka poseeraa kuvissa, jotta ne näyttävät rennolta ja luonnolliselta
Kuinka poseeraa kuvissa, jotta ne näyttävät rennolta ja luonnolliselta
 
Käytännöllinen lähestymistapa pelisuunnitteluun
Käytännöllinen lähestymistapa pelisuunnitteluun
Kuinka rakentaa vahva etätyökulttuuri: Haastattelu Christy Schumannin kanssa
Kuinka rakentaa vahva etätyökulttuuri: Haastattelu Christy Schumannin kanssa
Kannustaminen toimintaan ja oikea-aikaisuuteen etätyössä
Kannustaminen toimintaan ja oikea-aikaisuuteen etätyössä
Kuinka rekrytoida UX-tutkimuksen osallistujia
Kuinka rekrytoida UX-tutkimuksen osallistujia
Monimutkainen mutta kiinteä: Katsaus kiinteistöjen vesiputouksiin
Monimutkainen mutta kiinteä: Katsaus kiinteistöjen vesiputouksiin
Luokat
Tuotteen ElinkaariTeknologiaUi-SuunnitteluProsessi Ja TyökalutKaukosäätimen NousuiOS-vinkkejäSuunnitteluKetterä KykyOngelmien karttoittaminenSuunnittelu Ja Ennustaminen

© 2023 | Kaikki Oikeudet Pidätetään

socialgekon.com