Avatar billede hankster Nybegynder
14. marts 2002 - 15:26 Der er 15 kommentarer og
3 løsninger

Ikke for dummies!! (om connection pool)

Jeg har lavet en JSP-side, som kalder igennem en Bean til nogle forretningsklasser (.java) som laver sql-kald til databasen.

Jeg har lavet en connection pool som disse forretningsklasser skal hente connections fra (en arraylist med 10 connections i).

hvordan sikrer jeg at det er den samme connection pool alle besøgende på JSP-siden bruger, og at der ikke oprettes en connection pool til hver besøgende.

forslag ét:
Jeg laver connection poolen som en Bean med applikations scope, men hvordan får jeg fat i bønnen nede i forretningsklasserne?
kan man overhovedet kontakte en konkret bøne fra en .java fil?

forslag to:
Jeg kan lade arraylisten på connection pool klassen være static, men får hver besøgende på JSP-siden så ikke hver deres arraylist med connections?
Avatar billede greybeard Nybegynder
14. marts 2002 - 16:34 #1
Hvad med en singleton?
En private constructor.
En static variabel til at holde referencen til den ene instans.
En static getmetode til at få fat i instansen med.
Avatar billede disky Nybegynder
14. marts 2002 - 18:55 #2
greybeard, din løsning er ikke thread safe.

hankster:

Du bruger denne her, som er en fuldbyrdig connection pool, der tillader flere pools, med forskellige navne osv.
Hvodan den bruges kan ses her:
http://www.webdevelopersjournal.com/columns/connection_pool.html

Men du kan jo kigge på den for at få et overblik over hvordan du opnår det du ønsker.

Men du skal bruge et Singleton pattern til at løse opgaven.


import java.io.*;
import java.sql.*;
import java.util.*;
import java.util.Date;

/**
* This class is a Singleton that provides access to one or many
* connection pools defined in a Property file. A client gets
* access to the single instance through the static getInstance()
* method and can then check-out and check-in connections from a pool.
* When the client shuts down it should call the release() method
* to close all open connections and do other clean up.
*/
public class DBConnectionManager
{
    static private DBConnectionManager instance;      // The single instance
    static private int clients;
   
    private Vector drivers = new Vector();
    private PrintWriter log;
    private Hashtable pools = new Hashtable();
   
    /**
    * Returns the single instance, creating one if it's the
    * first time this method is called.
    *
    * @return DBConnectionManager The single instance.
    */
    static synchronized public DBConnectionManager getInstance()
    {
        if (instance == null)
        {
            instance = new DBConnectionManager();
        }
        clients++;
        return instance;
    }
   
    /**
    * A private constructor since this is a Singleton
    */
    private DBConnectionManager()
    {
        init();
    }
   
    /**
    * Returns a connection to the named pool.
    *
    * @param name The pool name as defined in the properties file
    * @param con The Connection
    */
    public void freeConnection(String name, Connection con)
    {
        DBConnectionPool pool = (DBConnectionPool) pools.get(name);
        if (pool != null)
        {
            pool.freeConnection(con);
        }
    }
   
    /**
    * Returns an open connection. If no one is available, and the max
    * number of connections has not been reached, a new connection is
    * created.
    *
    * @param name The pool name as defined in the properties file
    * @return Connection The connection or null
    */
    public Connection getConnection(String name)
    {
        DBConnectionPool pool = (DBConnectionPool) pools.get(name);
        if (pool != null)
        {
            return pool.getConnection();
        }
        return null;
    }
   
    /**
    * Returns an open connection. If no one is available, and the max
    * number of connections has not been reached, a new connection is
    * created. If the max number has been reached, waits until one
    * is available or the specified time has elapsed.
    *
    * @param name The pool name as defined in the properties file
    * @param time The number of milliseconds to wait
    * @return Connection The connection or null
    */
    public Connection getConnection(String name, long time)
    {
        DBConnectionPool pool = (DBConnectionPool) pools.get(name);
        if (pool != null)
        {
            return pool.getConnection(time);
        }
        return null;
    }
   
    /**
    * Closes all open connections and deregisters all drivers.
    */
    public synchronized void release()
    {
        // Wait until called by the last client
        if (--clients != 0)
        {
            return;
        }
       
        Enumeration allPools = pools.elements();
        while (allPools.hasMoreElements())
        {
            DBConnectionPool pool = (DBConnectionPool) allPools.nextElement();
            pool.release();
        }
        Enumeration allDrivers = drivers.elements();
        while (allDrivers.hasMoreElements())
        {
            Driver driver = (Driver) allDrivers.nextElement();
            try
            {
                DriverManager.deregisterDriver(driver);
                log("Deregistered JDBC driver " + driver.getClass().getName());
            }
            catch (SQLException e)
            {
                log(e, "Can't deregister JDBC driver: " + driver.getClass().getName());
            }
        }
    }
   
    /**
    * Creates instances of DBConnectionPool based on the properties.
    * A DBConnectionPool can be defined with the following properties:
    * <PRE>
    * &lt;poolname&gt;.url        The JDBC URL for the database
    * &lt;poolname&gt;.user        A database user (optional)
    * &lt;poolname&gt;.password    A database user password (if user specified)
    * &lt;poolname&gt;.maxconn    The maximal number of connections (optional)
    * </PRE>
    *
    * @param props The connection pool properties
    */
    private void createPools(Properties props)
    {
        Enumeration propNames = props.propertyNames();
        while (propNames.hasMoreElements())
        {
            String name = (String) propNames.nextElement();
            if (name.endsWith(".url"))
            {
                String poolName = name.substring(0, name.lastIndexOf("."));
                String url = props.getProperty(poolName + ".url");
                if (url == null)
                {
                    log("No URL specified for " + poolName);
                    continue;
                }
                String user = props.getProperty(poolName + ".user");
                String password = props.getProperty(poolName + ".password");
                String maxconn = props.getProperty(poolName + ".maxconn", "0");
                int max;
                try
                {
                    max = Integer.valueOf(maxconn).intValue();
                }
                catch (NumberFormatException e)
                {
                    log("Invalid maxconn value " + maxconn + " for " + poolName);
                    max = 0;
                }
                DBConnectionPool pool =    new DBConnectionPool(poolName, url, user, password, max);
                pools.put(poolName, pool);
                log("Initialized pool " + poolName);
            }
        }
    }
   
    /**
    * Loads properties and initializes the instance with its values.
    */
    private void init()
    {
        InputStream is = getClass().getResourceAsStream("/db.properties");
        Properties dbProps = new Properties();
        try
        {
            dbProps.load(is);
        }
        catch (Exception e)
        {
            System.err.println("Can't read the properties file. Make sure db.properties is in the CLASSPATH");
            return;
        }
        String logFile = dbProps.getProperty("logfile", "DBConnectionManager.log");
        try
        {
            log = new PrintWriter(new FileWriter(logFile, true), true);
        }
        catch (IOException e)
        {
            System.err.println("Can't open the log file: " + logFile);
            log = new PrintWriter(System.err);
        }
        loadDrivers(dbProps);
        createPools(dbProps);
    }
   
    /**
    * Loads and registers all JDBC drivers. This is done by the
    * DBConnectionManager, as opposed to the DBConnectionPool,
    * since many pools may share the same driver.
    *
    * @param props The connection pool properties
    */
    private void loadDrivers(Properties props)
    {
        String driverClasses = props.getProperty("drivers");
        StringTokenizer st = new StringTokenizer(driverClasses);
        while (st.hasMoreElements())
        {
            String driverClassName = st.nextToken().trim();
            try
            {
                Driver driver = (Driver)
                Class.forName(driverClassName).newInstance();
                DriverManager.registerDriver(driver);
                drivers.addElement(driver);
                log("Registered JDBC driver " + driverClassName);
            }
            catch (Exception e)
            {
                log("Can't register JDBC driver: " + driverClassName + ", Exception: " + e);
            }
        }
    }
   
    /**
    * Writes a message to the log file.
    */
    private void log(String msg)
    {
        log.println(new Date() + ": " + msg);
    }
   
    /**
    * Writes a message with an Exception to the log file.
    */
    private void log(Throwable e, String msg)
    {
        log.println(new Date() + ": " + msg);
        e.printStackTrace(log);
    }
   
    /**
    * This inner class represents a connection pool. It creates new
    * connections on demand, up to a max number if specified.
    * It also makes sure a connection is still open before it is
    * returned to a client.
    */
    class DBConnectionPool
    {
        private int checkedOut;
        private Vector freeConnections = new Vector();
        private int maxConn;
        private String name;
        private String password;
        private String URL;
        private String user;
       
        /**
        * Creates new connection pool.
        *
        * @param name The pool name
        * @param URL The JDBC URL for the database
        * @param user The database user, or null
        * @param password The database user password, or null
        * @param maxConn The maximal number of connections, or 0
        *  for no limit
        */
        public DBConnectionPool(String name, String URL, String user, String password, int maxConn)
        {
            this.name = name;
            this.URL = URL;
            this.user = user;
            this.password = password;
            this.maxConn = maxConn;
        }
       
        /**
        * Checks in a connection to the pool. Notify other Threads that
        * may be waiting for a connection.
        *
        * @param con The connection to check in
        */
        public synchronized void freeConnection(Connection con)
        {
            // Put the connection at the end of the Vector
            freeConnections.addElement(con);
            checkedOut--;
            notifyAll();
            log("Free'ed 1 connection from " + name);
        }
       
        /**
        * Checks out a connection from the pool. If no free connection
        * is available, a new connection is created unless the max
        * number of connections has been reached. If a free connection
        * has been closed by the database, it's removed from the pool
        * and this method is called again recursively.
        */
        public synchronized Connection getConnection()
        {
            Connection con=null;
            if (freeConnections.size() > 0)
            {
                // Pick the first Connection in the Vector
                // to get round-robin usage
                con = (Connection) freeConnections.firstElement();
                freeConnections.removeElementAt(0);
                try
                {
                    if (con.isClosed())
                    {
                        log("Removed bad connection from " + name);
                        // Try again recursively
                        con = getConnection();
                    }
                }
                catch (SQLException e)
                {
                    log("Removed bad connection from " + name);
                    // Try again recursively
                    con = getConnection();
                }
            }
            else if (maxConn == 0 || checkedOut < maxConn)
            {
                con = newConnection();
            }
            if (con != null)
            {
                checkedOut++;
            }
            log("Taken 1 connection from " + name);
            return con;
        }
       
        /**
        * Checks out a connection from the pool. If no free connection
        * is available, a new connection is created unless the max
        * number of connections has been reached. If a free connection
        * has been closed by the database, it's removed from the pool
        * and this method is called again recursively.
        * <P>
        * If no connection is available and the max number has been
        * reached, this method waits the specified time for one to be
        * checked in.
        *
        * @param timeout The timeout value in milliseconds
        */
        public synchronized Connection getConnection(long timeout)
        {
            long startTime = new Date().getTime();
            Connection con;
            while ((con = getConnection()) == null)
            {
                try
                {
                    wait(timeout);
                }
                catch (InterruptedException e)
                {}
                if ((new Date().getTime() - startTime) >= timeout)
                {
                    // Timeout has expired
                    return null;
                }
            }
            log("Taken 1 connection from " + name);
            return con;
        }
       
        /**
        * Closes all available connections.
        */
        public synchronized void release()
        {
            Enumeration allConnections = freeConnections.elements();
            while (allConnections.hasMoreElements())
            {
                Connection con = (Connection) allConnections.nextElement();
                try
                {
                    con.close();
                    log("Closed connection for pool " + name);
                }
                catch (SQLException e)
                {
                    log(e, "Can't close connection for pool " + name);
                }
            }
            freeConnections.removeAllElements();
        }
       
        /**
        * Creates a new connection, using a userid and password
        * if specified.
        */
        private Connection newConnection()
        {
            Connection con = null;
            try
            {
                if (user == null)
                {
                    con = DriverManager.getConnection(URL);
                }
                else
                {
                    con = DriverManager.getConnection(URL, user, password);
                }
                log("Created a new connection in pool " + name);
            }
            catch (SQLException e)
            {
                log(e, "Can't create a new connection for " + URL);
                return null;
            }
            return con;
        }
    }
}
Avatar billede greybeard Nybegynder
14. marts 2002 - 21:50 #3
disky: Nu gik spørgsmålet på at sikre sig at der kun var én Thread.pool, ikke på trådhåndtering. Det er jo et generelt selvstændigt problem, så snart der kører mere end én tråd
Avatar billede hankster Nybegynder
15. marts 2002 - 09:45 #4
Tak for responsen, men jeg har ikke fået svaret helt, så jeg kan gøre spørgsmålet færdigt.


Jeg sender lige min kode med nu.. 

Først en klasse hvor jeg overskriver Connection
Derefter kommer Connection poolen
Jeg bruger ingen DBconnectionManager

Det jeg tænker på er at hvis løsningen bliver at gøre min Connection pool til Singleton, vil de connections som poolen opretter så ikke blive nedlagt hvis der ingen bruger er på systemet. Klassen lever vel ikke videre ligesom en bean med application scope.

*************************MIN CONNECTION***************************

package XXX;

import java.sql.*;
import java.util.*;

public class Connection implements java.sql.Connection {

    private java.sql.Connection conn = initCon();
    private boolean ibrug  = false;
    private Glo_ConnectionPool pool;

  public Connection(Glo_ConnectionPool p) {
    this.pool  = p;
  }

  private java.sql.Connection initCon() {
    try {
      String driver = pool.getDriver();
      String url    = pool.getUrl();
      String user  = pool.getUser();
      String pass  = pool.getPass();
      Class.forName(driver);
      return DriverManager.getConnection(url,user,pass);
    }
    catch (Exception e) {return null;}
  }

    public void close() throws SQLException {
        this.ibrug=false;
        pool.connectionTilbage();
    }

    protected boolean måLåne() {
      boolean må = false;
      if (!ibrug) {
        this.ibrug = true;
        må = true;
      }
      return må;
    }




    //metoder som ikke bliver overskrevet af denne klasse:
    public void setTypeMap(Map m) throws SQLException {
      conn.setTypeMap(m);
    }

    public Map getTypeMap() throws SQLException {
      return conn.getTypeMap();
    }

    public CallableStatement prepareCall(String sql,int i1,int i2) throws SQLException {
      return conn.prepareCall(sql,i1,i2);
    }

    public CallableStatement prepareCall(String sql) throws SQLException {
        return conn.prepareCall(sql);
    }

    public PreparedStatement prepareStatement(String sql,int i1,int i2) throws SQLException {
        return conn.prepareStatement(sql,i1,i2);
    }

    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return conn.prepareStatement(sql);
    }

    public Statement createStatement(int i1,int i2) throws SQLException {
        return conn.createStatement(i1,i2);
    }

    public Statement createStatement() throws SQLException {
        return conn.createStatement();
    }

    public String nativeSQL(String sql) throws SQLException {
        return conn.nativeSQL(sql);
    }

    public void setAutoCommit(boolean autoCommit) throws SQLException {
        conn.setAutoCommit(autoCommit);
    }

    public boolean getAutoCommit() throws SQLException {
        return conn.getAutoCommit();
    }

    public void commit() throws SQLException {
        conn.commit();
    }

    public void rollback() throws SQLException {
        conn.rollback();
    }

    public boolean isClosed() throws SQLException {
        return conn.isClosed();
    }

    public DatabaseMetaData getMetaData() throws SQLException {
        return conn.getMetaData();
    }

    public void setReadOnly(boolean readOnly) throws SQLException {
        conn.setReadOnly(readOnly);
    }
 
    public boolean isReadOnly() throws SQLException {
        return conn.isReadOnly();
    }

    public void setCatalog(String catalog) throws SQLException {
        conn.setCatalog(catalog);
    }

    public String getCatalog() throws SQLException {
        return conn.getCatalog();
    }

    public void setTransactionIsolation(int level) throws SQLException {
        conn.setTransactionIsolation(level);
    }

    public int getTransactionIsolation() throws SQLException {
        return conn.getTransactionIsolation();
    }

    public SQLWarning getWarnings() throws SQLException {
        return conn.getWarnings();
    }

    public void clearWarnings() throws SQLException {
        conn.clearWarnings();
    }
}








*************************MIN CONNECTION POOL**********************
package XXX;

import java.sql.*;
import java.util.*;

public class Glo_ConnectionPool {

  final private int poolsize = 10;
  final private int cleanupinterval = 200;
  private int antalBrug = 0;
 
  private ArrayList connections;
  private String driver = "sun.jdbc.odbc.JdbcOdbcDriver";
  private String url    = "jdbc:odbc:MinServer";
  private String user  = "";
  private String pass  = "";

  public Glo_ConnectionPool() {
      connections = new ArrayList();
      for (int i=0; i<poolsize; i++) {
        connections.add(new dk.lec.smartfarm.Connection(this));
      }
  }

