Rumah  >  Artikel  >  Java  >  Bagaimana untuk membina SaaS berbilang penyewa Springboot

Bagaimana untuk membina SaaS berbilang penyewa Springboot

WBOY
WBOYke hadapan
2023-05-12 16:49:061618semak imbas

Rangka kerja teknikal

Versi springboot ialah 2.3.4.RELEASE

Lapisan kegigihan menggunakan JPA

Reka bentuk Model Penyewa

Kerana semua penyewa aplikasi saas Menggunakan perkhidmatan dan pangkalan data yang sama, untuk mengasingkan data penyewa, BaseSaasEntity

public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;
    }

dicipta di sini hanya terdapat satu tenantId medan, yang sepadan dengan ID penyewa mewarisi ini kelas ibu bapa. Akhir sekali, tenantId digunakan untuk membezakan penyewa mana yang dimiliki oleh data tersebut.

Penapisan data penyewa SQL

Seperti biasa, selepas jadual dibuat, CURD modul yang sepadan harus diikuti. Tetapi keperluan paling asas untuk aplikasi saas ialah pengasingan data penyewa, iaitu, orang dari syarikat B tidak dapat melihat data syarikat A. Bagaimana untuk menapisnya The BaseSaasEntity yang kami sediakan di atas akan berfungsi di sini dengan membezakan permintaan semasa daripada, Tambah di mana penyewa= kepada semua perniagaan penyewa sql untuk melaksanakan penapisan data penyewa.

Penapis hibernate

Jika kami menambah kod penapisan sql penyewa pada perniagaan kami, beban kerja bukan sahaja besar, tetapi kebarangkalian ralat juga akan tinggi. Yang ideal adalah untuk memproses penyambungan SQL yang ditapis bersama-sama dan membolehkan penapisan SQL pada antara muka perniagaan penyewa. Oleh kerana JPA dilaksanakan oleh hibernate, di sini kita boleh menggunakan beberapa fungsi hibernate

@MappedSuperclass
@Data
@FilterDef(name = "tenantFilter", parameters = {@ParamDef(name = "tenantId", type = "long")})
@Filter(condition = "tenant_id=:tenantId", name = "tenantFilter")
public abstract class BaseSaasEntity {
    @JsonIgnore
    @Column(nullable = false, updatable = false)
    protected Long tenantId;


    @PrePersist
    public void onPrePersist() {
        if (getTenantId() != null) {
            return;
        }
        Long tenantId = TenantContext.getTenantId();
        Check.notNull(tenantId, "租户不存在");
        setTenantId(tenantId);
    }
}

Hibernate3 menyediakan cara inovatif untuk memproses data dengan peraturan "keterlihatan", iaitu Gunakan penapis Hibernate. Penapis hibernate adalah sah secara global, dinamakan penapis yang boleh mengambil parameter Anda boleh memilih sama ada untuk mendayakan (atau melumpuhkan) penapis untuk sesi Hibernate tertentu.

Di sini kami mentakrifkan keadaan penapis sql melalui @FilterDef dan @Filter. Kemudian gunakan anotasi @TenantFilter untuk mengenal pasti bahawa antara muka memerlukan penapisan data

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface TenantFilter {

    boolean readOnly() default true;

}

Ia boleh dilihat bahawa antara muka ini diletakkan pada kaedah, yang sepadan dengan lapisan Pengawal. Kepentingan menambah anotasi transaksi @Transactional ialah transaksi mesti didayakan untuk mengaktifkan penapis hibernasi. Lalai di sini ialah transaksi baca sahaja. Akhir sekali tentukan aspek untuk mengaktifkan penapis

@Aspect
@Slf4j
@RequiredArgsConstructor
public class TenantSQLAspect {
    private static final String FILTER_NAME = "tenantFilter";
    private final EntityManager entityManager;
    @SneakyThrows
    @Around("@annotation(com.lvjusoft.njcommon.annotation.TenantFilter)")
    public Object aspect(ProceedingJoinPoint joinPoint) {
        Session session = entityManager.unwrap(Session.class);
        try {
            Long tenantId = TenantContext.getTenantId();
            Check.notNull(tenantId, "租户不存在");
            session.enableFilter(FILTER_NAME).setParameter("tenantId", tenantId);
            return joinPoint.proceed();
        } finally {
            session.disableFilter(FILTER_NAME);
        }
    }
}

Objek aspek di sini ialah anotasi @TenantFilter yang baru diperibadikan Dapatkan id penyewa semasa sebelum kaedah dilaksanakan dan dayakan penapis Dengan cara ini, data penyewa pengasingan selesai. Hanya perlu tambah anotasi @TenantFilter pada antara muka perniagaan penyewa, dan pembangunan hanya perlu mengambil berat tentang kod perniagaan. TenantContext dalam gambar di atas ialah konteks penyewa urutan semasa Dengan bersetuju dengan bahagian hadapan, id penyewa ditambahkan pada pengepala permintaan antara muka dan pelayan menggunakan pemintas untuk menyimpan id penyewa yang diperoleh dalam ThreadLocal

public class IdentityInterceptor extends HandlerInterceptorAdapter {
    public IdentityInterceptor() {
        log.info("IdentityInterceptor init");
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = request.getHeader(AuthConstant.USER_TOKEN_HEADER_NAME);
        UserContext.setToken(token);
        String tenantId = request.getHeader(AuthConstant.TENANT_TOKEN_HEADER_NAME);
        TenantContext.setTenantUUId(tenantId);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        UserContext.clear();
        TenantContext.clear();
    }
}
<.>Sub-perpustakaan

Apabila bilangan penyewa bertambah, data satu pangkalan data MySQL dan satu jadual pasti akan mencapai kesesakan Di sini, hanya kaedah sub-pangkalan data digunakan. Gunakan berbilang sumber data untuk melakukan pemetaan banyak dengan satu penyewa dan sumber data.

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private Map<Object, Object> targetDataSources;
    public DynamicRoutingDataSource() {
        targetDataSources =new HashMap<>();
        DruidDataSource druidDataSource1 = new DruidDataSource();
        druidDataSource1.setUsername("username");
        druidDataSource1.setPassword("password");
        druidDataSource1.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db1",druidDataSource1);
        
        DruidDataSource druidDataSource2 = new DruidDataSource();
        druidDataSource2.setUsername("username");
        druidDataSource2.setPassword("password");
        druidDataSource2.setUrl("jdbc:mysql://localhost:3306/db?useSSL=false&useUnicode=true&characterEncoding=utf-8");
        targetDataSources.put("db2",druidDataSource1);
        
        this.targetDataSources = targetDataSources;
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    public void addDataSource(String key, DataSource dataSource) {
        if (targetDataSources.containsKey(key)) {
            throw new IllegalArgumentException("dataSource key exist");
        }
        targetDataSources.put(key, dataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContext.getSource();
    }
}

Isytihar sumber data penghalaan dinamik dengan melaksanakan AbstractRoutingDataSource Sebelum rangka kerja menggunakan datesource, spring akan memanggil kaedah determineCurrentLookupKey() untuk menentukan sumber data yang hendak digunakan. DataSourceContext di sini adalah serupa dengan TenantContext di atas Selepas mendapatkan tenantInfo dalam pemintas, cari kunci sumber data yang sepadan dengan penyewa semasa dan tetapkan dalam ThreadLocal.

Atas ialah kandungan terperinci Bagaimana untuk membina SaaS berbilang penyewa Springboot. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam