Let's take a simple example. Let's say two tables named test and customer are there described as:
create table test(
  test_id int(11) not null auto_increment,
  primary key(test_id));
create table customer(
  customer_id int(11) not null auto_increment,
  name varchar(50) not null,
  primary key(customer_id));
One more table is there which keeps the track of tests and customer:
create table tests_purchased(
  customer_id int(11) not null,
  test_id int(11) not null,
  created_date datetime not null,
  primary key(customer_id, test_id));
We can see that in the table tests_purchased the primary key is a composite key, so we will use the <composite-id ...>...</composite-id> tag in the hbm.xml mapping file. So the PurchasedTest.hbm.xml will look like:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
  <class name="entities.PurchasedTest" table="tests_purchased">
    <composite-id name="purchasedTestId">
      <key-property name="testId" column="TEST_ID" />
      <key-property name="customerId" column="CUSTOMER_ID" />
    </composite-id>
    <property name="purchaseDate" type="timestamp">
      <column name="created_date" />
    </property>
  </class>
</hibernate-mapping>
But it doesn't end here. In Hibernate we use session.load (entityClass, id_type_object) to find and load the entity using primary key. In case of composite keys, the ID object should be a separate ID class (in above case a PurchasedTestId class) which just declares the primary key attributes like below:
import java.io.Serializable;
public class PurchasedTestId implements Serializable {
  private Long testId;
  private Long customerId;
  // an easy initializing constructor
  public PurchasedTestId(Long testId, Long customerId) {
    this.testId = testId;
    this.customerId = customerId;
  }
  public Long getTestId() {
    return testId;
  }
  public void setTestId(Long testId) {
    this.testId = testId;
  }
  public Long getCustomerId() {
    return customerId;
  }
  public void setCustomerId(Long customerId) {
    this.customerId = customerId;
  }
  @Override
  public boolean equals(Object arg0) {
    if(arg0 == null) return false;
    if(!(arg0 instanceof PurchasedTestId)) return false;
    PurchasedTestId arg1 = (PurchasedTestId) arg0;
    return (this.testId.longValue() == arg1.getTestId().longValue()) &&
           (this.customerId.longValue() == arg1.getCustomerId().longValue());
  }
  @Override
  public int hashCode() {
    int hsCode;
    hsCode = testId.hashCode();
    hsCode = 19 * hsCode+ customerId.hashCode();
    return hsCode;
  }
}
Important point is that we also implement the two functions hashCode() and equals() as Hibernate relies on them.