  public dk.lec.smartfarm.Connection getConnection() throws SQLException {
    dk.lec.smartfarm.Connection c = null;
    int i = 0;
    while ((c==null)&&(i<connections.size())) {
      c = (dk.lec.smartfarm.Connection)connections.get(i);
      if (!c.måLåne()) {
        c = null;
        i++;
      }
    }
    if (c==null) { //ingen connections er ledige, laver en ny
      dk.lec.smartfarm.Connection con = new dk.lec.smartfarm.Connection(this);
      c.måLåne();  //sætter den til at være ibrug
      connections.add(c);
    }
    return c;
  }

  protected void connectionTilbage() {
    this.antalBrug++;
    if (antalBrug > cleanupinterval) {
      int i=0;
      while ((i < connections.size())||(poolsize <= connections.size())) {
        dk.lec.smartfarm.Connection c = (dk.lec.smartfarm.Connection)connections.get(i);
        if (c.måLåne()) {
          connections.remove(i);
        }
        else {
          i++;
        }
      }
    }   
  }

  public String getDriver()
  {
    return driver;
  }

  public String getPass()
  {
    return pass;
  }

  public String getUrl()
  {
    return url;
  }

  public String getUser()
  {
    return user;
  }

}
Avatar billede hankster Nybegynder
15. marts 2002 - 09:52 #5
I øvrigt: hvad forskel gør det om man laver klassen som singleton eller laver klassen static?
Avatar billede nielsbrinch Nybegynder
15. marts 2002 - 12:47 #6
Eksempel på Singleton:

class S
{
  public static Admin admin = new Admin();
}

En singleton er altså bare en funktionskomponent. Tilgang til den samme instans af admin-objektet kan fås i hele applikationen, ved at skrive f.eks. S.admin.funktionPåAdmin()
Avatar billede disky Nybegynder
15. marts 2002 - 23:30 #7
greybeard:
han snakker om at alle brugere skal have adgang til samme pool og ikke en hver. Altså multiple tråde, da brugere godt kan komme samtidigt.


nielsbrinch:
Det er ikke et singleton pattern, men bare en initialisering af en statisk variable i en klasse.


hankster:
En singleton klasse er en klasse som du IKKE kan instantiere, altså du kan ikke bruge new() til at lave et objekt af den klasse, men du kan kalde klassen og få instancen, dette sikre at der altid kun findes 1 objekt af den klasse.
Hvis du derimod brugt nielsbrinch's eksempel sikre du dig kun at 'admin' er ens for alle objekter af 'S' men ikke at det er et singleton.


Man laver et singleton på f.eks. denne måde:

public class Singleton
{
    static private Singleton instance;

    static synchronized public Singleton getInstance()
    {
        if (instance == null)
        {
            instance = new Singleton();
        }
        return instance;
    }

    private Singleton(){}; //sikre at man IKKE kan bruge new()

}



Hele fidusen er at konstruktoren er private, derved kan den ikke kaldes udefra, derfor kan man ikke instantiere et objekt af typen Singleton.

Men man kan kalde Singleton.getInstance() som returnerer en reference til det absolut eneste objekt af typen Singleton som eksisterer. Hvis der ikke findes et i forvejen bliver det oprettet, ellers bare returneret.

Din pool pakker du ind i sådanne en Singleton, og sikre at der kun findes 1 pool i alt.
Du skal ikke være bange for at garbage collectoren henter dit objekt, da objektet peger på sig selv vil det ikke bliver garbage collectet.

Håber det hjalp lidt, ellers bare spørg.
Avatar billede nielsbrinch Nybegynder
16. marts 2002 - 10:13 #8
Hvor er du dygtig, disky (ja du er)

Men sådan har jeg lært singleton af min javalærer. Det gør det naturligvis ikke rigtigt :-) Men jeg vil nu til stadighed hævde at min singleton virker på samme måde som din, med mindre programmøren ligefrem prøver at gøre noget forkert.

Ved din metode kan programmøren ikke dumme sig, det er forskellen så vidt jeg kan se. Er nøgleordet "indkapsling"?
Avatar billede hankster Nybegynder
17. marts 2002 - 17:04 #9
Jeg takker for al hjælpen. Singleton mønstret var det der skulle til
Avatar billede nielsbrinch Nybegynder
17. marts 2002 - 17:52 #10
Ej, den var nu disky's den her, tror jeg ... men va fa'n.
Avatar billede disky Nybegynder
17. marts 2002 - 18:40 #11
niels: ikke helt

hvis jeg laver en klasse der ser 100% sådanne her ud:

class S
{
  public static Admin admin = new Admin();
}

Så indsætter java selv en defualt constructor, og så kan jeg findt lave følgende i en anden klasse:

S s1=new S();
S s2=new S();

Og det 2 objecter peger IKKE på samme instance, altså din klasse er ikke en singleton

Husk en singleton gælder for en hel klasse,

Måske hvis din admin klasse er et singleton pattern lavet via getInstance så er den, men ikke som du har beskrevet.

Hvis du ikke tror på mig, så køb denne bog:
http://java.sun.com/docs/books/effective/

James Gosling (manden bag java) har sagt han vill ønske han havde haft den bog for mange år siden, den indeholder en frygtelig masse tips og tricks, der iblandt hvordan man laver et rigtigt singleton pattern.

Du må egentligt gerne høre din lærer hvordan han vil dokumentere at der kun kan laves 1 object af type 'S' eller for den sags skyld 'admin' i det eksempel du har postet.
Avatar billede disky Nybegynder
17. marts 2002 - 18:41 #12
Det ville virke i en application, hvor dette var lavet i den fil der indeholder 'main' alle andre steder deriblandt JSP sider, ville det fejle ASAP.
Avatar billede disky Nybegynder
17. marts 2002 - 18:45 #13
beviset er her:

først en main klasse der starter programmet op og laver 2 instancer af 'S' og tester om det er en singleton eller ej:
public class mainKlasse
{
   
    /** Creates new Singleton */
    public mainKlasse()
    {
        S s1=new S();
        S s2=new S();
        if(s1.equals(s2))
        {
            System.out.println("Er en singleton");
        }
        else
        {
            System.out.println("Er ikke en singleton");
        }
        System.out.println("s1 = "+s1);
        System.out.println("s2 = "+s2);
    }
   
    /**
    * @param args the command line arguments
    */
    public static void main(String args[])
    {
        new mainKlasse();
    }
   
}

Så din S klasse

class S
{
  public static Admin admin = new Admin();
}


og en Admin klasse der bare er tom:
public class Admin
{
   
    /** Creates new Admin */
    public Admin()
    {
    }
   
}


prøv at compile de 3 klasse filer og se hvad resultatet er.
Men som sagt jeg vil gerne høre din lærers begrundelse for hvordan det skulle kunne være en singleton, specielt når det bruges på en webside.
Avatar billede disky Nybegynder
17. marts 2002 - 18:53 #14
Avatar billede nielsbrinch Nybegynder
17. marts 2002 - 19:13 #15
Det er ikke meningen man skal lave instanser af min "Singleton" som jeg kalder den. Jeg kan se den alligevel ikke hedder Singleton. Vi kan kalde den Banan i stedet, så.

Min Banan sikrer (hvis man ikke laver instanser af den) at der kun er en instans af admin, og at det er den samme instans alle klasserne arbejder med. Det var vist også det problemet gik på.

Jeg skal nok begynde at kalde min klasse med en statisk variabel noget andet end Singleton - kan godt se det forvirrer.
Avatar billede disky Nybegynder
17. marts 2002 - 19:16 #16
okay så er vi enige :)

Pas dog på ved multitrådning, pga. din manglende synchronized :)
Avatar billede nielsbrinch Nybegynder
17. marts 2002 - 19:42 #17
synchronized er en lille ting jeg ikke har stiftet bekendtskab med endnu
Avatar billede greybeard Nybegynder
17. marts 2002 - 21:09 #18
disky:
Hvis der er mulighed for at flere tråde kan instantiere klassen samtidig, har du selvfølgelig ret.
En bedre måde var måske så at instantiere den under opstart. Derved kan synchronized undgås på getInstance(), da den bliver en ren getmetode.
Generelt bør man begrænse brugen af synchronized til det nødvendige, da den modvirker formålet med flere tråde, nemlig samtidig afvikling.
Avatar billede Ny bruger Nybegynder

Din løsning...

Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.

Loading billede Opret Preview
Kategori
Kurser inden for grundlæggende programmering

Log ind eller opret profil

Hov!

For at kunne deltage på Computerworld Eksperten skal du være logget ind.

Det er heldigvis nemt at oprette en bruger: Det tager to minutter og du kan vælge at bruge enten e-mail, Facebook eller Google som login.

Du kan også logge ind via nedenstående tjenester